kcal/
unit.rs

1use std::{fmt::Display, str::FromStr};
2
3const GRAMS_PER_POUND: f64 = 453.592;
4const OUNCES_PER_POUND: f64 = 16.0;
5
6const GRAMS_PER_OUNCE: f64 = GRAMS_PER_POUND / OUNCES_PER_POUND;
7const OUNCES_PER_GRAM: f64 = OUNCES_PER_POUND / GRAMS_PER_POUND;
8
9#[derive(Debug)]
10pub struct BadUnit;
11
12#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
13pub enum Unit {
14    #[default]
15    Gram,
16    Ounce,
17    Pound,
18}
19
20impl Unit {
21    #[must_use]
22    pub fn dual(self) -> Unit {
23        match self {
24            Unit::Gram => Unit::Ounce,
25            Unit::Ounce | Unit::Pound => Unit::Gram,
26        }
27    }
28
29    /// TODO: Set this per item, so you can define `Unit::Each`.  For example,
30    /// the number of grams per egg is not necessarily the same as the number of
31    /// grams per banana.
32    #[must_use]
33    pub fn per(self, unit: Unit) -> f64 {
34        match (self, unit) {
35            (Unit::Gram, Unit::Gram) | (Unit::Ounce, Unit::Ounce) | (Unit::Pound, Unit::Pound) => {
36                1.0
37            }
38            (Unit::Gram, Unit::Ounce) => GRAMS_PER_OUNCE,
39            (Unit::Gram, Unit::Pound) => GRAMS_PER_POUND,
40            (Unit::Ounce, Unit::Gram) => OUNCES_PER_GRAM,
41            (Unit::Ounce, Unit::Pound) => OUNCES_PER_POUND,
42            (Unit::Pound, Unit::Gram) => 1.0 / GRAMS_PER_POUND,
43            (Unit::Pound, Unit::Ounce) => 1.0 / OUNCES_PER_POUND,
44        }
45    }
46}
47
48impl Display for Unit {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        let s = match self {
51            Unit::Gram => "g",
52            Unit::Ounce => "oz",
53            Unit::Pound => "lb",
54        };
55        write!(f, "{s}")
56    }
57}
58
59impl FromStr for Unit {
60    type Err = BadUnit;
61
62    fn from_str(s: &str) -> Result<Self, Self::Err> {
63        match s {
64            "" => Ok(Unit::default()),
65            "g" => Ok(Unit::Gram),
66            "lb" | "lbs" | "#" => Ok(Unit::Pound),
67            "oz" => Ok(Unit::Ounce),
68            _ => Err(BadUnit),
69        }
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn test_unit_parsing() {
79        assert_eq!("g".parse::<Unit>().unwrap(), Unit::Gram);
80        assert_eq!("oz".parse::<Unit>().unwrap(), Unit::Ounce);
81        assert_eq!("lb".parse::<Unit>().unwrap(), Unit::Pound);
82        assert_eq!("lbs".parse::<Unit>().unwrap(), Unit::Pound);
83        assert!("invalid".parse::<Unit>().is_err());
84    }
85
86    #[test]
87    fn test_unit_display() {
88        assert_eq!(Unit::Gram.to_string(), "g");
89        assert_eq!(Unit::Ounce.to_string(), "oz");
90        assert_eq!(Unit::Pound.to_string(), "lb");
91    }
92
93    #[test]
94    fn test_unit_conversion() {
95        // 1 oz should be ~28.35g
96        let grams_per_oz = Unit::Gram.per(Unit::Ounce);
97        assert!((grams_per_oz - 28.35).abs() < 0.1);
98
99        // 1 lb should be ~453.6g
100        let grams_per_lb = Unit::Gram.per(Unit::Pound);
101        assert!((grams_per_lb - 453.6).abs() < 0.1);
102
103        // Identity conversions
104        assert_eq!(Unit::Gram.per(Unit::Gram), 1.0);
105    }
106}