mirror of
https://github.com/zaphar/kitchen.git
synced 2025-07-22 19:40:14 -04:00
Recipe Datamodel: Recipe
This commit is contained in:
parent
1579d16943
commit
b96ee641af
@ -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<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 prep_time: std::time::Duration,
|
||||
pub prep_time: Option<std::time::Duration>,
|
||||
pub instructions: String,
|
||||
pub ingredients: Vec<Ingredient>,
|
||||
}
|
||||
|
||||
/// 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<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)]
|
||||
mod test;
|
||||
|
@ -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!()
|
||||
}
|
||||
|
@ -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};
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user