Recipe Datamodel: Recipe

This commit is contained in:
Jeremy Wall 2021-04-13 20:14:25 -04:00
parent 1579d16943
commit b96ee641af
3 changed files with 283 additions and 11 deletions

View File

@ -13,35 +13,135 @@
// limitations under the License. // limitations under the License.
pub mod unit; pub mod unit;
use std::collections::BTreeMap;
use unit::*; use unit::*;
/// A Recipe with a title, description, and a series of steps.
pub struct Recipe { pub struct Recipe {
pub title: String, pub title: String,
pub desc: String, pub desc: String,
pub steps: Vec<Step>, pub steps: Vec<Step>,
} }
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<Step>) {
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<IngredientKey, Ingredient> {
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 struct Step {
pub prep_time: std::time::Duration, pub prep_time: Option<std::time::Duration>,
pub instructions: String, pub instructions: String,
pub ingredients: Vec<Ingredient>, pub ingredients: Vec<Ingredient>,
} }
/// Form of the ingredient.
#[derive(PartialEq, Eq, Debug, PartialOrd, Ord, Clone)]
pub enum Form { pub enum Form {
Whole, // default Whole, // default
Chopped, Chopped,
Minced, Minced,
Sliced, Sliced,
Ground, Ground,
Mashed,
Custom(String), 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 struct Ingredient {
pub name: String, pub name: String,
pub amt: Measure,
pub form: Form, pub form: Form,
pub amt: Measure,
pub category: String, pub category: String,
} }
impl Ingredient {
pub fn new<S: Into<String>>(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)] #[cfg(test)]
mod test; mod test;

View File

@ -11,7 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
use crate::unit::*; use crate::*;
use VolumeMeasure::*; use VolumeMeasure::*;
use std::convert::Into; 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_ltr, "not a gal after normalize call");
assert_normalize!(Gal, into_tsp, "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!()
}

View File

@ -44,9 +44,10 @@ pub enum VolumeMeasure {
Gal(Quantity), // 3800 ml Gal(Quantity), // 3800 ml
/// Fluid Ounces /// Fluid Ounces
Floz(Quantity), // 30 ml Floz(Quantity), // 30 ml
/// Milliliter Measurements.
// Metric volume measurements. // Metric volume measurements.
/// Milliliter Measurements.
ML(Quantity), // Base unit ML(Quantity), // Base unit
// Liter Measurements.
Ltr(Quantity), // 1000 ml Ltr(Quantity), // 1000 ml
} }
use VolumeMeasure::{Cup, Floz, Gal, Ltr, Pint, Qrt, Tbsp, Tsp, 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. /// Convert into milliliters.
pub fn into_ml(self) -> Self { pub fn into_ml(self) -> Self {
ML(self.get_ml()) ML(self.get_ml())
@ -181,12 +189,12 @@ impl PartialEq for VolumeMeasure {
impl Display for VolumeMeasure { impl Display for VolumeMeasure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
Tsp(qty) => write!(f, "{} tsp", qty), Tsp(qty) => write!(f, "{} tsp{}", qty, if qty.plural() { "s" } else { "" }),
Tbsp(qty) => write!(f, "{} tbsp", qty), Tbsp(qty) => write!(f, "{} tbsp{}", qty, if qty.plural() { "s" } else { "" }),
Cup(qty) => write!(f, "{} cup", qty), Cup(qty) => write!(f, "{} cup{}", qty, if qty.plural() { "s" } else { "" }),
Pint(qty) => write!(f, "{} pint", qty), Pint(qty) => write!(f, "{} pint{}", qty, if qty.plural() { "s" } else { "" }),
Qrt(qty) => write!(f, "{} qrt", qty), Qrt(qty) => write!(f, "{} qrt{}", qty, if qty.plural() { "s" } else { "" }),
Gal(qty) => write!(f, "{} gal", qty), Gal(qty) => write!(f, "{} gal{}", qty, if qty.plural() { "s" } else { "" }),
Floz(qty) => write!(f, "{} floz", qty), Floz(qty) => write!(f, "{} floz", qty),
ML(qty) => write!(f, "{} ml", qty), ML(qty) => write!(f, "{} ml", qty),
Ltr(qty) => write!(f, "{} ltr", qty), Ltr(qty) => write!(f, "{} ltr", qty),
@ -216,6 +224,18 @@ impl Measure {
Volume(Tbsp(qty)) 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 { pub fn cup(qty: Quantity) -> Self {
Volume(Cup(qty)) Volume(Cup(qty))
} }
@ -239,6 +259,22 @@ impl Measure {
pub fn gram(qty: Quantity) -> Self { pub fn gram(qty: Quantity) -> Self {
Gram(qty) 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 { impl Display for Measure {
@ -246,7 +282,7 @@ impl Display for Measure {
match self { match self {
Volume(vm) => write!(w, "{}", vm), Volume(vm) => write!(w, "{}", vm),
Count(qty) => write!(w, "{}", qty), 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, 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}; use Quantity::{Frac, Whole};