From 96f5733e08af331c2192f875c096d720b084b7cc Mon Sep 17 00:00:00 2001 From: Ben Bridle Date: Sun, 21 Apr 2024 14:00:06 +1200 Subject: Simplify parsing logic --- src/lib.rs | 230 ++++++++++++++++++++---------------------------------------- src/main.rs | 22 ------ 2 files changed, 77 insertions(+), 175 deletions(-) delete mode 100644 src/main.rs diff --git a/src/lib.rs b/src/lib.rs index e961f1c..e671989 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,176 +2,100 @@ pub struct Ingredient { pub name: String, pub quantity: String, pub unit: Option, - pub addendum: Option, + pub note: Option, +} + +impl std::fmt::Display for Ingredient { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + let n = &self.name; + let q = if let Some((whole,frac)) = self.quantity.split_once('-') { + match frac_to_char(frac) { + Some(frac) => format!("{whole}{frac}"), + None => format!("{whole}-{frac}"), + } + } else if self.quantity.contains('/') { + let frac = &self.quantity; + match frac_to_char(frac) { + Some(frac) => format!("{frac}"), + None => format!("{frac}"), + } + } else { + self.quantity.to_string() + }; + + let string = match (&self.unit, &self.note) { + (Some(ref u), Some(ref a)) => format!("{q} {u} of {n} ({a})"), + (Some(ref u), None) => format!("{q} {u} of {n}"), + (None, Some(ref a)) => format!("{q} {n} ({a})"), + (None, None) => format!("{q} {n}"), + }; + f.write_str(&string) + } +} + +fn frac_to_char(frac: &str) -> Option<&str> { + match frac { + "1/2" => Some("½"), + "1/3" => Some("⅓"), + "1/4" => Some("¼"), + "1/8" => Some("⅛"), + "2/3" => Some("⅔"), + "3/4" => Some("¾"), + _ => None, + } } pub struct Recipe { - pub title: Option, pub ingredients: Vec, pub process: Vec, } + impl Recipe { - pub fn parse(recipe: &str) -> Self { + pub fn parse(raw_text: &str) -> Self { let mut ingredients = Vec::new(); let mut process = Vec::new(); - let mut paragraph = String::new(); - let mut title = None; - - for line in recipe.lines() { - if line.trim().is_empty() { - let paragraph = std::mem::take(&mut paragraph); - if !paragraph.is_empty() { - process.push(paragraph); - } - continue; - } - if line.starts_with("# ") && title.is_none() { - title = Some(line[2..].to_string()); - continue; - } + for line in raw_text.lines() { + let line = line.trim(); + if line.is_empty() { continue } + let mut paragraph = String::new(); let chars: Vec = line.chars().collect(); let mut i = 0; - while i < chars.len() { - match capture(&chars[i..]) { - Some((ingredient, length)) => { - paragraph.push_str(&ingredient.name); - ingredients.push(ingredient); - i += length; - } - None => { - paragraph.push(*chars.get(i).unwrap()); - i += 1; + + while let Some(c) = chars.get(i) { + if *c == '{' { + let mut j = i + 1; + while let Some(d) = chars.get(j) { + if *d == '}' { + // {rice, 2 cups} quan unit + // {rice, a handful} quan unit + // {egg, one} quan + let section: String = chars[i+1..j].iter().collect(); + let mut clauses = section.split(','); + let name = clauses.next().unwrap_or("").trim().to_string(); + let (quantity, unit) = { + let quantity_unit = clauses.next().unwrap_or("").trim(); + match quantity_unit.split_once(' ') { + Some((quantity, unit)) => (quantity.to_string(), Some(unit.to_string())), + None => (quantity_unit.to_string(), None) + } + }; + let note = clauses.next().and_then(|s| Some(s.trim().to_string())); + paragraph.push_str(&name); + ingredients.push(Ingredient { name, quantity, unit, note }); + i = j; break + } + j += 1; } + } else { + paragraph.push(*c); } - } - } - - let paragraph = std::mem::take(&mut paragraph); - if !paragraph.is_empty() { - process.push(paragraph); - } - - Self { - title, - ingredients, - process, - } - } -} - -fn capture(chars: &[char]) -> Option<(Ingredient, usize)> { - if chars.get(0) != Some(&'{') { - return None; - } - let mut i = 1; - let mut name = String::new(); - let mut quantity = String::new(); - let mut unit = None; - let mut addendum = None; - - // Ingredient name - loop { - match chars.get(i) { - Some(&',') => { i += 1; - break; } - Some(c) => { - name.push(*c); - i += 1; - } - None => return None, + if !paragraph.is_empty() { process.push(paragraph) } } - } - // Eat spaces - loop { - match chars.get(i) { - Some(&' ') => i += 1, - Some(_) => break, - None => return None, - } + Self { ingredients, process } } - - // Quantity - loop { - match chars.get(i) { - Some(&' ') => { - i += 1; - unit = Some(String::new()); - break; - } - Some(&',') => { - i += 1; - addendum = Some(String::new()); - break; - } - Some(&'}') => { - i += 1; - break; - } - Some(c) => { - quantity.push(*c); - i += 1; - } - None => return None, - } - } - - // Unit - - if let Some(ref mut unit) = unit { - loop { - match chars.get(i) { - Some(&'}') => { - i += 1; - break; - } - Some(&',') => { - i += 1; - addendum = Some(String::new()); - break; - } - Some(c) => { - unit.push(*c); - i += 1; - } - None => return None, - } - } - } - - // Addendum - if let Some(ref mut addendum) = addendum { - loop { - match chars.get(i) { - Some(&'}') => { - i += 1; - break; - } - Some(c) => { - addendum.push(*c); - i += 1; - } - None => return None, - } - } - } - - // Trim values - let name = name.trim().to_string(); - let quantity = quantity.trim().to_string(); - let unit = unit.and_then(|s| Some(s.trim().to_string())); - let addendum = addendum.and_then(|s| Some(s.trim().to_string())); - - Some(( - Ingredient { - name, - quantity, - unit, - addendum, - }, - i, - )) } + diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 96fac3a..0000000 --- a/src/main.rs +++ /dev/null @@ -1,22 +0,0 @@ -use recipe_parser::Recipe; - -fn main() { - let recipe = Recipe::parse( - "Combine {bulghur wheat, 1 cup} and {boiling water, 1 cup, for wheat}. Set aside for 20 minutes. Put {whole-meal flour, 2 cups}, {salt, 2 tsp}, and {yeast, 2 tbsp} in a bowl and stir together. Add {cold water, 1 cup} and {golden syrup, 1 tbsp}, immediately followed by {boiling water, 1 cup}. Stir to a smooth paste and stand for 2 to 3 minutes. Mix in the {egg, 1} and {high-grade flour, 3 cups}, adding the last cup of flour slowly (more or less than the cup may be needed to give a very thick batter, which is not quite as stiff as dough). Mix for 3 to 4 minutes. Cover and put in a warm place for 15 minutes. Stir well and pour into two 22cm greased loaf tins. Put in a warm place until the dough doubles in volume. Bake at 200°C for 35 minutes or until the loaf sounds hollow when tapped on the bottom. If loaf is browning too quickly, cover with foil.", - ); - for i in recipe.ingredients { - let unit = match i.unit { - Some(unit) => format!("{} of ", unit), - None => String::new(), - }; - let addendum = match i.addendum { - Some(addendum) => format!(" ({})", addendum), - None => String::new(), - }; - println!("- {} {}{}{}", i.quantity, unit, i.name, addendum); - } - println!(); - for paragraph in recipe.process { - println!("{}\n", paragraph); - } -} -- cgit v1.2.3-70-g09d2