From 67f77ce160f85276bf5b5c80a3f080a8d96119b9 Mon Sep 17 00:00:00 2001 From: Alex Wright Date: Mon, 6 Apr 2026 16:01:54 +0100 Subject: [PATCH] Change parser output Changed to parse main+plus+left of any types reusing the same parser functions for main and plus and returning an Enum of milk type. --- src/lib.rs | 41 ++++++---- src/model.rs | 52 ++++++++++-- src/parser.rs | 215 +++++++++++++++++++++++++++++++++++--------------- 3 files changed, 219 insertions(+), 89 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index d6f4638..171188e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,7 @@ use ext_php_rs::types::ZendObject; mod model; mod parser; +use model::Type; #[php_class] #[php(name = "SamiAndAlex\\TrackerApp\\Parser")] @@ -19,24 +20,32 @@ impl Parser { } pub fn parse_feed(&self, input: &str) -> PhpResult>> { - let (unparsed, fed) = crate::parser::parse_feed(input) + let (_unparsed, fed) = crate::parser::parse_feed(input) .map_err(|e| format!("nom parsing failed: {}", e))?; - if fed.formula.is_some() || fed.whole_milk.is_some() || fed.left.is_some() { - let mut result = ZendObject::new_stdclass(); - result.set_property("unparsed", unparsed)?; - if fed.formula.is_some() { - result.set_property("formula", fed.formula.unwrap().volume)?; - } - if fed.whole_milk.is_some() { - result.set_property("whole_milk", fed.whole_milk.unwrap().volume)?; - } - if fed.left.is_some() { - result.set_property("left", fed.left.unwrap().volume)?; - } - Ok(Some(result)) - } else { - Ok(None) + + let mut result = ZendObject::new_stdclass(); + if let Some((main_amount, main_type)) = fed.main { + let key = match main_type { + Type::Formula => "formula", + Type::PremixedFormula => "premixed_formula", + Type::BreastMilk => "breast_milk", + Type::WholeMilk => "whole_milk", + }; + result.set_property(key, main_amount.volume)?; } + if let Some((plus_amount, plus_type)) = fed.plus { + let key = match plus_type { + Type::Formula => "formula", + Type::PremixedFormula => "premixed_formula", + Type::BreastMilk => "breast_milk", + Type::WholeMilk => "whole_milk", + }; + result.set_property(key, plus_amount.volume)?; + } + if let Some(left_amount) = fed.left { + result.set_property("left", left_amount.volume)?; + } + Ok(Some(result)) } } diff --git a/src/model.rs b/src/model.rs index da74935..0e4d2fc 100644 --- a/src/model.rs +++ b/src/model.rs @@ -1,3 +1,4 @@ +use std::convert::TryFrom; use std::fmt::{ Debug, Display, @@ -38,21 +39,56 @@ impl Display for Amount { } } +#[derive(Debug)] +pub enum Type { + Formula, + PremixedFormula, + WholeMilk, + BreastMilk, +} + +impl Display for Type { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) + } +} + +impl TryFrom<&str> for Type { + type Error = (); + + fn try_from(value: &str) -> Result { + match value { + "formula" => Ok(Self::Formula), + "premixed formula" => Ok(Self::PremixedFormula), + "premixed" => Ok(Self::PremixedFormula), + "whole milk" => Ok(Self::WholeMilk), + "milk" => Ok(Self::WholeMilk), + "breast milk" => Ok(Self::BreastMilk), + _ => Err(()), + } + } +} + #[derive(Debug)] pub struct Fed { - pub formula: Option, - pub whole_milk: Option, + pub main: Option<(Amount, Type)>, + pub plus: Option<(Amount, Type)>, pub left: Option, } impl Display for Fed { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let amount_or_0 = |o: Option<&Amount>| o.map_or(0, |a| a.volume); - write!(f, "Fed {}ml formula and {}ml whole milk. Left {}ml.", - amount_or_0(self.formula.as_ref()), - amount_or_0(self.whole_milk.as_ref()), - amount_or_0(self.left.as_ref()), - ) + if let Some((main_amount, main_type)) = &self.main { + write!(f, "{}", format!("Fed {} of {}", &main_amount, &main_type))?; + } + if let Some((plus_amount, plus_type)) = &self.plus { + write!(f, " and {} of {}", &plus_amount, &plus_type)?; + } + if let Some(left_amount) = &self.left { + write!(f, ". Left {}", &left_amount)?; + } + write!(f, ".")?; + Ok(()) } } diff --git a/src/parser.rs b/src/parser.rs index 35a29b2..3df4cee 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -23,6 +23,7 @@ use crate::model::{ Amount, Duration, Time, + Type, }; fn parse_time(input: &str) -> IResult<&str, Time> { @@ -73,26 +74,37 @@ pub fn parse_amount(input: &str) -> IResult<&str, Amount> { 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"), - ))), +pub fn parse_type(input: &str) -> IResult<&str, Option> { + let (rest, feed_type_str) = opt( + preceded( + opt(tag_no_case(" of")), + preceded( + tag(" "), + alt(( + tag_no_case("premixed formula"), + tag_no_case("formula"), + tag_no_case("whole milk"), + tag_no_case("breast milk"), + tag_no_case("milk"), + )) + ) + ) )(input)?; - Ok((rest, amount)) + Ok(( + rest, + feed_type_str + .map(Type::try_from) + .transpose() + .expect("Failed to resolve Type from str"), + )) } -pub fn parse_second_amount(input: &str) -> IResult<&str, Amount> { - let (rest, (amount, _)) = pair( +pub fn parse_amount_and_type(input: &str) -> IResult<&str, (Amount, Option)> { + let (rest, (amount, feed_type)) = pair( parse_amount, - opt(alt(( - tag_no_case(" whole milk"), - tag_no_case(" milk"), - ))), + parse_type, )(input)?; - Ok((rest, amount)) + Ok((rest, (amount, feed_type))) } pub fn parse_left_amount(input: &str) -> IResult<&str, Amount> { @@ -117,22 +129,24 @@ pub fn parse_and_plus(input: &str) -> IResult<&str, &str> { } pub fn parse_feed(input: &str) -> IResult<&str, Fed> { - let (rest, (main, plus, _timestamp, left)) = tuple(( + let (rest, ((main_amount, main_type), plus, _timestamp, left)) = tuple(( preceded( tag("Fed "), - parse_main_amount, + parse_amount_and_type, ), opt(preceded( parse_and_plus, - parse_second_amount, + parse_amount_and_type, )), parse_timestamp, opt(parse_left_amount), ))(input)?; + let main = Some((main_amount, main_type.unwrap_or(Type::Formula))); + let plus = plus.map(|(a, t)| (a, t.unwrap_or(Type::WholeMilk))); let fed = Fed { - formula: Some(main), - whole_milk: plus, - left: left, + main, + plus, + left, }; Ok((rest, fed)) } @@ -207,33 +221,60 @@ mod tests { "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 }), + main: Some((Amount { volume: 150 }, Type::Formula)), + plus: Some((Amount { volume: 50 }, Type::WholeMilk)), + left: Some(Amount { volume: 40 }), + }, + ), + ( + "Fed 150ml formula and 50ml whole milk at 01:16-01:27 40ml left", + "", + Fed { + main: Some((Amount { volume: 150 }, Type::Formula)), + plus: Some((Amount { volume: 50 }, Type::WholeMilk)), + left: Some(Amount { volume: 40 }), + }, + ), + ( + "Fed 150ml formula and 50ml milk at 01:16-01:27 40ml left", + "", + Fed { + main: Some((Amount { volume: 150 }, Type::Formula)), + plus: Some((Amount { volume: 50 }, Type::WholeMilk)), left: Some(Amount { volume: 40 }), }, ), ]; - for (input, remaining, output) in test_cases { + for (input, remaining, expected) 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); + if let Some((expected_main_amount, _expected_main_type)) = expected.main { + assert!(actual.main.is_some(), "Expected to have a main"); + let main = actual.main.unwrap(); + assert_eq!(expected_main_amount.volume, main.0.volume); + assert!(matches!(main.1, Type::Formula)); } else { - assert!(actual.formula.is_none(), "Wasn't expecting formula"); + assert!(actual.main.is_none(), "Wasn't expecting main"); } - 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"); + macro_rules! assert_amount_and_type { + ($expected_amount_and_type:expr, $actual_amount_and_type:expr, $type_pattern:pat) => { + if let Some((expected_amount, _expected_type)) = $expected_amount_and_type { + assert!($actual_amount_and_type.is_some(), "Expected to be Some"); + let value = $actual_amount_and_type.unwrap(); + assert_eq!(expected_amount.volume, value.0.volume, "Expected amount to be {}", expected_amount.volume); + assert!(matches!(value.1, $type_pattern)); + } else { + assert!($actual_amount_and_type.is_none(), "Expected to be None"); + } + } } + assert_amount_and_type!(expected.plus, actual.plus, Type::WholeMilk); - if let Some(expected_left) = output.left { + if let Some(expected_left) = expected.left { assert!(actual.left.is_some(), "Expected to have left"); assert_eq!(expected_left.volume, actual.left.unwrap().volume); } else { @@ -246,80 +287,124 @@ mod tests { ( $name:ident, $input:expr, - $formula:expr, - $whole_milk:expr, - $left:expr $(,)? + $remained:expr, + $expected_main_amount:expr, + $expected_main_type:pat, + $expected_plus_amount:expr, + $expected_plus_type:pat, + $expected_left_amount:expr, ) => { #[test] fn $name() { let result = parse_feed($input); assert!(result.is_ok(), "Parsing failed"); - let (rest, actual) = result.unwrap(); - assert_eq!("", rest); + let (rest, result) = result.unwrap(); + assert_eq!($remained, rest, "Unexpected remaining text"); - // 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"), + if let Some(expected) = $expected_main_amount { + assert!(result.main.is_some(), "Expecting a main amount"); + let actual = result.main.unwrap(); + assert_eq!(expected.volume, actual.0.volume); + assert!(matches!(actual.1, $expected_main_type)); + } else { + assert!(result.main.is_none(), "Expecting a None main"); } - // 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"), + if let Some(expected) = $expected_plus_amount { + assert!(result.plus.is_some(), "Expecting a plus amount"); + let actual = result.plus.unwrap(); + assert_eq!(expected.volume, actual.0.volume); + assert!(matches!(actual.1, $expected_plus_type)); + } else { + assert!(result.plus.is_none(), "Expecting a None plus"); } - // 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"), + if let Some(expected) = $expected_left_amount { + assert!(result.left.is_some(), "Expecting a left amount"); + let actual = result.left.unwrap(); + assert_eq!(expected.volume, actual.volume); + } else { + assert!(result.left.is_none(), "Expecting a None left"); } } - }; + } } make_feed_test!( - test_parse_feed_basic, + basic_parsing_1, "Fed 150ml with 50ml whole milk at 01:16-01:27 40ml left", + "", Some(Amount { volume: 150 }), + Type::Formula, Some(Amount { volume: 50 }), + Type::WholeMilk, + Some(Amount { volume: 40 }), + ); + + make_feed_test!( + test_parse_feed_basic, + "Fed 150ml breast milk with 50ml whole milk at 01:16-01:27 40ml left", + "", + Some(Amount { volume: 150 }), + Type::BreastMilk, + Some(Amount { volume: 50 }), + Type::WholeMilk, 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 }), + Type::Formula, Some(Amount { volume: 50 }), + Type::WholeMilk, Some(Amount { volume: 40 }), ); make_feed_test!( test_parse_feed_3, "Fed 150ml + 50ml milk at 01:16-01:27", + "", Some(Amount { volume: 150 }), + Type::Formula, Some(Amount { volume: 50 }), + Type::WholeMilk, + None::, + ); + + make_feed_test!( + test_parse_feed_4, + "Fed 150ml and 25ml whole milk at 01:16-01:27", + "", + Some(Amount { volume: 150 }), + Type::Formula, + Some(Amount { volume: 25 }), + Type::WholeMilk, None::, ); make_feed_test!( test_parse_feed_5, "Fed 150ml and 25ml at 01:16-01:27", + "", Some(Amount { volume: 150 }), + Type::Formula, Some(Amount { volume: 25 }), + Type::WholeMilk, + None::, + ); + + make_feed_test!( + test_parse_feed_6, + "Fed 50ml of breast milk @ 15:00-15:15", + "", + Some(Amount { volume: 50 }), + Type::BreastMilk, + None::, + Type::WholeMilk, None::, ); }