diff --git a/recipes/src/lib.rs b/recipes/src/lib.rs index 416b5dd..53d40a7 100644 --- a/recipes/src/lib.rs +++ b/recipes/src/lib.rs @@ -156,16 +156,25 @@ impl IngredientAccumulator { set.insert(recipe_title.clone()); self.inner.insert(key, (i.clone(), set)); } else { - let amt = match (self.inner[&key].0.amt, i.amt) { - (Volume(rvm), Volume(lvm)) => Volume(lvm + rvm), - (Count(lqty), Count(rqty)) => Count(lqty + rqty), - (Weight(lqty), Weight(rqty)) => Weight(lqty + rqty), + let amts = match (&self.inner[&key].0.amt, &i.amt) { + (Volume(rvm), Volume(lvm)) => vec![Volume(lvm + rvm)], + (Count(lqty), Count(rqty)) => vec![Count(lqty + rqty)], + (Weight(lqty), Weight(rqty)) => vec![Weight(lqty + rqty)], + (Package(lnm, lqty), Package(rnm, rqty)) => { + if lnm == rnm { + vec![Package(lnm.clone(), lqty + rqty)] + } else { + vec![Package(lnm.clone(), lqty.clone()), Package(rnm.clone(), rqty.clone())] + } + } _ => unreachable!(), }; - self.inner.get_mut(&key).map(|(i, set)| { - i.amt = amt; - set.insert(recipe_title.clone()); - }); + for amt in amts { + self.inner.get_mut(&key).map(|(i, set)| { + i.amt = amt; + set.insert(recipe_title.clone()); + }); + } } } } diff --git a/recipes/src/parse.rs b/recipes/src/parse.rs index bf2e30e..38fac0f 100644 --- a/recipes/src/parse.rs +++ b/recipes/src/parse.rs @@ -334,7 +334,14 @@ make_fn!(unit, text_token!("kg"), text_token!("grams"), text_token!("gram"), - text_token!("g")), + text_token!("g"), + text_token!("pkg"), + text_token!("package"), + text_token!("bottle"), + text_token!("bot"), + text_token!("bag"), + text_token!("can") + ), _ => ws, (u.to_lowercase().to_singular()) ) @@ -393,6 +400,7 @@ pub fn measure(i: StrIter) -> abortable_parser::Result { "oz" => Weight(Oz(qty)), "kg" | "kilogram" => Weight(Kilogram(qty)), "g" | "gram" => Weight(Gram(qty)), + "pkg" | "package" | "can" | "bag" | "bottle" | "bot" => Measure::pkg(s, qty), _u => { eprintln!("Invalid unit: {}", _u); unreachable!() @@ -411,6 +419,8 @@ pub fn measure(i: StrIter) -> abortable_parser::Result { } } +// TODO(jwall): I think this is a mistake. We should rethink what noralizing means or we should +// remove it. pub fn normalize_name(name: &str) -> String { let parts: Vec<&str> = name.split_whitespace().collect(); if parts.len() >= 2 { diff --git a/recipes/src/test.rs b/recipes/src/test.rs index ff5ebd6..62d0633 100644 --- a/recipes/src/test.rs +++ b/recipes/src/test.rs @@ -235,32 +235,30 @@ fn test_ingredient_name_parse() { #[test] fn test_ingredient_parse() { for (i, expected) in vec![ - //( - // "1 cup flour ", - // Ingredient::new("flour", None, Volume(Cup(Quantity::Whole(1))), ""), - //), - //( - // "\t1 cup flour ", - // Ingredient::new("flour", None, Volume(Cup(Quantity::Whole(1))), ""), - //), - //( - // "1 cup apple (chopped)", - // Ingredient::new( - // "apple", - // Some("chopped".to_owned()), - // Volume(Cup(Quantity::Whole(1))), - // "", - // ), - //), - //( - // "1 cup apple (chopped) ", - // Ingredient::new( - // "apple", - // Some("chopped".to_owned()), - // Volume(Cup(Quantity::Whole(1))), - // "", - // ), - //), + ( + "1 cup flour ", + Ingredient::new("flour", None, Volume(Cup(Quantity::Whole(1)))), + ), + ( + "\t1 cup flour ", + Ingredient::new("flour", None, Volume(Cup(Quantity::Whole(1)))), + ), + ( + "1 cup apple (chopped)", + Ingredient::new( + "apple", + Some("chopped".to_owned()), + Volume(Cup(Quantity::Whole(1))), + ), + ), + ( + "1 cup apple (chopped) ", + Ingredient::new( + "apple", + Some("chopped".to_owned()), + Volume(Cup(Quantity::Whole(1))), + ), + ), ( "1 green bell pepper (chopped) ", Ingredient::new( @@ -269,6 +267,46 @@ fn test_ingredient_parse() { Count(Quantity::Whole(1)), ), ), + ( + "1 pkg green onion", + Ingredient::new( + "green onion", + None, + Package("pkg".into(), Quantity::Whole(1)), + ), + ), + ( + "1 bottle green onion", + Ingredient::new( + "green onion", + None, + Package("bottle".into(), Quantity::Whole(1)), + ), + ), + ( + "1 bot green onion", + Ingredient::new( + "green onion", + None, + Package("bot".into(), Quantity::Whole(1)), + ), + ), + ( + "1 bag green onion", + Ingredient::new( + "green onion", + None, + Package("bag".into(), Quantity::Whole(1)), + ), + ), + ( + "1 can baked beans", + Ingredient::new( + "baked bean", + None, + Package("can".into(), Quantity::Whole(1)), + ), + ), ] { match parse::ingredient(StrIter::new(i)) { ParseResult::Complete(_, ing) => assert_eq!(ing, expected), diff --git a/recipes/src/unit.rs b/recipes/src/unit.rs index f31333a..2ba223d 100644 --- a/recipes/src/unit.rs +++ b/recipes/src/unit.rs @@ -21,7 +21,7 @@ use std::{ cmp::{Ordering, PartialEq, PartialOrd}, convert::TryFrom, fmt::Display, - ops::{Add, Div, Mul, Sub}, + ops::{Add, Div, Mul, Sub}, rc::Rc, }; use num_rational::Ratio; @@ -179,6 +179,20 @@ impl VolumeMeasure { macro_rules! volume_op { ($trait:ident, $method:ident) => { + impl $trait for &VolumeMeasure { + type Output = VolumeMeasure; + + fn $method(self, lhs: Self) -> Self::Output { + let (l, r) = (self.get_ml(), lhs.get_ml()); + let result = ML($trait::$method(l, r)); + if self.metric() { + result.normalize() + } else { + result.into_tsp().normalize() + } + } + } + impl $trait for VolumeMeasure { type Output = Self; @@ -293,6 +307,20 @@ impl WeightMeasure { macro_rules! weight_op { ($trait:ident, $method:ident) => { + impl $trait for &WeightMeasure { + type Output = WeightMeasure; + + fn $method(self, lhs: Self) -> Self::Output { + let (l, r) = (self.get_grams(), lhs.get_grams()); + let result = WeightMeasure::Gram($trait::$method(l, r)); + if self.metric() { + result.normalize() + } else { + result.into_oz().normalize() + } + } + } + impl $trait for WeightMeasure { type Output = Self; @@ -335,18 +363,19 @@ impl Display for WeightMeasure { use WeightMeasure::{Gram, Kilogram, Oz, Pound}; -#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord)] +#[derive(Clone, Debug, PartialEq, PartialOrd, Eq, Ord)] /// Measurements in a Recipe with associated units for them. pub enum Measure { /// Volume measurements as meter cubed base unit Volume(VolumeMeasure), /// Simple count of items Count(Quantity), + Package(Rc, Quantity), /// Weight measure as Grams base unit Weight(WeightMeasure), } -use Measure::{Count, Volume, Weight}; +use Measure::{Count, Volume, Weight, Package}; impl Measure { pub fn tsp(qty: Quantity) -> Self { @@ -407,11 +436,16 @@ impl Measure { Weight(Oz(qty)) } + pub fn pkg>>(name: S, qty: Quantity) -> Self { + Package(name.into(), qty) + } + pub fn measure_type(&self) -> String { match self { Volume(_) => "Volume", Count(_) => "Count", Weight(_) => "Weight", + Package(_, _) => "Package", } .to_owned() } @@ -421,6 +455,7 @@ impl Measure { Volume(vm) => vm.plural(), Count(qty) => qty.plural(), Weight(wm) => wm.plural(), + Package(_, qty) => qty.plural(), } } @@ -429,6 +464,7 @@ impl Measure { Volume(vm) => Volume(vm.normalize()), Count(qty) => Count(qty.clone()), Weight(wm) => Weight(wm.normalize()), + Package(nm, qty) => Package(nm.clone(), qty.clone()), } } } @@ -439,6 +475,7 @@ impl Display for Measure { Volume(vm) => write!(w, "{}", vm), Count(qty) => write!(w, "{}", qty), Weight(wm) => write!(w, "{}", wm), + Package(nm, qty) => write!(w, "{} {}", qty, nm), } } } @@ -533,6 +570,22 @@ impl TryFrom for Quantity { macro_rules! quantity_op { ($trait:ident, $method:ident) => { + impl $trait for &Quantity { + type Output = Quantity; + + fn $method(self, lhs: Self) -> Self::Output { + match (self, lhs) { + (Whole(rhs), Whole(lhs)) => Frac($trait::$method( + Ratio::from_integer(*rhs), + Ratio::from_integer(*lhs), + )), + (Frac(rhs), Frac(lhs)) => Frac($trait::$method(rhs, lhs)), + (Whole(rhs), Frac(lhs)) => Frac($trait::$method(Ratio::from_integer(*rhs), lhs)), + (Frac(rhs), Whole(lhs)) => Frac($trait::$method(rhs, Ratio::from_integer(*lhs))), + } + } + } + impl $trait for Quantity { type Output = Self;