diff --git a/examples/meatloaf.txt b/examples/meatloaf.txt new file mode 100644 index 0000000..d5e60fe --- /dev/null +++ b/examples/meatloaf.txt @@ -0,0 +1,16 @@ +title: Meatloaf + +Good old fashioned meatloaf. + +step: + +1 lb ground beef +1 onion (chopped) +1 cup oatmeal +2 tbsp garlic powder +1 egg +2 tbsp salt +1/2 cup ketchup + +Mix ingredients excluding the ketchup together thoroughly. Bake in oven for 35 minutes at 350. +Cover with ketchup and cook for an additional 10 minutes. Cut into slices and serve. \ No newline at end of file diff --git a/recipes/src/lib.rs b/recipes/src/lib.rs index 91a9214..834dc3a 100644 --- a/recipes/src/lib.rs +++ b/recipes/src/lib.rs @@ -108,7 +108,7 @@ impl Recipe { /// Get entire ingredients list for each step of the recipe. With duplicate /// ingredients added together. pub fn get_ingredients(&self) -> BTreeMap { - use Measure::{Count, Gram, Volume}; + use Measure::{Count, Volume, Weight}; self.steps .iter() .map(|s| s.ingredients.iter()) @@ -121,7 +121,7 @@ impl Recipe { let amt = match (acc[&key].amt, i.amt) { (Volume(rvm), Volume(lvm)) => Volume(lvm + rvm), (Count(lqty), Count(rqty)) => Count(lqty + rqty), - (Gram(lqty), Gram(rqty)) => Gram(lqty + rqty), + (Weight(lqty), Weight(rqty)) => Weight(lqty + rqty), _ => unreachable!(), }; acc.get_mut(&key).map(|i| i.amt = amt); diff --git a/recipes/src/parse.rs b/recipes/src/parse.rs index d17ffd3..0dae279 100644 --- a/recipes/src/parse.rs +++ b/recipes/src/parse.rs @@ -14,13 +14,13 @@ use std::str::FromStr; use abortable_parser::{ - ascii_digit, ascii_ws, consume_all, discard, do_each, either, eoi, make_fn, must, not, - optional, peek, repeat, separated, text_token, trap, until, Result, StrIter, + ascii_digit, consume_all, discard, do_each, either, eoi, make_fn, must, not, optional, peek, + repeat, separated, text_token, trap, until, Result, StrIter, }; use num_rational::Ratio; use crate::{ - unit::{Measure, Measure::*, Quantity, VolumeMeasure::*}, + unit::{Measure, Measure::*, Quantity, VolumeMeasure::*, WeightMeasure::*}, Ingredient, Recipe, Step, }; @@ -222,11 +222,10 @@ pub fn measure(i: StrIter) -> abortable_parser::Result { Some("qrt") | Some("quart") => Volume(Qrt(qty)), Some("pint") | Some("pnt") => Volume(Pint(qty)), Some("cnt") | Some("count") => Count(qty), - Some("lb") => Measure::lb(qty), - Some("oz") => Measure::oz(qty), - Some("kg") => Measure::kilogram(qty), - Some("g") => Gram(qty), - Some("gram") => Gram(qty), + Some("lb") => Weight(Pound(qty)), + Some("oz") => Weight(Oz(qty)), + Some("kg") => Weight(Kilogram(qty)), + Some("g") | Some("gram") => Weight(Gram(qty)), Some(u) => { return Result::Abort(abortable_parser::Error::new( format!("Invalid Unit {}", u), @@ -254,7 +253,7 @@ make_fn!( discard!(text_token!("\n")), eoi, discard!(text_token!("(")))), - (name) + (name.trim()) ) ); diff --git a/recipes/src/unit.rs b/recipes/src/unit.rs index fd265ed..13c80b9 100644 --- a/recipes/src/unit.rs +++ b/recipes/src/unit.rs @@ -65,6 +65,11 @@ const QRT: Quantity = Quantity::Whole(960); const LTR: Quantity = Quantity::Whole(1000); const GAL: Quantity = Quantity::Whole(3840); +// multiplier constants for various units into grams +const LB: Quantity = Quantity::Frac(Ratio::new_raw(4535924, 10000)); +const OZ: Quantity = Quantity::Frac(Ratio::new_raw(2834952, 100000)); +const KG: Quantity = Quantity::Whole(1000); + const ONE: Quantity = Quantity::Whole(1); impl VolumeMeasure { @@ -205,6 +210,102 @@ impl Display for VolumeMeasure { } } +#[derive(Copy, Clone, Debug)] +pub enum WeightMeasure { + Gram(Quantity), + Kilogram(Quantity), + Pound(Quantity), + Oz(Quantity), +} + +impl WeightMeasure { + pub fn get_grams(&self) -> Quantity { + match self { + &Self::Gram(ref qty) => *qty, + &Self::Kilogram(ref qty) => *qty * KG, + &Self::Pound(ref qty) => *qty * LB, + &Self::Oz(ref qty) => *qty * OZ, + } + } + + pub fn plural(&self) -> bool { + match self { + &Self::Gram(qty) | &Self::Kilogram(qty) | &Self::Pound(qty) | &Self::Oz(qty) => { + qty.plural() + } + } + } + + pub fn into_gram(self) -> Self { + Self::Gram(self.get_grams()) + } + + pub fn into_kilo(self) -> Self { + Self::Kilogram(self.get_grams() / KG) + } + + pub fn into_pound(self) -> Self { + Self::Pound(self.get_grams() / LB) + } + + pub fn into_oz(self) -> Self { + Self::Oz(self.get_grams() / OZ) + } + + pub fn normalize(self) -> Self { + let grams = self.get_grams(); + if (grams / KG) >= ONE { + return self.into_kilo(); + } + if (grams / LB) >= ONE { + return self.into_pound(); + } + if (grams / OZ) >= ONE { + return self.into_oz(); + } + return self.into_gram(); + } +} + +macro_rules! weight_op { + ($trait:ident, $method:ident) => { + impl $trait for WeightMeasure { + type Output = Self; + + fn $method(self, lhs: Self) -> Self::Output { + let (l, r) = (self.get_grams(), lhs.get_grams()); + WeightMeasure::Gram($trait::$method(l, r)) + } + } + }; +} + +weight_op!(Add, add); +weight_op!(Sub, sub); + +impl PartialEq for WeightMeasure { + fn eq(&self, lhs: &Self) -> bool { + let rhs = self.get_grams(); + let lhs = lhs.get_grams(); + PartialEq::eq(&rhs, &lhs) + } +} + +impl Display for WeightMeasure { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + &Self::Gram(qty) => write!(f, "{} gram{}", qty, if qty.plural() { "s" } else { "" }), + &Self::Kilogram(qty) => { + write!(f, "{} kilogram{}", qty, if qty.plural() { "s" } else { "" }) + } + &Self::Pound(qty) => write!(f, "{} lb{}", qty, if qty.plural() { "s" } else { "" }), + &Self::Oz(qty) => write!(f, "{} oz", qty), + } + } +} + +use WeightMeasure::{Gram, Kilogram, Oz, Pound}; + #[derive(Copy, Clone, Debug, PartialEq)] /// Measurements in a Recipe with associated units for them. pub enum Measure { @@ -213,10 +314,10 @@ pub enum Measure { /// Simple count of items Count(Quantity), /// Weight measure as Grams base unit - Gram(Quantity), + Weight(WeightMeasure), } -use Measure::{Count, Gram, Volume}; +use Measure::{Count, Volume, Weight}; impl Measure { pub fn tsp(qty: Quantity) -> Self { @@ -260,29 +361,28 @@ impl Measure { } pub fn gram(qty: Quantity) -> Self { - Gram(qty) + Weight(Gram(qty)) } pub fn kilogram(qty: Quantity) -> Self { - Gram(qty * Quantity::Whole(1000)) + Weight(Kilogram(qty)) } - // TODO(jwall): Should these be separate units with conversions? pub fn lb(qty: Quantity) -> Self { - // This is an approximation obviously - Gram(qty * (Quantity::Whole(453) + Quantity::Frac(Ratio::new(6, 10)))) + // This is an approximation + Weight(Pound(qty)) } pub fn oz(qty: Quantity) -> Self { - // This is an approximation obviously - Gram(qty * (Quantity::Whole(28) + Quantity::Frac(Ratio::new(4, 10)))) + // This is an approximation + Weight(Oz(qty)) } pub fn measure_type(&self) -> String { match self { Volume(_) => "Volume", Count(_) => "Count", - Gram(_) => "Weight", + Weight(_) => "Weight", } .to_owned() } @@ -290,12 +390,12 @@ impl Measure { pub fn plural(&self) -> bool { match self { Volume(vm) => vm.plural(), - Count(qty) | Gram(qty) => qty.plural(), + Count(qty) => qty.plural(), + Weight(wm) => wm.plural(), } } - // TODO(jwall): parse from string. - + // TODO(jwall): Remove this it's unnecessary pub fn parse(input: &str) -> std::result::Result { Ok(match measure(StrIter::new(input)) { Result::Complete(_, measure) => measure, @@ -312,7 +412,8 @@ impl Display for Measure { match self { Volume(vm) => write!(w, "{}", vm), Count(qty) => write!(w, "{}", qty), - Gram(qty) => write!(w, "{} gram{}", qty, if qty.plural() { "s" } else { "" }), + // TODO(jwall): Should I allow auto convert upwards for the grams to kgs? + Weight(wm) => write!(w, "{}", wm), } } }