From 6651bf2996b47dee75b8fe184cdbdef20832e4e7 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Sat, 20 Nov 2021 12:17:04 -0500 Subject: [PATCH] Menu lists can output the ingredients list for shopping. --- examples/cornbread_dressing.txt | 41 ++++++++++++++++++++++++ examples/meatloaf.txt | 5 +-- examples/menu.txt | 2 ++ kitchen/src/cli.rs | 48 +++++++++++++++++++++++++--- kitchen/src/main.rs | 34 +++++++++++++++++--- recipes/src/lib.rs | 56 +++++++++++++++++++++------------ recipes/src/test.rs | 18 ++++++++--- 7 files changed, 168 insertions(+), 36 deletions(-) create mode 100644 examples/cornbread_dressing.txt create mode 100644 examples/menu.txt diff --git a/examples/cornbread_dressing.txt b/examples/cornbread_dressing.txt new file mode 100644 index 0000000..2a33b12 --- /dev/null +++ b/examples/cornbread_dressing.txt @@ -0,0 +1,41 @@ +title: Cornbread Dressing + +A lovely dressing recipe for the holidays. + +step: + +1 package dry cornbread mix + +Prepare corn bread as directed on package. Cool, and crumble. + +step: + +1 cup butter +2 onions (chopped) +1 green bell pepper (chopped) +6 stalks celery (chopped) +1 pound pork sausage + +Melt butter in a large skillet over medium heat. Cook onions, bell pepper, and +celery in butter until tender, but not brown. In another pan, cook sausage over +medium-high heat until evenly browned. + +step: + +16 slices white bread +2 tsp dried sage +1 tsp dried thyme +1 tsp poultry seasoning +1 tsp salt +1/2 teaspoon ground black pepper +1/2 cup chopped fresh parsley +2 eggs +4 cups chicken stock + +Place corn bread and bread slices in a food processor. Pulse until they turn +into a crumbly mixture. Transfer mixture to a large bowl. Season with sage, +thyme, poultry seasoning, salt, and pepper. Mix in chopped parsley, cooked +vegetables, and sausage with drippings. Stir in eggs and chicken stock. This +mixture should be a bit mushy. Transfer to a greased 9x13 inch pan. + +Bake at 325 degrees F (165 degrees C) for 1 hour. \ No newline at end of file diff --git a/examples/meatloaf.txt b/examples/meatloaf.txt index d5e60fe..1c7f8ed 100644 --- a/examples/meatloaf.txt +++ b/examples/meatloaf.txt @@ -12,5 +12,6 @@ step: 2 tbsp salt 1/2 cup ketchup -Mix ingredients excluding the ketchup together thoroughly. Bake in oven for 35 minutes at 350. -Cover with ketchup and cook for an additional 10 minutes. Cut into slices and serve. \ No newline at end of file +Mix ingredients excluding the ketchup together thoroughly. Bake in oven for 35 +minutes at 350. Cover with ketchup and cook for an additional 10 minutes. Cut +into slices and serve. \ No newline at end of file diff --git a/examples/menu.txt b/examples/menu.txt new file mode 100644 index 0000000..76f31f0 --- /dev/null +++ b/examples/menu.txt @@ -0,0 +1,2 @@ +meatloaf.txt +cornbread_dressing.txt \ No newline at end of file diff --git a/kitchen/src/cli.rs b/kitchen/src/cli.rs index e0c262d..ec2e9e0 100644 --- a/kitchen/src/cli.rs +++ b/kitchen/src/cli.rs @@ -11,9 +11,10 @@ // 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. +use std::fmt::Debug; use std::fs::File; -use std::io::BufReader; use std::io::Read; +use std::io::{BufRead, BufReader}; use std::path::Path; use recipes::{parse, Recipe}; @@ -24,8 +25,22 @@ pub enum ParseError { Syntax(String), } +macro_rules! try_open { + ($path:expr) => { + match File::open(&$path) { + Ok(reader) => reader, + Err(e) => { + eprintln!("Error opening file for read: {:?}", $path); + return Err(ParseError::from(e)); + } + } + }; +} + impl From for ParseError { fn from(err: std::io::Error) -> Self { + // TODO(jwall): This error should allow us to collect more information + // about the cause of the error. ParseError::IO(err) } } @@ -38,11 +53,34 @@ impl From for ParseError { pub fn parse_recipe

