Better unit handling for weight measures

This commit is contained in:
Jeremy Wall 2021-11-17 19:06:48 -05:00
parent 8e0e66e8b8
commit 91c3ad603b
4 changed files with 141 additions and 25 deletions

16
examples/meatloaf.txt Normal file
View File

@ -0,0 +1,16 @@
title: Meatloaf
Good old fashioned meatloaf.
step:
1 lb ground beef
1 onion (chopped)
1 cup oatmeal
2 tbsp garlic powder
1 egg
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.

View File

@ -108,7 +108,7 @@ impl Recipe {
/// Get entire ingredients list for each step of the recipe. With duplicate /// Get entire ingredients list for each step of the recipe. With duplicate
/// ingredients added together. /// ingredients added together.
pub fn get_ingredients(&self) -> BTreeMap<IngredientKey, Ingredient> { pub fn get_ingredients(&self) -> BTreeMap<IngredientKey, Ingredient> {
use Measure::{Count, Gram, Volume}; use Measure::{Count, Volume, Weight};
self.steps self.steps
.iter() .iter()
.map(|s| s.ingredients.iter()) .map(|s| s.ingredients.iter())
@ -121,7 +121,7 @@ impl Recipe {
let amt = match (acc[&key].amt, i.amt) { let amt = match (acc[&key].amt, i.amt) {
(Volume(rvm), Volume(lvm)) => Volume(lvm + rvm), (Volume(rvm), Volume(lvm)) => Volume(lvm + rvm),
(Count(lqty), Count(rqty)) => Count(lqty + rqty), (Count(lqty), Count(rqty)) => Count(lqty + rqty),
(Gram(lqty), Gram(rqty)) => Gram(lqty + rqty), (Weight(lqty), Weight(rqty)) => Weight(lqty + rqty),
_ => unreachable!(), _ => unreachable!(),
}; };
acc.get_mut(&key).map(|i| i.amt = amt); acc.get_mut(&key).map(|i| i.amt = amt);

View File

@ -14,13 +14,13 @@
use std::str::FromStr; use std::str::FromStr;
use abortable_parser::{ use abortable_parser::{
ascii_digit, ascii_ws, consume_all, discard, do_each, either, eoi, make_fn, must, not, ascii_digit, consume_all, discard, do_each, either, eoi, make_fn, must, not, optional, peek,
optional, peek, repeat, separated, text_token, trap, until, Result, StrIter, repeat, separated, text_token, trap, until, Result, StrIter,
}; };
use num_rational::Ratio; use num_rational::Ratio;
use crate::{ use crate::{
unit::{Measure, Measure::*, Quantity, VolumeMeasure::*}, unit::{Measure, Measure::*, Quantity, VolumeMeasure::*, WeightMeasure::*},
Ingredient, Recipe, Step, Ingredient, Recipe, Step,
}; };
@ -222,11 +222,10 @@ pub fn measure(i: StrIter) -> abortable_parser::Result<StrIter, Measure> {
Some("qrt") | Some("quart") => Volume(Qrt(qty)), Some("qrt") | Some("quart") => Volume(Qrt(qty)),
Some("pint") | Some("pnt") => Volume(Pint(qty)), Some("pint") | Some("pnt") => Volume(Pint(qty)),
Some("cnt") | Some("count") => Count(qty), Some("cnt") | Some("count") => Count(qty),
Some("lb") => Measure::lb(qty), Some("lb") => Weight(Pound(qty)),
Some("oz") => Measure::oz(qty), Some("oz") => Weight(Oz(qty)),
Some("kg") => Measure::kilogram(qty), Some("kg") => Weight(Kilogram(qty)),
Some("g") => Gram(qty), Some("g") | Some("gram") => Weight(Gram(qty)),
Some("gram") => Gram(qty),
Some(u) => { Some(u) => {
return Result::Abort(abortable_parser::Error::new( return Result::Abort(abortable_parser::Error::new(
format!("Invalid Unit {}", u), format!("Invalid Unit {}", u),
@ -254,7 +253,7 @@ make_fn!(
discard!(text_token!("\n")), discard!(text_token!("\n")),
eoi, eoi,
discard!(text_token!("(")))), discard!(text_token!("(")))),
(name) (name.trim())
) )
); );

View File

@ -65,6 +65,11 @@ const QRT: Quantity = Quantity::Whole(960);
const LTR: Quantity = Quantity::Whole(1000); const LTR: Quantity = Quantity::Whole(1000);
const GAL: Quantity = Quantity::Whole(3840); const GAL: Quantity = Quantity::Whole(3840);
// multiplier constants for various units into grams
const LB: Quantity = Quantity::Frac(Ratio::new_raw(4535924, 10000));
const OZ: Quantity = Quantity::Frac(Ratio::new_raw(2834952, 100000));
const KG: Quantity = Quantity::Whole(1000);
const ONE: Quantity = Quantity::Whole(1); const ONE: Quantity = Quantity::Whole(1);
impl VolumeMeasure { impl VolumeMeasure {
@ -205,6 +210,102 @@ impl Display for VolumeMeasure {
} }
} }
#[derive(Copy, Clone, Debug)]
pub enum WeightMeasure {
Gram(Quantity),
Kilogram(Quantity),
Pound(Quantity),
Oz(Quantity),
}
impl WeightMeasure {
pub fn get_grams(&self) -> Quantity {
match self {
&Self::Gram(ref qty) => *qty,
&Self::Kilogram(ref qty) => *qty * KG,
&Self::Pound(ref qty) => *qty * LB,
&Self::Oz(ref qty) => *qty * OZ,
}
}
pub fn plural(&self) -> bool {
match self {
&Self::Gram(qty) | &Self::Kilogram(qty) | &Self::Pound(qty) | &Self::Oz(qty) => {
qty.plural()
}
}
}
pub fn into_gram(self) -> Self {
Self::Gram(self.get_grams())
}
pub fn into_kilo(self) -> Self {
Self::Kilogram(self.get_grams() / KG)
}
pub fn into_pound(self) -> Self {
Self::Pound(self.get_grams() / LB)
}
pub fn into_oz(self) -> Self {
Self::Oz(self.get_grams() / OZ)
}
pub fn normalize(self) -> Self {
let grams = self.get_grams();
if (grams / KG) >= ONE {
return self.into_kilo();
}
if (grams / LB) >= ONE {
return self.into_pound();
}
if (grams / OZ) >= ONE {
return self.into_oz();
}
return self.into_gram();
}
}
macro_rules! weight_op {
($trait:ident, $method:ident) => {
impl $trait for WeightMeasure {
type Output = Self;
fn $method(self, lhs: Self) -> Self::Output {
let (l, r) = (self.get_grams(), lhs.get_grams());
WeightMeasure::Gram($trait::$method(l, r))
}
}
};
}
weight_op!(Add, add);
weight_op!(Sub, sub);
impl PartialEq for WeightMeasure {
fn eq(&self, lhs: &Self) -> bool {
let rhs = self.get_grams();
let lhs = lhs.get_grams();
PartialEq::eq(&rhs, &lhs)
}
}
impl Display for WeightMeasure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
&Self::Gram(qty) => write!(f, "{} gram{}", qty, if qty.plural() { "s" } else { "" }),
&Self::Kilogram(qty) => {
write!(f, "{} kilogram{}", qty, if qty.plural() { "s" } else { "" })
}
&Self::Pound(qty) => write!(f, "{} lb{}", qty, if qty.plural() { "s" } else { "" }),
&Self::Oz(qty) => write!(f, "{} oz", qty),
}
}
}
use WeightMeasure::{Gram, Kilogram, Oz, Pound};
#[derive(Copy, Clone, Debug, PartialEq)] #[derive(Copy, Clone, Debug, PartialEq)]
/// Measurements in a Recipe with associated units for them. /// Measurements in a Recipe with associated units for them.
pub enum Measure { pub enum Measure {
@ -213,10 +314,10 @@ pub enum Measure {
/// Simple count of items /// Simple count of items
Count(Quantity), Count(Quantity),
/// Weight measure as Grams base unit /// Weight measure as Grams base unit
Gram(Quantity), Weight(WeightMeasure),
} }
use Measure::{Count, Gram, Volume}; use Measure::{Count, Volume, Weight};
impl Measure { impl Measure {
pub fn tsp(qty: Quantity) -> Self { pub fn tsp(qty: Quantity) -> Self {
@ -260,29 +361,28 @@ impl Measure {
} }
pub fn gram(qty: Quantity) -> Self { pub fn gram(qty: Quantity) -> Self {
Gram(qty) Weight(Gram(qty))
} }
pub fn kilogram(qty: Quantity) -> Self { pub fn kilogram(qty: Quantity) -> Self {
Gram(qty * Quantity::Whole(1000)) Weight(Kilogram(qty))
} }
// TODO(jwall): Should these be separate units with conversions?
pub fn lb(qty: Quantity) -> Self { pub fn lb(qty: Quantity) -> Self {
// This is an approximation obviously // This is an approximation
Gram(qty * (Quantity::Whole(453) + Quantity::Frac(Ratio::new(6, 10)))) Weight(Pound(qty))
} }
pub fn oz(qty: Quantity) -> Self { pub fn oz(qty: Quantity) -> Self {
// This is an approximation obviously // This is an approximation
Gram(qty * (Quantity::Whole(28) + Quantity::Frac(Ratio::new(4, 10)))) Weight(Oz(qty))
} }
pub fn measure_type(&self) -> String { pub fn measure_type(&self) -> String {
match self { match self {
Volume(_) => "Volume", Volume(_) => "Volume",
Count(_) => "Count", Count(_) => "Count",
Gram(_) => "Weight", Weight(_) => "Weight",
} }
.to_owned() .to_owned()
} }
@ -290,12 +390,12 @@ impl Measure {
pub fn plural(&self) -> bool { pub fn plural(&self) -> bool {
match self { match self {
Volume(vm) => vm.plural(), Volume(vm) => vm.plural(),
Count(qty) | Gram(qty) => qty.plural(), Count(qty) => qty.plural(),
Weight(wm) => wm.plural(),
} }
} }
// TODO(jwall): parse from string. // TODO(jwall): Remove this it's unnecessary
pub fn parse(input: &str) -> std::result::Result<Self, String> { pub fn parse(input: &str) -> std::result::Result<Self, String> {
Ok(match measure(StrIter::new(input)) { Ok(match measure(StrIter::new(input)) {
Result::Complete(_, measure) => measure, Result::Complete(_, measure) => measure,
@ -312,7 +412,8 @@ impl Display for Measure {
match self { match self {
Volume(vm) => write!(w, "{}", vm), Volume(vm) => write!(w, "{}", vm),
Count(qty) => write!(w, "{}", qty), Count(qty) => write!(w, "{}", qty),
Gram(qty) => write!(w, "{} gram{}", qty, if qty.plural() { "s" } else { "" }), // TODO(jwall): Should I allow auto convert upwards for the grams to kgs?
Weight(wm) => write!(w, "{}", wm),
} }
} }
} }