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.
This commit is contained in:
parent
8d3190c343
commit
67f77ce160
33
src/lib.rs
33
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<Option<ZBox<ZendObject>>> {
|
||||
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 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 fed.whole_milk.is_some() {
|
||||
result.set_property("whole_milk", fed.whole_milk.unwrap().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 fed.left.is_some() {
|
||||
result.set_property("left", fed.left.unwrap().volume)?;
|
||||
if let Some(left_amount) = fed.left {
|
||||
result.set_property("left", left_amount.volume)?;
|
||||
}
|
||||
Ok(Some(result))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
52
src/model.rs
52
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<Self, Self::Error> {
|
||||
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<Amount>,
|
||||
pub whole_milk: Option<Amount>,
|
||||
pub main: Option<(Amount, Type)>,
|
||||
pub plus: Option<(Amount, Type)>,
|
||||
pub left: Option<Amount>,
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
213
src/parser.rs
213
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<Type>> {
|
||||
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<Type>)> {
|
||||
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);
|
||||
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.whole_milk.is_none(), "Wasn't expecting whole_milk");
|
||||
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::<Amount>,
|
||||
);
|
||||
|
||||
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::<Amount>,
|
||||
);
|
||||
|
||||
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::<Amount>,
|
||||
);
|
||||
|
||||
make_feed_test!(
|
||||
test_parse_feed_6,
|
||||
"Fed 50ml of breast milk @ 15:00-15:15",
|
||||
"",
|
||||
Some(Amount { volume: 50 }),
|
||||
Type::BreastMilk,
|
||||
None::<Amount>,
|
||||
Type::WholeMilk,
|
||||
None::<Amount>,
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user