Initial attempt at a nom parser
This commit is contained in:
commit
b3d810484e
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target
|
||||
32
Cargo.lock
generated
Normal file
32
Cargo.lock
generated
Normal file
@ -0,0 +1,32 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parse"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"nom",
|
||||
]
|
||||
7
Cargo.toml
Normal file
7
Cargo.toml
Normal file
@ -0,0 +1,7 @@
|
||||
[package]
|
||||
name = "parse"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
nom = "7.*"
|
||||
398
src/main.rs
Normal file
398
src/main.rs
Normal file
@ -0,0 +1,398 @@
|
||||
use nom::IResult;
|
||||
use nom::branch::{
|
||||
alt,
|
||||
};
|
||||
pub use nom::bytes::complete::{
|
||||
tag,
|
||||
tag_no_case,
|
||||
};
|
||||
pub use nom::character::complete::{
|
||||
digit1,
|
||||
space0,
|
||||
space1,
|
||||
};
|
||||
use nom::combinator::opt;
|
||||
use nom::sequence::{
|
||||
pair,
|
||||
preceded,
|
||||
};
|
||||
use nom::sequence::tuple;
|
||||
use std::fmt::{
|
||||
Debug,
|
||||
Display,
|
||||
};
|
||||
|
||||
pub fn do_nothing_parser(input: &str) -> IResult<&str, &str> {
|
||||
Ok((input, ""))
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Time {
|
||||
pub h: i8,
|
||||
pub m: i8,
|
||||
}
|
||||
|
||||
impl Display for Time {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:02}:{:02}", self.h, self.m)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Duration {
|
||||
pub start: Time,
|
||||
pub end: Time,
|
||||
}
|
||||
|
||||
impl Display for Duration {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:02}:{:02} - {:02}:{:02}", self.start.h, self.start.m, self.end.h, self.end.m)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Amount {
|
||||
pub volume: i16,
|
||||
}
|
||||
|
||||
impl Display for Amount {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}ml", self.volume)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Fed {
|
||||
pub formula: Option<Amount>,
|
||||
pub whole_milk: Option<Amount>,
|
||||
pub left: Option<Amount>,
|
||||
}
|
||||
|
||||
impl Display for Fed {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "Fed {}ml", self.formula.as_ref().unwrap().volume)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_time(input: &str) -> IResult<&str, Time> {
|
||||
let (rest, (h, _, m)) = tuple((
|
||||
digit1,
|
||||
tag(":"),
|
||||
digit1,
|
||||
))(input)?;
|
||||
Ok((rest, Time { h: h.parse().unwrap(), m: m.parse().unwrap() }))
|
||||
}
|
||||
|
||||
fn parse_duration(input: &str) -> IResult<&str, Duration> {
|
||||
let (rest, (start, _, end)) = tuple((
|
||||
parse_time,
|
||||
tuple((
|
||||
space0,
|
||||
tag("-"),
|
||||
space0,
|
||||
)),
|
||||
parse_time,
|
||||
))(input)?;
|
||||
Ok((rest, Duration { start, end }))
|
||||
}
|
||||
|
||||
fn parse_at_or_at(input: &str) -> IResult<&str, &str> {
|
||||
let (rest, (_, at, _)) = tuple((
|
||||
space0,
|
||||
alt((
|
||||
tag("@"),
|
||||
tag("at"),
|
||||
)),
|
||||
space0,
|
||||
))(input)?;
|
||||
Ok((rest, at))
|
||||
}
|
||||
|
||||
fn parse_timestamp(input: &str) -> IResult<&str, Duration> {
|
||||
let (rest, (_, duration)) = tuple((
|
||||
parse_at_or_at,
|
||||
parse_duration,
|
||||
))(input)?;
|
||||
Ok((rest, duration))
|
||||
}
|
||||
|
||||
pub fn parse_amount(input: &str) -> IResult<&str, Amount> {
|
||||
let (rest, (amount, _ml)) = pair(digit1, tag("ml"))(input)?;
|
||||
println!("Parsed amount: {}", &amount);
|
||||
let volume = amount.parse().unwrap();
|
||||
Ok((rest, Amount { volume }))
|
||||
}
|
||||
|
||||
pub fn parse_main_amount(input: &str) -> IResult<&str, Amount> {
|
||||
let (rest, (amount, _)) = pair(
|
||||
parse_amount,
|
||||
opt(alt((
|
||||
tag_no_case(" premixed formula"),
|
||||
tag_no_case(" formula"),
|
||||
))),
|
||||
)(input)?;
|
||||
Ok((rest, amount))
|
||||
}
|
||||
|
||||
pub fn parse_second_amount(input: &str) -> IResult<&str, Amount> {
|
||||
let (rest, (amount, _)) = pair(
|
||||
parse_amount,
|
||||
opt(alt((
|
||||
tag_no_case(" whole milk"),
|
||||
tag_no_case(" milk"),
|
||||
))),
|
||||
)(input)?;
|
||||
Ok((rest, amount))
|
||||
}
|
||||
|
||||
pub fn parse_left_amount(input: &str) -> IResult<&str, Amount> {
|
||||
let (rest, (amount, _)) = pair(
|
||||
preceded(
|
||||
space1,
|
||||
parse_amount,
|
||||
),
|
||||
opt(alt((
|
||||
tag_no_case(" left"),
|
||||
))),
|
||||
)(input)?;
|
||||
Ok((rest, amount))
|
||||
}
|
||||
|
||||
pub fn parse_and_plus(input: &str) -> IResult<&str, &str> {
|
||||
alt((
|
||||
tag(" and "),
|
||||
tag(" + "),
|
||||
tag_no_case(" with "),
|
||||
))(input)
|
||||
}
|
||||
|
||||
pub fn parse_feed(input: &str) -> IResult<&str, Fed> {
|
||||
let (rest, (main, plus, _timestamp, left)) = tuple((
|
||||
preceded(
|
||||
tag("Fed "),
|
||||
parse_main_amount,
|
||||
),
|
||||
opt(preceded(
|
||||
parse_and_plus,
|
||||
parse_second_amount,
|
||||
)),
|
||||
parse_timestamp,
|
||||
opt(parse_left_amount),
|
||||
))(input)?;
|
||||
let fed = Fed {
|
||||
formula: Some(main),
|
||||
whole_milk: plus,
|
||||
left: left,
|
||||
};
|
||||
Ok((rest, fed))
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let tests = vec![
|
||||
"Fed 180ml @ 11:05-11:25",
|
||||
"Fed 200ml at 14:35-14:46",
|
||||
"Fed 180ml formula @ 00:30-00:45",
|
||||
"Fed 200ml premixed formula @ 14:58-15:15",
|
||||
"Fed 120ml and 90ml whole milk at 03:40-03:50",
|
||||
"Fed 150ml + 50ml whole milk @ 18:29-18:40",
|
||||
"Fed 150ml with 50ml whole milk at 01:16-01:27 40ml left",
|
||||
"Fed 90ml formula @ 10:00-10:10",
|
||||
"Fed 120ml formula at 13:30-13:50",
|
||||
"Fed 180ml formula @ 17:00-17:20",
|
||||
"Fed 110ml and 90ml milk at 13:30-13:50",
|
||||
];
|
||||
for test in tests {
|
||||
println!("Testing: {}", &test);
|
||||
let result = parse_feed(test)?;
|
||||
println!("Result: {:?}", &result);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_time_valid() {
|
||||
let test_cases = vec![
|
||||
("12:30", Time { h: 12, m: 30 }),
|
||||
("01:05", Time { h: 1, m: 5 }),
|
||||
("23:59", Time { h: 23, m: 59 }),
|
||||
("00:00", Time { h: 0, m: 0 }),
|
||||
("9:45", Time { h: 9, m: 45 }),
|
||||
// Nb. Nothings stoping wonky times being parsed as valid..
|
||||
("123:126", Time { h: 123, m: 126 }),
|
||||
];
|
||||
|
||||
for (input, expected) in test_cases {
|
||||
let result = parse_time(input);
|
||||
assert!(result.is_ok(), "Failed to parse '{}'", input);
|
||||
let (rest, time) = result.unwrap();
|
||||
assert_eq!(time.h, expected.h, "Incorrect hours parsed for '{}'", input);
|
||||
assert_eq!(time.m, expected.m, "Incorrect minutes parsed for '{}'", input);
|
||||
assert!(rest.is_empty(), "Should consume entire input for '{}'", input);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_duration_valid() {
|
||||
let test_cases = vec![
|
||||
("12:30-12:45", Duration { start: Time { h: 12, m: 30 }, end: Time { h: 12, m: 45 } }),
|
||||
("10:30-15:05", Duration { start: Time { h: 10, m: 30 }, end: Time { h: 15, m: 05 } }),
|
||||
("2:30-2:05", Duration { start: Time { h: 2, m: 30 }, end: Time { h: 2, m: 5 } }),
|
||||
];
|
||||
|
||||
for (input, expected) in test_cases {
|
||||
let result = parse_duration(input);
|
||||
assert!(result.is_ok(), "Failed to parse duration '{}'", input);
|
||||
let (rest, duration) = result.unwrap();
|
||||
assert_eq!(duration.start.h, expected.start.h, "Incorrect start hours parsed for '{}'", input);
|
||||
assert_eq!(duration.start.m, expected.start.m, "Incorrect start minutes parsed for '{}'", input);
|
||||
assert_eq!(duration.end.h, expected.end.h, "Incorrect end hours parsed for '{}'", input);
|
||||
assert_eq!(duration.end.m, expected.end.m, "Incorrect end minutes parsed for '{}'", input);
|
||||
assert!(rest.is_empty(), "Should have consumed entire input for '{}'", input);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_at_or_at() {
|
||||
let test_cases = vec![
|
||||
("@", "@"),
|
||||
(" @ ", "@"),
|
||||
("at", "at"),
|
||||
(" at ", "at"),
|
||||
];
|
||||
for (input, expected) in test_cases {
|
||||
let result = parse_at_or_at(input);
|
||||
assert!(result.is_ok(), "Somehow invalid? '{}'", input);
|
||||
let (rest, at) = result.unwrap();
|
||||
assert_eq!(at, expected, "Should be the same type of @ or at for '{}'", input);
|
||||
assert!(rest.is_empty(), "Should have consumed entire input for '{}'", input);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_feed() {
|
||||
let test_cases = vec![
|
||||
(
|
||||
"Fed 150ml with 50ml whole milk at 01:16-01:27 40ml left",
|
||||
"",
|
||||
Fed {
|
||||
formula: Some(Amount { volume: 150 }),
|
||||
whole_milk: Some(Amount { volume: 50 }),
|
||||
left: Some(Amount { volume: 40 }),
|
||||
},
|
||||
),
|
||||
];
|
||||
for (input, remaining, output) in test_cases {
|
||||
let result = parse_feed(input);
|
||||
assert!(result.is_ok(), "Parsing failed");
|
||||
let (rest, actual) = result.unwrap();
|
||||
assert_eq!(remaining, rest);
|
||||
|
||||
if let Some(expected_formula) = output.formula {
|
||||
assert!(actual.formula.is_some(), "Expected to have formula");
|
||||
assert_eq!(expected_formula.volume, actual.formula.unwrap().volume);
|
||||
} else {
|
||||
assert!(actual.formula.is_none(), "Wasn't expecting formula");
|
||||
}
|
||||
|
||||
if let Some(expected_whole_milk) = output.whole_milk {
|
||||
assert!(actual.whole_milk.is_some(), "Expected to have whole_milk");
|
||||
assert_eq!(expected_whole_milk.volume, actual.whole_milk.unwrap().volume);
|
||||
} else {
|
||||
assert!(actual.whole_milk.is_none(), "Wasn't expecting whole_milk");
|
||||
}
|
||||
|
||||
if let Some(expected_left) = output.left {
|
||||
assert!(actual.left.is_some(), "Expected to have left");
|
||||
assert_eq!(expected_left.volume, actual.left.unwrap().volume);
|
||||
} else {
|
||||
assert!(actual.left.is_none(), "Wasn't expecting left");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! make_feed_test {
|
||||
(
|
||||
$name:ident,
|
||||
$input:expr,
|
||||
$formula:expr,
|
||||
$whole_milk:expr,
|
||||
$left:expr $(,)?
|
||||
) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
let result = parse_feed($input);
|
||||
assert!(result.is_ok(), "Parsing failed");
|
||||
|
||||
let (rest, actual) = result.unwrap();
|
||||
assert_eq!("", rest);
|
||||
|
||||
// formula
|
||||
match ($formula, actual.formula) {
|
||||
(Some(expected), Some(actual)) => {
|
||||
assert_eq!(expected.volume, actual.volume, "formula mismatch");
|
||||
}
|
||||
(None, None) => {}
|
||||
(Some(_), None) => panic!("Expected formula but got none"),
|
||||
(None, Some(_)) => panic!("Unexpected formula"),
|
||||
}
|
||||
|
||||
// whole_milk
|
||||
match ($whole_milk, actual.whole_milk) {
|
||||
(Some(expected), Some(actual)) => {
|
||||
assert_eq!(expected.volume, actual.volume, "whole_milk mismatch");
|
||||
}
|
||||
(None, None) => {}
|
||||
(Some(_), None) => panic!("Expected whole_milk but got none"),
|
||||
(None, Some(_)) => panic!("Unexpected whole_milk"),
|
||||
}
|
||||
|
||||
// left
|
||||
match ($left, actual.left) {
|
||||
(Some(expected), Some(actual)) => {
|
||||
assert_eq!(expected.volume, actual.volume, "left mismatch");
|
||||
}
|
||||
(None, None) => {}
|
||||
(Some(_), None) => panic!("Expected left but got none"),
|
||||
(None, Some(_)) => panic!("Unexpected left"),
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
make_feed_test!(
|
||||
test_parse_feed_basic,
|
||||
"Fed 150ml with 50ml whole milk at 01:16-01:27 40ml left",
|
||||
Some(Amount { volume: 150 }),
|
||||
Some(Amount { volume: 50 }),
|
||||
Some(Amount { volume: 40 }),
|
||||
);
|
||||
|
||||
make_feed_test!(
|
||||
test_parse_feed_2,
|
||||
"Fed 150ml + 50ml whole milk at 01:16-01:27 40ml left",
|
||||
Some(Amount { volume: 150 }),
|
||||
Some(Amount { volume: 50 }),
|
||||
Some(Amount { volume: 40 }),
|
||||
);
|
||||
|
||||
make_feed_test!(
|
||||
test_parse_feed_3,
|
||||
"Fed 150ml + 50ml milk at 01:16-01:27",
|
||||
Some(Amount { volume: 150 }),
|
||||
Some(Amount { volume: 50 }),
|
||||
None::<Amount>,
|
||||
);
|
||||
|
||||
make_feed_test!(
|
||||
test_parse_feed_5,
|
||||
"Fed 150ml and 25ml at 01:16-01:27",
|
||||
Some(Amount { volume: 150 }),
|
||||
Some(Amount { volume: 25 }),
|
||||
None::<Amount>,
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user