(path: P) -> Result where - P: AsRef, + P: AsRef + Debug, { - let mut br = BufReader::new(File::open(path)?); + let mut br = BufReader::new(try_open!(path)); let mut buf = Vec::new(); - br.read_to_end(&mut buf)?; - let i = String::from_utf8_lossy(&buf).to_string(); + let sz = br.read_to_end(&mut buf)?; + let i = String::from_utf8_lossy(&buf[0..sz]).to_string(); Ok(parse::as_recipe(&i)?) } + +pub fn read_menu_list

(path: P) -> Result, ParseError> +where + P: AsRef + Debug, +{ + let path = path.as_ref(); + let wd = path.parent().unwrap(); + let mut br = BufReader::new(try_open!(path)); + eprintln!("Switching to {:?}", wd); + std::env::set_current_dir(wd)?; + let mut buf = String::new(); + let mut recipe_list = Vec::new(); + loop { + let sz = br.read_line(&mut buf)?; + if sz == 0 { + break; + } + let recipe = parse_recipe(buf.trim())?; + buf.clear(); + recipe_list.push(recipe); + } + Ok(recipe_list) +} diff --git a/kitchen/src/main.rs b/kitchen/src/main.rs index d5a9b1d..b512fac 100644 --- a/kitchen/src/main.rs +++ b/kitchen/src/main.rs @@ -15,7 +15,7 @@ mod cli; use std::env; -use recipes::Recipe; +use recipes::{IngredientAccumulator, Recipe}; use clap; use clap::{clap_app, crate_authors, crate_version}; @@ -33,6 +33,10 @@ where (@arg ingredients: -i --ingredients "Output the ingredients list.") (@arg INPUT: +required "Input recipe file to parse") ) + (@subcommand groceries => + (about: "print out a grocery list for a set of recipes") + (@arg INPUT: +required "Input menu file to parse. One recipe file per line.") + ) ) .setting(clap::AppSettings::SubcommandRequiredElseHelp) } @@ -42,13 +46,24 @@ fn output_recipe_info(r: Recipe, print_ingredients: bool) { println!(""); if print_ingredients { println!("Ingredients:"); - for (_, ing) in r.get_ingredients() { - print!("\t* {}", ing.amt); - println!(" {}", ing.name); + for (_, i) in r.get_ingredients() { + print!("\t* {}", i.amt); + println!(" {}", i.name); } } } +fn output_ingredients_list(rs: Vec) { + let mut acc = IngredientAccumulator::new(); + for r in rs { + acc.accumulate_from(&r); + } + for (_, i) in acc.ingredients() { + print!("{}", i.amt); + println!(" {}", i.name); + } +} + fn main() { let matches = create_app().get_matches(); if let Some(matches) = matches.subcommand_matches("recipe") { @@ -62,5 +77,16 @@ fn main() { eprintln!("{:?}", e); } } + } else if let Some(matches) = matches.subcommand_matches("groceries") { + // The input argument is required so if we made it here then it's safe to unrwap this value. + let menu_file = matches.value_of("INPUT").unwrap(); + match cli::read_menu_list(menu_file) { + Ok(rs) => { + output_ingredients_list(rs); + } + Err(e) => { + eprintln!("{:?}", e); + } + } } } diff --git a/recipes/src/lib.rs b/recipes/src/lib.rs index fdc625c..ca64577 100644 --- a/recipes/src/lib.rs +++ b/recipes/src/lib.rs @@ -20,6 +20,7 @@ use chrono::NaiveDate; use uuid::{self, Uuid}; use unit::*; +use Measure::*; #[derive(Debug, Clone, PartialEq)] pub struct Mealplan { @@ -106,29 +107,44 @@ impl Recipe { /// Get entire ingredients list for each step of the recipe. With duplicate /// ingredients added together. pub fn get_ingredients(&self) -> BTreeMap { - use Measure::{Count, Volume, Weight}; - 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), - (Weight(lqty), Weight(rqty)) => Weight(lqty + rqty), - _ => unreachable!(), - }; - acc.get_mut(&key).map(|i| i.amt = amt); - } - return acc; - }) + let mut acc = IngredientAccumulator::new(); + acc.accumulate_from(&self); + acc.ingredients() } } +pub struct IngredientAccumulator { + inner: BTreeMap, +} + +impl IngredientAccumulator { + pub fn new() -> Self { + Self { + inner: BTreeMap::new(), + } + } + + pub fn accumulate_from(&mut self, r: &Recipe) { + for i in r.steps.iter().map(|s| s.ingredients.iter()).flatten() { + let key = i.key(); + if !self.inner.contains_key(&key) { + self.inner.insert(key, i.clone()); + } else { + let amt = match (self.inner[&key].amt, i.amt) { + (Volume(rvm), Volume(lvm)) => Volume(lvm + rvm), + (Count(lqty), Count(rqty)) => Count(lqty + rqty), + (Weight(lqty), Weight(rqty)) => Weight(lqty + rqty), + _ => unreachable!(), + }; + self.inner.get_mut(&key).map(|i| i.amt = amt); + } + } + } + + pub fn ingredients(self) -> BTreeMap { + self.inner + } +} /// A Recipe step. It has the time for the step if there is one, instructions, and an ingredients /// list. #[derive(Debug, Clone, PartialEq)] diff --git a/recipes/src/test.rs b/recipes/src/test.rs index 2fb2e4f..6b9e169 100644 --- a/recipes/src/test.rs +++ b/recipes/src/test.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. use crate::*; -use VolumeMeasure::*; +use crate::{VolumeMeasure::*, WeightMeasure::*}; use std::convert::Into; @@ -56,7 +56,7 @@ fn test_volume_math() { macro_rules! assert_normalize { ($typ:path, $conv:ident, $msg:expr) => { - if let $typ(qty) = dbg!($typ(1.into()).$conv().normalize()) { + if let $typ(qty) = $typ(1.into()).$conv().normalize() { assert_eq!(qty, 1.into()); } else { assert!(false, $msg); @@ -300,7 +300,7 @@ fn test_ingredient_parse() { ), ), ] { - match ingredient(StrIter::new(i)) { + match parse::ingredient(StrIter::new(i)) { ParseResult::Complete(_, ing) => assert_eq!(ing, expected), err => assert!(false, "{:?}", err), } @@ -332,7 +332,7 @@ fn test_ingredient_list_parse() { ], ), ] { - match ingredient_list(StrIter::new(i)) { + match parse::ingredient_list(StrIter::new(i)) { ParseResult::Complete(_, ing) => assert_eq!(ing, expected), err => assert!(false, "{:?}", err), } @@ -411,13 +411,21 @@ step: 1 tbsp flour 2 tbsp butter +Saute apples in butter until golden brown. Add flour slowly +until thickened. Set aside to cool. + +step: + +1 tbsp flour +2 tbsp butter + Saute apples in butter until golden brown. Add flour slowly until thickened. Set aside to cool. "; match parse::recipe(StrIter::new(recipe)) { ParseResult::Complete(_, recipe) => { - assert_eq!(recipe.steps.len(), 2); + assert_eq!(recipe.steps.len(), 3); assert_eq!(recipe.steps[0].ingredients.len(), 3); } err => assert!(false, "{:?}", err),