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.
|
// 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;
|
||||||
|
@ -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!()
|
||||||
|
}
|
||||||
|
@ -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};
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user