kcal/
portion.rs

1use std::{fmt, str::FromStr};
2
3use crate::unit::Unit;
4
5#[derive(Debug)]
6pub enum BadPortion {
7    BadAmount(String),
8    BadUnit(String),
9    MissingUnit,
10}
11
12impl fmt::Display for BadPortion {
13    fn fmt(&self, f: &mut fmt::Formatter) -> std::fmt::Result {
14        match self {
15            BadPortion::BadAmount(value) => write!(f, "{value}: bad amount"),
16            BadPortion::BadUnit(value) => write!(f, "{value}: bad unit"),
17            BadPortion::MissingUnit => write!(f, "missing unit"),
18        }
19    }
20}
21
22#[derive(Clone, Copy)]
23pub struct Portion {
24    pub number: f64,
25    pub unit: Unit,
26}
27
28impl Portion {
29    #[must_use]
30    pub fn convert(&self) -> Portion {
31        self.convert_to(self.unit.dual())
32    }
33
34    #[must_use]
35    pub fn convert_to(&self, unit: Unit) -> Portion {
36        Portion {
37            number: self.number * unit.per(self.unit),
38            unit,
39        }
40    }
41}
42
43impl fmt::Display for Portion {
44    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        match self.unit {
46            // There's no point in showing fractions of a gram.
47            Unit::Gram => write!(f, "{}", self.number.round())?,
48            _ => write!(f, "{:.2}", self.number)?,
49        }
50        write!(f, "{}", self.unit)
51    }
52}
53
54impl FromStr for Portion {
55    type Err = BadPortion;
56    fn from_str(s: &str) -> Result<Self, Self::Err> {
57        let unit_begin = s
58            .bytes()
59            .position(|c| c != b'.' && !c.is_ascii_digit())
60            .unwrap_or(s.len());
61        let (number, unit) = s.split_at(unit_begin);
62        let number = number
63            .parse()
64            .map_err(|_| BadPortion::BadAmount(number.to_string()))?;
65        let unit = unit
66            .parse()
67            .map_err(|_| BadPortion::BadUnit(unit.to_string()))?;
68        Ok(Portion { number, unit })
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    #[test]
77    fn test_portion_parsing() {
78        let p: Portion = "100g".parse().unwrap();
79        assert_eq!(p.number, 100.0);
80        assert_eq!(p.unit, Unit::Gram);
81
82        let p: Portion = "8oz".parse().unwrap();
83        assert_eq!(p.number, 8.0);
84        assert_eq!(p.unit, Unit::Ounce);
85
86        let p: Portion = "1.5lb".parse().unwrap();
87        assert_eq!(p.number, 1.5);
88        assert_eq!(p.unit, Unit::Pound);
89    }
90
91    #[test]
92    fn test_portion_conversion() {
93        let p: Portion = "1oz".parse().unwrap();
94        let grams = p.convert_to(Unit::Gram);
95        assert!((grams.number - 28.35).abs() < 0.1);
96        assert_eq!(grams.unit, Unit::Gram);
97    }
98
99    #[test]
100    fn test_portion_display() {
101        let p = Portion { number: 100.0, unit: Unit::Gram };
102        assert_eq!(p.to_string(), "100g");
103    }
104}