Menu lists can output the ingredients list for shopping.

This commit is contained in:
Jeremy Wall 2021-11-20 12:17:04 -05:00
parent a7923fbcf3
commit 6651bf2996
7 changed files with 168 additions and 36 deletions

View File

@ -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.

View File

@ -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.
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.

2
examples/menu.txt Normal file
View File

@ -0,0 +1,2 @@
meatloaf.txt
cornbread_dressing.txt

View File

@ -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<std::io::Error> 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<String> for ParseError {
pub fn parse_recipe<P>(path: P) -> Result<Recipe, ParseError>
where
P: AsRef<Path>,
P: AsRef<Path> + 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<P>(path: P) -> Result<Vec<Recipe>, ParseError>
where
P: AsRef<Path> + 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)
}

View File

@ -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<Recipe>) {
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);
}
}
}
}

View File

@ -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<IngredientKey, Ingredient> {
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<IngredientKey, Ingredient>,
}
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<IngredientKey, Ingredient> {
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)]

View File

@ -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),