2021-04-12 20:22:18 -04:00
|
|
|
// Copyright 2021 Jeremy Wall
|
|
|
|
//
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
// You may obtain a copy of the License at
|
|
|
|
//
|
|
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
//
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
// 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.
|
2021-11-02 21:09:00 -04:00
|
|
|
pub mod parse;
|
2021-04-12 20:22:18 -04:00
|
|
|
pub mod unit;
|
|
|
|
|
2022-03-23 17:38:27 -04:00
|
|
|
use std::collections::{BTreeMap, BTreeSet};
|
2021-04-13 20:14:25 -04:00
|
|
|
|
2021-05-04 18:46:46 -04:00
|
|
|
use chrono::NaiveDate;
|
2022-10-21 14:57:50 -04:00
|
|
|
use serde::{Deserialize, Serialize};
|
2021-05-04 18:46:46 -04:00
|
|
|
|
2021-04-12 20:22:18 -04:00
|
|
|
use unit::*;
|
2021-11-20 12:17:04 -05:00
|
|
|
use Measure::*;
|
2021-04-12 20:22:18 -04:00
|
|
|
|
2021-05-04 18:46:46 -04:00
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
|
|
pub struct Mealplan {
|
|
|
|
pub start_date: Option<NaiveDate>,
|
|
|
|
pub recipes: Vec<Recipe>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Mealplan {
|
|
|
|
pub fn new() -> Self {
|
|
|
|
Self {
|
|
|
|
start_date: None,
|
|
|
|
recipes: Vec::new(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn with_start_date(mut self, start_date: NaiveDate) -> Self {
|
|
|
|
self.start_date = Some(start_date);
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn add_recipes<Iter>(&mut self, recipes: Iter)
|
|
|
|
where
|
|
|
|
Iter: IntoIterator<Item = Recipe>,
|
|
|
|
{
|
|
|
|
self.recipes.extend(recipes.into_iter())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-21 14:57:50 -04:00
|
|
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
2024-07-12 17:43:05 -04:00
|
|
|
pub struct RecipeEntry {
|
|
|
|
pub id: String,
|
|
|
|
pub text: String,
|
|
|
|
pub category: Option<String>,
|
|
|
|
pub serving_count: Option<i64>,
|
|
|
|
}
|
2022-10-21 14:57:50 -04:00
|
|
|
|
|
|
|
impl RecipeEntry {
|
2022-11-07 16:47:46 -05:00
|
|
|
pub fn new<IS: Into<String>, TS: Into<String>>(recipe_id: IS, text: TS) -> Self {
|
2024-07-12 17:43:05 -04:00
|
|
|
Self {
|
|
|
|
id: recipe_id.into(),
|
|
|
|
text: text.into(),
|
|
|
|
category: None,
|
|
|
|
serving_count: None,
|
|
|
|
}
|
2022-11-07 16:47:46 -05:00
|
|
|
}
|
|
|
|
|
2022-11-01 20:38:14 -04:00
|
|
|
pub fn set_recipe_id<S: Into<String>>(&mut self, id: S) {
|
2024-07-12 17:43:05 -04:00
|
|
|
self.id = id.into();
|
2022-11-01 20:38:14 -04:00
|
|
|
}
|
|
|
|
|
2022-10-21 14:57:50 -04:00
|
|
|
pub fn recipe_id(&self) -> &str {
|
2024-07-12 17:43:05 -04:00
|
|
|
self.id.as_str()
|
2022-10-21 14:57:50 -04:00
|
|
|
}
|
|
|
|
|
2022-11-01 20:38:14 -04:00
|
|
|
pub fn set_recipe_text<S: Into<String>>(&mut self, text: S) {
|
2024-07-12 17:43:05 -04:00
|
|
|
self.text = text.into();
|
2022-11-01 20:38:14 -04:00
|
|
|
}
|
|
|
|
|
2022-10-21 14:57:50 -04:00
|
|
|
pub fn recipe_text(&self) -> &str {
|
2024-07-12 17:43:05 -04:00
|
|
|
self.text.as_str()
|
2022-10-21 14:57:50 -04:00
|
|
|
}
|
2023-01-24 17:46:47 -05:00
|
|
|
|
|
|
|
pub fn set_category<S: Into<String>>(&mut self, cat: S) {
|
2024-07-12 17:43:05 -04:00
|
|
|
self.category = Some(cat.into());
|
2023-01-24 17:46:47 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn category(&self) -> Option<&String> {
|
2024-07-12 17:43:05 -04:00
|
|
|
self.category.as_ref()
|
2023-01-24 17:46:47 -05:00
|
|
|
}
|
2024-07-12 17:43:05 -04:00
|
|
|
|
2024-07-10 11:49:39 -04:00
|
|
|
pub fn serving_count(&self) -> Option<i64> {
|
2024-07-12 17:43:05 -04:00
|
|
|
self.serving_count.clone()
|
2024-07-10 11:49:39 -04:00
|
|
|
}
|
2022-10-21 14:57:50 -04:00
|
|
|
}
|
|
|
|
|
2021-04-13 20:14:25 -04:00
|
|
|
/// A Recipe with a title, description, and a series of steps.
|
2022-03-23 17:38:27 -04:00
|
|
|
#[derive(Debug, Clone, PartialEq, PartialOrd, Ord, Eq)]
|
2021-04-12 20:22:18 -04:00
|
|
|
pub struct Recipe {
|
|
|
|
pub title: String,
|
2021-11-02 21:45:41 -04:00
|
|
|
pub desc: Option<String>,
|
2021-04-12 20:22:18 -04:00
|
|
|
pub steps: Vec<Step>,
|
|
|
|
}
|
|
|
|
|
2021-04-13 20:14:25 -04:00
|
|
|
impl Recipe {
|
2021-11-02 21:45:41 -04:00
|
|
|
pub fn new<S: Into<String>>(title: S, desc: Option<S>) -> Self {
|
2021-04-13 20:14:25 -04:00
|
|
|
Self {
|
2021-04-29 18:41:54 -04:00
|
|
|
title: title.into(),
|
2021-11-02 21:45:41 -04:00
|
|
|
desc: desc.map(|s| s.into()),
|
2021-04-13 20:14:25 -04:00
|
|
|
steps: Vec::new(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-02 21:45:41 -04:00
|
|
|
pub fn with_steps<Iter>(mut self, steps: Iter) -> Self
|
|
|
|
where
|
|
|
|
Iter: IntoIterator<Item = Step>,
|
|
|
|
{
|
|
|
|
self.add_steps(steps);
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
2021-04-13 20:14:25 -04:00
|
|
|
/// Add steps to the end of the recipe.
|
2021-05-04 18:46:46 -04:00
|
|
|
pub fn add_steps<Iter>(&mut self, steps: Iter)
|
|
|
|
where
|
|
|
|
Iter: IntoIterator<Item = Step>,
|
|
|
|
{
|
2021-04-13 20:14:25 -04:00
|
|
|
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> {
|
2021-11-20 12:17:04 -05:00
|
|
|
let mut acc = IngredientAccumulator::new();
|
|
|
|
acc.accumulate_from(&self);
|
|
|
|
acc.ingredients()
|
2022-03-23 17:38:27 -04:00
|
|
|
.into_iter()
|
|
|
|
.map(|(k, v)| (k, v.0))
|
|
|
|
.collect()
|
2021-04-13 20:14:25 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-20 12:17:04 -05:00
|
|
|
pub struct IngredientAccumulator {
|
2022-03-23 17:38:27 -04:00
|
|
|
inner: BTreeMap<IngredientKey, (Ingredient, BTreeSet<String>)>,
|
2021-11-20 12:17:04 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
impl IngredientAccumulator {
|
|
|
|
pub fn new() -> Self {
|
|
|
|
Self {
|
|
|
|
inner: BTreeMap::new(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-06 11:04:03 -05:00
|
|
|
pub fn accumulate_ingredients_for<'a, Iter, S>(&'a mut self, recipe_title: S, ingredients: Iter)
|
|
|
|
where
|
|
|
|
Iter: Iterator<Item = &'a Ingredient>,
|
|
|
|
S: Into<String>,
|
|
|
|
{
|
|
|
|
let recipe_title = recipe_title.into();
|
|
|
|
for i in ingredients {
|
2021-11-20 12:17:04 -05:00
|
|
|
let key = i.key();
|
|
|
|
if !self.inner.contains_key(&key) {
|
2022-03-23 17:38:27 -04:00
|
|
|
let mut set = BTreeSet::new();
|
2023-01-06 11:04:03 -05:00
|
|
|
set.insert(recipe_title.clone());
|
2022-03-23 17:38:27 -04:00
|
|
|
self.inner.insert(key, (i.clone(), set));
|
2021-11-20 12:17:04 -05:00
|
|
|
} else {
|
2024-01-03 14:02:48 -05:00
|
|
|
let amts = match (&self.inner[&key].0.amt, &i.amt) {
|
|
|
|
(Volume(rvm), Volume(lvm)) => vec![Volume(lvm + rvm)],
|
|
|
|
(Count(lqty), Count(rqty)) => vec![Count(lqty + rqty)],
|
|
|
|
(Weight(lqty), Weight(rqty)) => vec![Weight(lqty + rqty)],
|
|
|
|
(Package(lnm, lqty), Package(rnm, rqty)) => {
|
|
|
|
if lnm == rnm {
|
|
|
|
vec![Package(lnm.clone(), lqty + rqty)]
|
|
|
|
} else {
|
2024-01-05 18:58:48 -05:00
|
|
|
vec![
|
|
|
|
Package(lnm.clone(), lqty.clone()),
|
|
|
|
Package(rnm.clone(), rqty.clone()),
|
|
|
|
]
|
2024-01-03 14:02:48 -05:00
|
|
|
}
|
|
|
|
}
|
2021-11-20 12:17:04 -05:00
|
|
|
_ => unreachable!(),
|
|
|
|
};
|
2024-01-03 14:02:48 -05:00
|
|
|
for amt in amts {
|
|
|
|
self.inner.get_mut(&key).map(|(i, set)| {
|
|
|
|
i.amt = amt;
|
|
|
|
set.insert(recipe_title.clone());
|
|
|
|
});
|
|
|
|
}
|
2021-11-20 12:17:04 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-06 11:04:03 -05:00
|
|
|
pub fn accumulate_from(&mut self, r: &Recipe) {
|
|
|
|
self.accumulate_ingredients_for(
|
|
|
|
&r.title,
|
|
|
|
r.steps.iter().map(|s| s.ingredients.iter()).flatten(),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-03-23 17:38:27 -04:00
|
|
|
pub fn ingredients(self) -> BTreeMap<IngredientKey, (Ingredient, BTreeSet<String>)> {
|
2021-11-20 12:17:04 -05:00
|
|
|
self.inner
|
|
|
|
}
|
|
|
|
}
|
2022-04-28 21:43:30 -04:00
|
|
|
|
2021-04-13 20:14:25 -04:00
|
|
|
/// A Recipe step. It has the time for the step if there is one, instructions, and an ingredients
|
|
|
|
/// list.
|
2022-03-23 17:38:27 -04:00
|
|
|
#[derive(Debug, Clone, PartialEq, PartialOrd, Ord, Eq)]
|
2021-04-12 20:22:18 -04:00
|
|
|
pub struct Step {
|
2021-04-13 20:14:25 -04:00
|
|
|
pub prep_time: Option<std::time::Duration>,
|
2021-04-12 20:22:18 -04:00
|
|
|
pub instructions: String,
|
|
|
|
pub ingredients: Vec<Ingredient>,
|
|
|
|
}
|
|
|
|
|
2021-04-29 18:41:54 -04:00
|
|
|
impl Step {
|
|
|
|
pub fn new<S: Into<String>>(prep_time: Option<std::time::Duration>, instructions: S) -> Self {
|
|
|
|
Self {
|
2024-07-13 20:11:11 -04:00
|
|
|
prep_time,
|
2021-04-29 18:41:54 -04:00
|
|
|
instructions: instructions.into(),
|
|
|
|
ingredients: Vec::new(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-02 21:09:00 -04:00
|
|
|
pub fn with_ingredients<Iter>(mut self, ingredients: Iter) -> Step
|
|
|
|
where
|
|
|
|
Iter: IntoIterator<Item = Ingredient>,
|
|
|
|
{
|
|
|
|
self.add_ingredients(ingredients);
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
2021-05-04 18:46:46 -04:00
|
|
|
pub fn add_ingredients<Iter>(&mut self, ingredients: Iter)
|
|
|
|
where
|
|
|
|
Iter: IntoIterator<Item = Ingredient>,
|
|
|
|
{
|
|
|
|
self.ingredients.extend(ingredients.into_iter());
|
2021-04-29 18:41:54 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn add_ingredient(&mut self, ingredient: Ingredient) {
|
|
|
|
self.ingredients.push(ingredient);
|
|
|
|
}
|
2021-04-12 20:22:18 -04:00
|
|
|
}
|
|
|
|
|
2021-04-13 20:14:25 -04:00
|
|
|
/// Unique identifier for an Ingredient. Ingredients are identified by name, form,
|
|
|
|
/// and measurement type. (Volume, Count, Weight)
|
2022-11-11 17:35:10 -05:00
|
|
|
#[derive(PartialEq, PartialOrd, Eq, Ord, Clone, Hash, Debug, Deserialize, Serialize)]
|
2021-04-29 18:41:54 -04:00
|
|
|
pub struct IngredientKey(String, Option<String>, String);
|
2021-04-13 20:14:25 -04:00
|
|
|
|
2022-11-11 17:35:10 -05:00
|
|
|
impl IngredientKey {
|
|
|
|
pub fn new(name: String, form: Option<String>, measure_type: String) -> Self {
|
|
|
|
Self(name, form, measure_type)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn name(&self) -> &String {
|
|
|
|
&self.0
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn form(&self) -> String {
|
|
|
|
self.1.clone().unwrap_or_else(|| String::new())
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn measure_type(&self) -> &String {
|
|
|
|
&self.2
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-13 20:14:25 -04:00
|
|
|
/// Ingredient in a recipe. The `name` and `form` fields with the measurement type
|
|
|
|
/// uniquely identify an ingredient.
|
2022-03-23 17:38:27 -04:00
|
|
|
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord)]
|
2021-04-12 20:22:18 -04:00
|
|
|
pub struct Ingredient {
|
2021-05-04 18:46:46 -04:00
|
|
|
pub id: Option<i64>, // TODO(jwall): use uuid instead?
|
2021-04-12 20:22:18 -04:00
|
|
|
pub name: String,
|
2021-04-29 18:41:54 -04:00
|
|
|
pub form: Option<String>,
|
2021-04-13 20:14:25 -04:00
|
|
|
pub amt: Measure,
|
2021-04-12 20:22:18 -04:00
|
|
|
}
|
|
|
|
|
2021-04-13 20:14:25 -04:00
|
|
|
impl Ingredient {
|
2023-01-05 17:56:15 -05:00
|
|
|
pub fn new<S: Into<String>>(name: S, form: Option<String>, amt: Measure) -> Self {
|
2021-04-13 20:14:25 -04:00
|
|
|
Self {
|
2021-04-29 18:41:54 -04:00
|
|
|
id: None,
|
|
|
|
name: name.into(),
|
|
|
|
form,
|
|
|
|
amt,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-08 15:01:20 -04:00
|
|
|
pub fn new_with_id<S: Into<String>>(
|
2021-04-29 18:41:54 -04:00
|
|
|
id: i64,
|
|
|
|
name: S,
|
|
|
|
form: Option<String>,
|
|
|
|
amt: Measure,
|
|
|
|
) -> Self {
|
|
|
|
Self {
|
|
|
|
id: Some(id),
|
2021-04-13 20:14:25 -04:00
|
|
|
name: name.into(),
|
|
|
|
form,
|
|
|
|
amt,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// 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)?;
|
2021-04-29 18:41:54 -04:00
|
|
|
if let Some(f) = &self.form {
|
|
|
|
write!(w, " ({})", f)?;
|
|
|
|
}
|
|
|
|
Ok(())
|
2021-04-13 20:14:25 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-12 20:22:18 -04:00
|
|
|
#[cfg(test)]
|
|
|
|
mod test;
|