mirror of
https://github.com/zaphar/kitchen.git
synced 2025-07-22 19:40:14 -04:00
Menu lists can output the ingredients list for shopping.
This commit is contained in:
parent
a7923fbcf3
commit
6651bf2996
41
examples/cornbread_dressing.txt
Normal file
41
examples/cornbread_dressing.txt
Normal 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.
|
@ -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
2
examples/menu.txt
Normal file
@ -0,0 +1,2 @@
|
||||
meatloaf.txt
|
||||
cornbread_dressing.txt
|
@ -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)
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 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 !acc.contains_key(&key) {
|
||||
acc.insert(key, i.clone());
|
||||
if !self.inner.contains_key(&key) {
|
||||
self.inner.insert(key, i.clone());
|
||||
} else {
|
||||
let amt = match (acc[&key].amt, i.amt) {
|
||||
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!(),
|
||||
};
|
||||
acc.get_mut(&key).map(|i| i.amt = amt);
|
||||
self.inner.get_mut(&key).map(|i| i.amt = amt);
|
||||
}
|
||||
return acc;
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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)]
|
||||
|
@ -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),
|
||||
|
Loading…
x
Reference in New Issue
Block a user