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 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}