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
/// ingredients added together.
pub fn get_ingredients(&self) -> BTreeMap<IngredientKey, Ingredient> {
use Measure::{Count, Gram, Volume};
use Measure::{Count, Volume, Weight};
self.steps
.iter()
.map(|s| s.ingredients.iter())
@ -121,7 +121,7 @@ impl Recipe {
let amt = match (acc[&key].amt, i.amt) {
(Volume(rvm), Volume(lvm)) => Volume(lvm + rvm),
(Count(lqty), Count(rqty)) => Count(lqty + rqty),
(Gram(lqty), Gram(rqty)) => Gram(lqty + rqty),
(Weight(lqty), Weight(rqty)) => Weight(lqty + rqty),
_ => unreachable!(),
};
acc.get_mut(&key).map(|i| i.amt = amt);

View File

@ -14,13 +14,13 @@
use std::str::FromStr;
use abortable_parser::{
ascii_digit, ascii_ws, consume_all, discard, do_each, either, eoi, make_fn, must, not,
optional, peek, repeat, separated, text_token, trap, until, Result, StrIter,
ascii_digit, consume_all, discard, do_each, either, eoi, make_fn, must, not, optional, peek,
repeat, separated, text_token, trap, until, Result, StrIter,
};
use num_rational::Ratio;
use crate::{
unit::{Measure, Measure::*, Quantity, VolumeMeasure::*},
unit::{Measure, Measure::*, Quantity, VolumeMeasure::*, WeightMeasure::*},
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("pint") | Some("pnt") => Volume(Pint(qty)),
Some("cnt") | Some("count") => Count(qty),
Some("lb") => Measure::lb(qty),
Some("oz") => Measure::oz(qty),
Some("kg") => Measure::kilogram(qty),
Some("g") => Gram(qty),
Some("gram") => Gram(qty),
Some("lb") => Weight(Pound(qty)),
Some("oz") => Weight(Oz(qty)),
Some("kg") => Weight(Kilogram(qty)),
Some("g") | Some("gram") => Weight(Gram(qty)),
Some(u) => {
return Result::Abort(abortable_parser::Error::new(
format!("Invalid Unit {}", u),
@ -254,7 +253,7 @@ make_fn!(
discard!(text_token!("\n")),
eoi,
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 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);
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)]
/// Measurements in a Recipe with associated units for them.
pub enum Measure {
@ -213,10 +314,10 @@ pub enum Measure {
/// Simple count of items
Count(Quantity),
/// Weight measure as Grams base unit
Gram(Quantity),
Weight(WeightMeasure),
}
use Measure::{Count, Gram, Volume};
use Measure::{Count, Volume, Weight};
impl Measure {
pub fn tsp(qty: Quantity) -> Self {
@ -260,29 +361,28 @@ impl Measure {
}
pub fn gram(qty: Quantity) -> Self {
Gram(qty)
Weight(Gram(qty))
}
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 {
// This is an approximation obviously
Gram(qty * (Quantity::Whole(453) + Quantity::Frac(Ratio::new(6, 10))))
// This is an approximation
Weight(Pound(qty))
}
pub fn oz(qty: Quantity) -> Self {
// This is an approximation obviously
Gram(qty * (Quantity::Whole(28) + Quantity::Frac(Ratio::new(4, 10))))
// This is an approximation
Weight(Oz(qty))
}
pub fn measure_type(&self) -> String {
match self {
Volume(_) => "Volume",
Count(_) => "Count",
Gram(_) => "Weight",
Weight(_) => "Weight",
}
.to_owned()
}
@ -290,12 +390,12 @@ impl Measure {
pub fn plural(&self) -> bool {
match self {
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> {
Ok(match measure(StrIter::new(input)) {
Result::Complete(_, measure) => measure,
@ -312,7 +412,8 @@ impl Display for Measure {
match self {
Volume(vm) => write!(w, "{}", vm),
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),
}
}
}