diff --git a/src/commands/groceries.rs b/src/commands/groceries.rs index 67525c0..4eb1062 100644 --- a/src/commands/groceries.rs +++ b/src/commands/groceries.rs @@ -85,15 +85,42 @@ where Ok(ingredients.into_iter().flatten().collect()) } -fn merge_ingredients(mut ingredients: Vec<(Ingredient, String)>) -> Vec<(Ingredient, String)> { - ingredients.sort(); +fn merge_ingredients(mut ingredients: Vec<(Ingredient, String)>) -> Vec<(Ingredient, Vec)> { + if ingredients.is_empty() { + return vec![]; + } - // TODO actual merging + // Prime merged_ingredients with the first ingredient in sorted order. + ingredients.sort_by(|(a, _), (b, _)| { + a.name.cmp(&b.name).then_with(|| { + // inefficient, but not so bad for now + a.unit.to_string().cmp(&b.unit.to_string()) + }) + }); + let (mut merged_ingredients, ingredients): (Vec<(Ingredient, Vec)>, _) = { + let v = ingredients.split_off(1); + ( + ingredients.into_iter().map(|(i, s)| (i, vec![s])).collect(), + v, + ) + }; - ingredients + for (ingredient, recipe) in ingredients { + // If it can be summed to the last item of merged_ingredients, do it; + // else append it + let (last_i, last_rs) = merged_ingredients.last_mut().unwrap(); + if last_i.name == ingredient.name && last_i.unit == ingredient.unit { + last_i.amount += ingredient.amount; + last_rs.push(recipe); + } else { + merged_ingredients.push((ingredient, vec![recipe])); + } + } + + merged_ingredients } -fn prepare_grocery_list(ingredients: &Vec<(Ingredient, String)>) -> Result { +fn prepare_grocery_list(ingredients: &Vec<(Ingredient, Vec)>) -> Result { let mut out = String::new(); use std::fmt::Write; @@ -103,9 +130,11 @@ fn prepare_grocery_list(ingredients: &Vec<(Ingredient, String)>) -> Result Deserialize<'de> for Ingredient { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_str(IngredientVisitor) + } +} + +struct IngredientVisitor; + +impl<'de> serde::de::Visitor<'de> for IngredientVisitor { + type Value = Ingredient; + + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + use std::cmp::min; + + let quantity_regex_start: regex::Regex = + regex::Regex::new(r"^(?P[0-9,\.]+)\s*(?P\w+)?\s+(?P.*)").unwrap(); + let quantity_regex_end: regex::Regex = + regex::Regex::new(r"(?P.*?)\s+(?P[0-9,\.]+)\s*(?P\w+)?$").unwrap(); + + if let Some(captures) = quantity_regex_start + .captures(value) + .or_else(|| quantity_regex_end.captures(value)) + { + let v = captures.name("v").unwrap().as_str(); + let um = captures.name("u"); + let u = um.map(|u| u.as_str()).unwrap_or(""); + let im = captures.name("i").unwrap(); + let i = im.as_str(); + + let (amount, unit) = Self::parse_quantity(v, u)?; + let name = match unit { + Unit::None => { + // pick the longest string including what was recognized by the regex as the possible unit + let start = um + .map(|um| min(um.start(), im.start())) + .unwrap_or_else(|| im.start()); + &value[start..im.end()] + } + _ => i, + } + .to_owned(); + + Ok(Ingredient { name, amount, unit }) + } else { + Ok(Ingredient { + name: value.to_owned(), + amount: 1.0, + unit: Unit::None, + }) + } + } + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a parseable ingredient name with an optional mass or volume quantity at the beginning or end") + } +} + +impl IngredientVisitor { + fn parse_quantity(value: &str, unit: &str) -> Result<(f64, Unit), E> + where + E: serde::de::Error, + { + let mut v = value + .parse::() + .map_err(|e| serde::de::Error::custom(e.to_string()))?; + let u = match unit.to_lowercase().as_str() { + "kg" => Unit::Kilogram, + "g" | "gr" => { + v = v / 1000.0; + Unit::Kilogram + } + "hg" => { + v = v / 10.0; + Unit::Kilogram + } + "l" => Unit::Liter, + "dl" => { + v = v / 10.0; + Unit::Liter + } + "cl" => { + v = v / 100.0; + Unit::Liter + } + "ml" => { + v = v / 1000.0; + Unit::Liter + } + "tl" => { + v = v / 1000.0 * 5.0; + Unit::Liter + } + "el" => { + v = v / 1000.0 * 15.0; + Unit::Liter + } + "tsp" => { + v = v / 1000.0 * 4.93; + Unit::Liter + } + "tblsp" => { + v = v / 1000.0 * 14.79; + Unit::Liter + } + _ => Unit::None, + }; + + Ok((v, u)) + } +} + +impl core::fmt::Display for Ingredient { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{amount:.3}{unit} {name}", + name = self.name, + amount = self.amount, + unit = self.unit + ) + } +} + +impl core::fmt::Display for Unit { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let u: &dyn core::fmt::Display = match self { + Unit::Kilogram => &"kg", + Unit::Liter => &"L", + Unit::None => &"", + }; + + write!(f, "{}", u) + } +}