Add rudimentary ingredient parsing and merging
This commit is contained in:
parent
e69d8fc24c
commit
7025ee9e3a
|
@ -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<String>)> {
|
||||
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<String>)>, _) = {
|
||||
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<String> {
|
||||
fn prepare_grocery_list(ingredients: &Vec<(Ingredient, Vec<String>)>) -> Result<String> {
|
||||
let mut out = String::new();
|
||||
use std::fmt::Write;
|
||||
|
||||
|
@ -103,9 +130,11 @@ fn prepare_grocery_list(ingredients: &Vec<(Ingredient, String)>) -> Result<Strin
|
|||
chrono::Local::now().format("%Y-%m-%d").to_string()
|
||||
)?;
|
||||
writeln!(out)?; // leave an empty line
|
||||
for ingredient in ingredients {
|
||||
let ingredient_s = ingredient.0 .0.as_str();
|
||||
writeln!(out, "- [ ] {} ({})", ingredient_s, ingredient.1)?;
|
||||
for (ingredient, recipes) in ingredients {
|
||||
writeln!(out, "- [ ] {}", ingredient)?;
|
||||
for recipe in recipes {
|
||||
writeln!(out, " * {}", recipe)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
|
|
157
src/recipe.rs
157
src/recipe.rs
|
@ -60,9 +60,19 @@ pub struct Recipe {
|
|||
//pub nutrition: Nutrition,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Ord, Eq, PartialEq, PartialOrd)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Ingredient(pub String);
|
||||
#[derive(Debug, PartialEq, PartialOrd)]
|
||||
pub struct Ingredient {
|
||||
pub name: String,
|
||||
pub amount: f64,
|
||||
pub unit: Unit,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, PartialOrd)]
|
||||
pub enum Unit {
|
||||
None,
|
||||
Kilogram,
|
||||
Liter,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
@ -130,3 +140,144 @@ impl Recipe {
|
|||
.unwrap_or_else(|| self.prep_time + self.cook_time.unwrap_or(Duration::zero()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Ingredient {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_str(IngredientVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
struct IngredientVisitor;
|
||||
|
||||
impl<'de> serde::de::Visitor<'de> for IngredientVisitor {
|
||||
type Value = Ingredient;
|
||||
|
||||
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
use std::cmp::min;
|
||||
|
||||
let quantity_regex_start: regex::Regex =
|
||||
regex::Regex::new(r"^(?P<v>[0-9,\.]+)\s*(?P<u>\w+)?\s+(?P<i>.*)").unwrap();
|
||||
let quantity_regex_end: regex::Regex =
|
||||
regex::Regex::new(r"(?P<i>.*?)\s+(?P<v>[0-9,\.]+)\s*(?P<u>\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<E>(value: &str, unit: &str) -> Result<(f64, Unit), E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
let mut v = value
|
||||
.parse::<f64>()
|
||||
.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)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue