diff --git a/recipes/src/lib.rs b/recipes/src/lib.rs index 3b35fd1..0d29aee 100644 --- a/recipes/src/lib.rs +++ b/recipes/src/lib.rs @@ -13,35 +13,135 @@ // limitations under the License. pub mod unit; +use std::collections::BTreeMap; + use unit::*; +/// A Recipe with a title, description, and a series of steps. pub struct Recipe { pub title: String, pub desc: String, pub steps: Vec, } +impl Recipe { + pub fn new(title: String, desc: String) -> Self { + Self { + title, + desc, + steps: Vec::new(), + } + } + + /// Add steps to the end of the recipe. + pub fn add_steps(&mut self, steps: Vec) { + self.steps.extend(steps.into_iter()); + } + + /// Add a single step to the end of the recipe. + pub fn add_step(&mut self, step: Step) { + self.steps.push(step); + } + + /// 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}; + self.steps + .iter() + .map(|s| s.ingredients.iter()) + .flatten() + .fold(BTreeMap::new(), |mut acc, i| { + let key = i.key(); + if !acc.contains_key(&key) { + acc.insert(key, i.clone()); + } else { + 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), + _ => unreachable!(), + }; + acc.get_mut(&key).map(|i| i.amt = amt); + } + return acc; + }) + } +} + +/// A Recipe step. It has the time for the step if there is one, instructions, and an ingredients +/// list. pub struct Step { - pub prep_time: std::time::Duration, + pub prep_time: Option, pub instructions: String, pub ingredients: Vec, } +/// Form of the ingredient. +#[derive(PartialEq, Eq, Debug, PartialOrd, Ord, Clone)] pub enum Form { Whole, // default Chopped, Minced, Sliced, Ground, + Mashed, Custom(String), } +/// Unique identifier for an Ingredient. Ingredients are identified by name, form, +/// and measurement type. (Volume, Count, Weight) +#[derive(PartialEq, PartialOrd, Eq, Ord)] +pub struct IngredientKey(String, Form, String); + +/// Ingredient in a recipe. The `name` and `form` fields with the measurement type +/// uniquely identify an ingredient. +#[derive(Clone)] pub struct Ingredient { pub name: String, - pub amt: Measure, pub form: Form, + pub amt: Measure, pub category: String, } +impl Ingredient { + pub fn new>(name: S, form: Form, amt: Measure, category: S) -> Self { + Self { + name: name.into(), + form, + amt, + category: category.into(), + } + } + + /// Unique identifier for this Ingredient. + pub fn key(&self) -> IngredientKey { + return IngredientKey( + self.name.clone(), + self.form.clone(), + self.amt.measure_type(), + ); + } +} + +impl std::fmt::Display for Ingredient { + fn fmt(&self, w: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(w, "{} {}", self.amt, self.name)?; + write!( + w, + " ({})", + match self.form { + Form::Whole => return Ok(()), + Form::Chopped => "chopped", + Form::Minced => "minced", + Form::Sliced => "sliced", + Form::Ground => "ground", + Form::Mashed => "mashed", + Form::Custom(ref s) => return write!(w, " ({})", s), + } + ) + } +} + #[cfg(test)] mod test; diff --git a/recipes/src/test.rs b/recipes/src/test.rs index 3fda70b..f287a51 100644 --- a/recipes/src/test.rs +++ b/recipes/src/test.rs @@ -11,7 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -use crate::unit::*; +use crate::*; use VolumeMeasure::*; use std::convert::Into; @@ -74,3 +74,132 @@ fn test_volume_normalize() { assert_normalize!(Gal, into_ltr, "not a gal after normalize call"); assert_normalize!(Gal, into_tsp, "not a gal after normalize call"); } + +#[test] +fn test_ingredient_display() { + let cases = vec![ + ( + Ingredient::new("onion", Form::Chopped, Measure::cup(1.into()), "Produce"), + "1 cup onion (chopped)", + ), + ( + Ingredient::new("onion", Form::Chopped, Measure::cup(2.into()), "Produce"), + "2 cups onion (chopped)", + ), + ( + Ingredient::new("onion", Form::Chopped, Measure::tbsp(1.into()), "Produce"), + "1 tbsp onion (chopped)", + ), + ( + Ingredient::new("onion", Form::Chopped, Measure::tbsp(2.into()), "Produce"), + "2 tbsps onion (chopped)", + ), + ( + Ingredient::new("soy sauce", Form::Whole, Measure::floz(1.into()), "Produce"), + "1 floz soy sauce", + ), + ( + Ingredient::new("soy sauce", Form::Whole, Measure::floz(2.into()), "Produce"), + "2 floz soy sauce", + ), + ( + Ingredient::new("soy sauce", Form::Whole, Measure::qrt(1.into()), "Produce"), + "1 qrt soy sauce", + ), + ( + Ingredient::new("soy sauce", Form::Whole, Measure::qrt(2.into()), "Produce"), + "2 qrts soy sauce", + ), + ( + Ingredient::new("soy sauce", Form::Whole, Measure::pint(1.into()), "Produce"), + "1 pint soy sauce", + ), + ( + Ingredient::new("soy sauce", Form::Whole, Measure::pint(2.into()), "Produce"), + "2 pints soy sauce", + ), + ( + Ingredient::new("soy sauce", Form::Whole, Measure::gal(1.into()), "Produce"), + "1 gal soy sauce", + ), + ( + Ingredient::new("soy sauce", Form::Whole, Measure::gal(2.into()), "Produce"), + "2 gals soy sauce", + ), + ( + Ingredient::new("soy sauce", Form::Whole, Measure::ml(1.into()), "Produce"), + "1 ml soy sauce", + ), + ( + Ingredient::new("soy sauce", Form::Whole, Measure::ml(2.into()), "Produce"), + "2 ml soy sauce", + ), + ( + Ingredient::new("soy sauce", Form::Whole, Measure::ltr(1.into()), "Produce"), + "1 ltr soy sauce", + ), + ( + Ingredient::new("soy sauce", Form::Whole, Measure::ltr(2.into()), "Produce"), + "2 ltr soy sauce", + ), + ( + Ingredient::new("apple", Form::Whole, Measure::count(1), "Produce"), + "1 apple", + ), + ( + Ingredient::new("salt", Form::Whole, Measure::gram(1.into()), "Produce"), + "1 gram salt", + ), + ( + Ingredient::new("salt", Form::Whole, Measure::gram(2.into()), "Produce"), + "2 grams salt", + ), + ( + Ingredient::new("onion", Form::Minced, Measure::cup(1.into()), "Produce"), + "1 cup onion (minced)", + ), + ( + Ingredient::new( + "pepper", + Form::Ground, + Measure::tsp(Ratio::new(1, 2).into()), + "Produce", + ), + "1/2 tsp pepper (ground)", + ), + ( + Ingredient::new( + "pepper", + Form::Ground, + Measure::tsp(Ratio::new(3, 2).into()), + "Produce", + ), + "1 1/2 tsps pepper (ground)", + ), + ( + Ingredient::new("apple", Form::Sliced, Measure::count(1), "Produce"), + "1 apple (sliced)", + ), + ( + Ingredient::new("potato", Form::Mashed, Measure::count(1), "Produce"), + "1 potato (mashed)", + ), + ( + Ingredient::new( + "potato", + Form::Custom("blanched".to_owned()), + Measure::count(1), + "Produce", + ), + "1 potato (blanched)", + ), + ]; + for (i, expected) in cases { + assert_eq!(format!("{}", i), expected); + } +} + +#[test] +fn test_recipe_display() { + todo!() +} diff --git a/recipes/src/unit.rs b/recipes/src/unit.rs index a7dc4e6..eb5fd35 100644 --- a/recipes/src/unit.rs +++ b/recipes/src/unit.rs @@ -44,9 +44,10 @@ pub enum VolumeMeasure { Gal(Quantity), // 3800 ml /// Fluid Ounces Floz(Quantity), // 30 ml - /// Milliliter Measurements. // Metric volume measurements. + /// Milliliter Measurements. ML(Quantity), // Base unit + // Liter Measurements. Ltr(Quantity), // 1000 ml } use VolumeMeasure::{Cup, Floz, Gal, Ltr, Pint, Qrt, Tbsp, Tsp, ML}; @@ -79,6 +80,13 @@ impl VolumeMeasure { } } + pub fn plural(&self) -> bool { + match self { + Tsp(qty) | Tbsp(qty) | Cup(qty) | Pint(qty) | Qrt(qty) | Gal(qty) | Floz(qty) + | ML(qty) | Ltr(qty) => qty.plural(), + } + } + /// Convert into milliliters. pub fn into_ml(self) -> Self { ML(self.get_ml()) @@ -181,12 +189,12 @@ impl PartialEq for VolumeMeasure { impl Display for VolumeMeasure { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Tsp(qty) => write!(f, "{} tsp", qty), - Tbsp(qty) => write!(f, "{} tbsp", qty), - Cup(qty) => write!(f, "{} cup", qty), - Pint(qty) => write!(f, "{} pint", qty), - Qrt(qty) => write!(f, "{} qrt", qty), - Gal(qty) => write!(f, "{} gal", qty), + Tsp(qty) => write!(f, "{} tsp{}", qty, if qty.plural() { "s" } else { "" }), + Tbsp(qty) => write!(f, "{} tbsp{}", qty, if qty.plural() { "s" } else { "" }), + Cup(qty) => write!(f, "{} cup{}", qty, if qty.plural() { "s" } else { "" }), + Pint(qty) => write!(f, "{} pint{}", qty, if qty.plural() { "s" } else { "" }), + Qrt(qty) => write!(f, "{} qrt{}", qty, if qty.plural() { "s" } else { "" }), + Gal(qty) => write!(f, "{} gal{}", qty, if qty.plural() { "s" } else { "" }), Floz(qty) => write!(f, "{} floz", qty), ML(qty) => write!(f, "{} ml", qty), Ltr(qty) => write!(f, "{} ltr", qty), @@ -216,6 +224,18 @@ impl Measure { Volume(Tbsp(qty)) } + pub fn floz(qty: Quantity) -> Self { + Volume(Floz(qty)) + } + + pub fn ml(qty: Quantity) -> Self { + Volume(ML(qty)) + } + + pub fn ltr(qty: Quantity) -> Self { + Volume(Ltr(qty)) + } + pub fn cup(qty: Quantity) -> Self { Volume(Cup(qty)) } @@ -239,6 +259,22 @@ impl Measure { pub fn gram(qty: Quantity) -> Self { Gram(qty) } + + pub fn measure_type(&self) -> String { + match self { + Volume(_) => "Volume", + Count(_) => "Count", + Gram(_) => "Weight", + } + .to_owned() + } + + pub fn plural(&self) -> bool { + match self { + Volume(vm) => vm.plural(), + Count(qty) | Gram(qty) => qty.plural(), + } + } } impl Display for Measure { @@ -246,7 +282,7 @@ impl Display for Measure { match self { Volume(vm) => write!(w, "{}", vm), Count(qty) => write!(w, "{}", qty), - Gram(qty) => write!(w, "{} grams", qty), + Gram(qty) => write!(w, "{} gram{}", qty, if qty.plural() { "s" } else { "" }), } } } @@ -301,6 +337,13 @@ impl Quantity { Frac(v) => (*v.numer() / *v.denom()) as f32, } } + + pub fn plural(&self) -> bool { + match self { + Whole(v) => *v > 1, + Frac(r) => *r > Ratio::new(1, 1), + } + } } use Quantity::{Frac, Whole};