Show the recipes each item is for in the inventory

fixes #10
This commit is contained in:
Jeremy Wall 2022-03-23 17:38:27 -04:00
parent e40b84173e
commit fbd4aeb59c
5 changed files with 32 additions and 21 deletions

View File

@ -86,7 +86,7 @@ pub fn output_ingredients_list(rs: Vec<Recipe>) {
for r in rs { for r in rs {
acc.accumulate_from(&r); acc.accumulate_from(&r);
} }
for (_, i) in acc.ingredients() { for (_, (i, _)) in acc.ingredients() {
print!("{}", i.amt.normalize()); print!("{}", i.amt.normalize());
println!(" {}", i.name); println!(" {}", i.name);
} }
@ -99,7 +99,7 @@ pub fn output_ingredients_csv(rs: Vec<Recipe>) {
} }
let out = std::io::stdout(); let out = std::io::stdout();
let mut writer = csv::Writer::from_writer(out); let mut writer = csv::Writer::from_writer(out);
for (_, i) in acc.ingredients() { for (_, (i, _)) in acc.ingredients() {
writer writer
.write_record(&[format!("{}", i.amt.normalize()), i.name]) .write_record(&[format!("{}", i.amt.normalize()), i.name])
.expect("Failed to write csv."); .expect("Failed to write csv.");

View File

@ -14,7 +14,7 @@
pub mod parse; pub mod parse;
pub mod unit; pub mod unit;
use std::collections::BTreeMap; use std::collections::{BTreeMap, BTreeSet};
use chrono::NaiveDate; use chrono::NaiveDate;
@ -49,7 +49,7 @@ impl Mealplan {
} }
/// A Recipe with a title, description, and a series of steps. /// A Recipe with a title, description, and a series of steps.
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq, PartialOrd, Ord, Eq)]
pub struct Recipe { pub struct Recipe {
pub title: String, pub title: String,
pub desc: Option<String>, pub desc: Option<String>,
@ -92,11 +92,14 @@ impl Recipe {
let mut acc = IngredientAccumulator::new(); let mut acc = IngredientAccumulator::new();
acc.accumulate_from(&self); acc.accumulate_from(&self);
acc.ingredients() acc.ingredients()
.into_iter()
.map(|(k, v)| (k, v.0))
.collect()
} }
} }
pub struct IngredientAccumulator { pub struct IngredientAccumulator {
inner: BTreeMap<IngredientKey, Ingredient>, inner: BTreeMap<IngredientKey, (Ingredient, BTreeSet<String>)>,
} }
impl IngredientAccumulator { impl IngredientAccumulator {
@ -110,26 +113,31 @@ impl IngredientAccumulator {
for i in r.steps.iter().map(|s| s.ingredients.iter()).flatten() { for i in r.steps.iter().map(|s| s.ingredients.iter()).flatten() {
let key = i.key(); let key = i.key();
if !self.inner.contains_key(&key) { if !self.inner.contains_key(&key) {
self.inner.insert(key, i.clone()); let mut set = BTreeSet::new();
set.insert(r.title.clone());
self.inner.insert(key, (i.clone(), set));
} else { } else {
let amt = match (self.inner[&key].amt, i.amt) { let amt = match (self.inner[&key].0.amt, i.amt) {
(Volume(rvm), Volume(lvm)) => Volume(lvm + rvm), (Volume(rvm), Volume(lvm)) => Volume(lvm + rvm),
(Count(lqty), Count(rqty)) => Count(lqty + rqty), (Count(lqty), Count(rqty)) => Count(lqty + rqty),
(Weight(lqty), Weight(rqty)) => Weight(lqty + rqty), (Weight(lqty), Weight(rqty)) => Weight(lqty + rqty),
_ => unreachable!(), _ => unreachable!(),
}; };
self.inner.get_mut(&key).map(|i| i.amt = amt); self.inner.get_mut(&key).map(|(i, set)| {
i.amt = amt;
set.insert(r.title.clone());
});
} }
} }
} }
pub fn ingredients(self) -> BTreeMap<IngredientKey, Ingredient> { pub fn ingredients(self) -> BTreeMap<IngredientKey, (Ingredient, BTreeSet<String>)> {
self.inner self.inner
} }
} }
/// A Recipe step. It has the time for the step if there is one, instructions, and an ingredients /// A Recipe step. It has the time for the step if there is one, instructions, and an ingredients
/// list. /// list.
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq, PartialOrd, Ord, Eq)]
pub struct Step { pub struct Step {
pub prep_time: Option<std::time::Duration>, pub prep_time: Option<std::time::Duration>,
pub instructions: String, pub instructions: String,
@ -172,7 +180,7 @@ pub struct IngredientKey(String, Option<String>, String);
/// Ingredient in a recipe. The `name` and `form` fields with the measurement type /// Ingredient in a recipe. The `name` and `form` fields with the measurement type
/// uniquely identify an ingredient. /// uniquely identify an ingredient.
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord)]
pub struct Ingredient { pub struct Ingredient {
pub id: Option<i64>, // TODO(jwall): use uuid instead? pub id: Option<i64>, // TODO(jwall): use uuid instead?
pub name: String, pub name: String,

View File

@ -26,7 +26,7 @@ use std::{
use num_rational::Ratio; use num_rational::Ratio;
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug, PartialOrd, Eq, Ord)]
/// Volume Measurements for ingredients in a recipe. /// Volume Measurements for ingredients in a recipe.
pub enum VolumeMeasure { pub enum VolumeMeasure {
// Imperial volume measurements. US. // Imperial volume measurements. US.
@ -225,7 +225,7 @@ impl Display for VolumeMeasure {
} }
} }
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug, PartialOrd, Eq, Ord)]
pub enum WeightMeasure { pub enum WeightMeasure {
Gram(Quantity), Gram(Quantity),
Kilogram(Quantity), Kilogram(Quantity),
@ -338,7 +338,7 @@ impl Display for WeightMeasure {
use WeightMeasure::{Gram, Kilogram, Oz, Pound}; use WeightMeasure::{Gram, Kilogram, Oz, Pound};
#[derive(Copy, Clone, Debug, PartialEq)] #[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Eq, Ord)]
/// Measurements in a Recipe with associated units for them. /// Measurements in a Recipe with associated units for them.
pub enum Measure { pub enum Measure {
/// Volume measurements as meter cubed base unit /// Volume measurements as meter cubed base unit
@ -447,7 +447,7 @@ impl Display for Measure {
} }
/// Represents a Quantity for an ingredient of a recipe. /// Represents a Quantity for an ingredient of a recipe.
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug, Eq, Ord)]
pub enum Quantity { pub enum Quantity {
/// Whole or non fractional quantities of an ingredient in a recipe. /// Whole or non fractional quantities of an ingredient in a recipe.
Whole(u32), Whole(u32),

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
use crate::service::AppService; use crate::service::AppService;
use std::collections::HashMap; use std::collections::HashMap;
use std::collections::{BTreeMap, HashSet}; use std::collections::{BTreeMap, BTreeSet, HashSet};
use recipes::{Ingredient, IngredientKey}; use recipes::{Ingredient, IngredientKey};
use sycamore::{context::use_context, prelude::*}; use sycamore::{context::use_context, prelude::*};
@ -33,7 +33,7 @@ pub fn shopping_list() -> View<G> {
.iter() .iter()
.map(|(k, v)| (k.clone(), v.clone())) .map(|(k, v)| (k.clone(), v.clone()))
.filter(|(k, _v)| !filtered_keys.get().contains(k)) .filter(|(k, _v)| !filtered_keys.get().contains(k))
.collect::<Vec<(IngredientKey, Ingredient)>>() .collect::<Vec<(IngredientKey, (Ingredient, BTreeSet<String>))>>()
})); }));
let table_view = Signal::new(View::empty()); let table_view = Signal::new(View::empty());
create_effect( create_effect(
@ -44,15 +44,17 @@ pub fn shopping_list() -> View<G> {
tr { tr {
th { " Quantity " } th { " Quantity " }
th { " Ingredient " } th { " Ingredient " }
th { " Recipes " }
} }
tbody {Indexed(IndexedProps{ tbody {Indexed(IndexedProps{
iterable: ingredients.clone(), iterable: ingredients.clone(),
template: cloned!((filtered_keys, modified_amts) => move |(k, i)| { template: cloned!((filtered_keys, modified_amts) => move |(k, (i, rs))| {
let mut modified_amt_set = (*modified_amts.get()).clone(); let mut modified_amt_set = (*modified_amts.get()).clone();
let amt = modified_amt_set.entry(k.clone()).or_insert(Signal::new(format!("{}", i.amt.normalize()))).clone(); let amt = modified_amt_set.entry(k.clone()).or_insert(Signal::new(format!("{}", i.amt.normalize()))).clone();
modified_amts.set(modified_amt_set); modified_amts.set(modified_amt_set);
let name = i.name; let name = i.name;
let form = i.form.map(|form| format!("({})", form)).unwrap_or_default(); let form = i.form.map(|form| format!("({})", form)).unwrap_or_default();
let names = rs.iter().fold(String::new(), |acc, s| format!("{}{},", acc, s)).trim_end_matches(",").to_owned();
view! { view! {
tr { tr {
td { input(bind:value=amt.clone(), class="ingredient-count-sel", type="text") } td { input(bind:value=amt.clone(), class="ingredient-count-sel", type="text") }
@ -60,7 +62,8 @@ pub fn shopping_list() -> View<G> {
let mut keyset = (*filtered_keys.get()).clone(); let mut keyset = (*filtered_keys.get()).clone();
keyset.insert(k.clone()); keyset.insert(k.clone());
filtered_keys.set(keyset); filtered_keys.set(keyset);
})) " " (name) " " (form) } })) " " (name) " " (form) "" }
td { (names) }
} }
} }
}), }),

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 std::collections::BTreeMap; use std::collections::{BTreeMap, BTreeSet};
use crate::{console_debug, console_error, console_log}; use crate::{console_debug, console_error, console_log};
@ -124,7 +124,7 @@ impl AppService {
self.recipes.get().get(idx).map(|(_, r)| r.clone()) self.recipes.get().get(idx).map(|(_, r)| r.clone())
} }
pub fn get_shopping_list(&self) -> BTreeMap<IngredientKey, Ingredient> { pub fn get_shopping_list(&self) -> BTreeMap<IngredientKey, (Ingredient, BTreeSet<String>)> {
let mut acc = IngredientAccumulator::new(); let mut acc = IngredientAccumulator::new();
let recipe_counts = self.menu_list.get(); let recipe_counts = self.menu_list.get();
for (idx, count) in recipe_counts.iter() { for (idx, count) in recipe_counts.iter() {