diff --git a/src/main.rs b/src/main.rs index 708322f..8ae61b9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,191 +1,7 @@ -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, -}; +mod model; +mod parser; -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, - pub whole_milk: Option, - pub left: Option, -} - -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)) -} +use parser::parse_feed; fn main() -> Result<(), Box> { let tests = vec![ @@ -205,194 +21,9 @@ fn main() -> Result<(), Box> { println!("Testing: {}", &test); let result = parse_feed(test)?; println!("Result: {:?}", &result); + let (_unparsed, fed) = result; + println!(" as string: {}", fed); } 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::, - ); - - 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::, - ); -} - diff --git a/src/model.rs b/src/model.rs new file mode 100644 index 0000000..da74935 --- /dev/null +++ b/src/model.rs @@ -0,0 +1,58 @@ +use std::fmt::{ + Debug, + Display, +}; + +#[derive(Debug)] +pub 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)] +pub 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, + pub whole_milk: Option, + 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()), + ) + } +} + diff --git a/src/parser.rs b/src/parser.rs new file mode 100644 index 0000000..35a29b2 --- /dev/null +++ b/src/parser.rs @@ -0,0 +1,326 @@ +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 crate::model::{ + Fed, + Amount, + Duration, + Time, +}; + +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)?; + 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)) +} + +#[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::, + ); + + 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::, + ); +} +