241 lines
6.6 KiB
Rust
Raw Normal View History

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.
mod parse;
2021-04-12 20:22:18 -04:00
pub mod unit;
2021-04-13 20:14:25 -04:00
use std::collections::BTreeMap;
use std::str::FromStr;
2021-04-13 20:14:25 -04:00
use chrono::NaiveDate;
use uuid::{self, Uuid};
use parse::{ingredient, measure};
2021-04-12 20:22:18 -04:00
use unit::*;
#[derive(Debug, Clone, PartialEq)]
pub struct Mealplan {
pub id: uuid::Uuid,
pub start_date: Option<NaiveDate>,
pub recipes: Vec<Recipe>,
}
impl Mealplan {
pub fn new() -> Self {
Self::new_id(uuid::Uuid::new_v4())
}
pub fn new_id(id: Uuid) -> Self {
Self {
id: id,
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())
}
}
2021-04-13 20:14:25 -04:00
/// A Recipe with a title, description, and a series of steps.
2021-04-29 18:41:54 -04:00
#[derive(Debug, Clone, PartialEq)]
2021-04-12 20:22:18 -04:00
pub struct Recipe {
2021-05-08 15:01:20 -04:00
pub id: uuid::Uuid,
2021-04-12 20:22:18 -04:00
pub title: String,
pub desc: String,
pub steps: Vec<Step>,
}
2021-04-13 20:14:25 -04:00
impl Recipe {
2021-04-29 18:41:54 -04:00
pub fn new<S: Into<String>>(title: S, desc: S) -> Self {
2021-04-13 20:14:25 -04:00
Self {
id: uuid::Uuid::new_v4(),
2021-04-29 18:41:54 -04:00
title: title.into(),
desc: desc.into(),
steps: Vec::new(),
}
}
2021-05-08 15:01:20 -04:00
pub fn new_with_id<S: Into<String>>(id: uuid::Uuid, title: S, desc: S) -> Self {
2021-04-29 18:41:54 -04:00
Self {
id: id,
2021-04-29 18:41:54 -04:00
title: title.into(),
desc: desc.into(),
2021-04-13 20:14:25 -04:00
steps: Vec::new(),
}
}
/// Add steps to the end of the recipe.
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> {
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.
2021-04-29 18:41:54 -04:00
#[derive(Debug, Clone, PartialEq)]
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 {
prep_time: prep_time,
instructions: instructions.into(),
ingredients: Vec::new(),
}
}
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)
#[derive(PartialEq, PartialOrd, Eq, Ord)]
2021-04-29 18:41:54 -04:00
pub struct IngredientKey(String, Option<String>, String);
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.
2021-04-29 18:41:54 -04:00
#[derive(Debug, Clone, PartialEq)]
2021-04-12 20:22:18 -04:00
pub struct Ingredient {
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
pub category: String,
}
2021-04-13 20:14:25 -04:00
impl Ingredient {
2021-04-29 18:41:54 -04:00
pub fn new<S: Into<String>>(name: S, form: Option<String>, amt: Measure, category: S) -> 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,
category: category.into(),
}
}
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,
category: S,
) -> Self {
Self {
id: Some(id),
2021-04-13 20:14:25 -04:00
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(),
);
}
pub fn parse(s: &str) -> Result<Ingredient, String> {
Ok(match ingredient(abortable_parser::StrIter::new(s)) {
abortable_parser::Result::Complete(_, ing) => ing,
abortable_parser::Result::Abort(e) | abortable_parser::Result::Fail(e) => {
return Err(format!("Failed to parse as Ingredient {:?}", e))
}
abortable_parser::Result::Incomplete(_) => {
return Err(format!("Incomplete input: {}", s))
}
})
}
}
impl FromStr for Ingredient {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ingredient::parse(s)
}
2021-04-13 20:14:25 -04:00
}
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;