Move parsers and models to their own files
This commit is contained in:
parent
b3d810484e
commit
e579bc5a66
379
src/main.rs
379
src/main.rs
@ -1,191 +1,7 @@
|
|||||||
use nom::IResult;
|
mod model;
|
||||||
use nom::branch::{
|
mod parser;
|
||||||
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> {
|
use parser::parse_feed;
|
||||||
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>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let tests = vec![
|
let tests = vec![
|
||||||
@ -205,194 +21,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
println!("Testing: {}", &test);
|
println!("Testing: {}", &test);
|
||||||
let result = parse_feed(test)?;
|
let result = parse_feed(test)?;
|
||||||
println!("Result: {:?}", &result);
|
println!("Result: {:?}", &result);
|
||||||
|
let (_unparsed, fed) = result;
|
||||||
|
println!(" as string: {}", fed);
|
||||||
}
|
}
|
||||||
Ok(())
|
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>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
58
src/model.rs
Normal file
58
src/model.rs
Normal file
@ -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<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 {
|
||||||
|
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()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
326
src/parser.rs
Normal file
326
src/parser.rs
Normal file
@ -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::<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