1use std::{fmt, str::FromStr};
2
3use crate::{Portion, Unit};
4
5#[derive(Debug)]
6pub struct BadFood(String);
7
8impl fmt::Display for BadFood {
9 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
10 write!(f, "{}: bad food", self.0)
11 }
12}
13
14#[derive(Clone)]
15pub struct Food {
16 pub kcal: f64,
18 pub protein: f64,
20}
21
22macro_rules! food {
28 ($name: ident, $kcal: expr_2021, $protein: expr_2021, $per: expr_2021) => {
29 (
30 stringify!($name),
31 Food {
32 kcal: $kcal as f64 * 100.0 / $per as f64,
33 protein: $protein as f64 * 100.0 / $per as f64,
34 },
35 )
36 };
37}
38
39#[rustfmt::skip]
40const FOODS: &[(&str, Food)] = &[
41 food!(almond, 164, 6.0, 28),
42 food!(apple, 59, 0.3, 100),
43 food!(asparagus, 20, 2.2, 100),
44 food!(avocado, 80, 1.0, 50),
45 food!(bacon, 80, 6.0, 16), food!(banana, 89, 1.1, 100),
47 food!(blueberry, 39, 0.5, 68),
48 food!(broccoli, 34, 2.8, 100),
49 food!(brussels, 43, 3.4, 100),
50 food!(butter, 717, 0.9, 100),
51 food!(cheese, 110, 7.0, 28),
52 food!(cabbage, 25, 1.3, 100),
53 food!(carrot, 41, 0.8, 100),
54 food!(cauliflower, 25, 1.9, 100),
55 food!(celery, 14, 0.7, 100),
56 food!(chicken, 60, 11.0, 56),
57 food!(coconutroll, 100, 0.0, 20), food!(cucumber, 15, 0.6, 100),
59 food!(eggwhite, 25, 5.0, 46),
60 food!(endive, 17, 1.3, 100),
61 food!(enoki, 44, 2.4, 100),
62 food!(grape, 34, 0.4, 49),
63 food!(greenbean, 44, 2.4, 125),
64 food!(ham, 61, 9.1, 57),
65 food!(honey, 60, 0.0, 21),
66 food!(lettuce, 17, 1.0, 100),
67 food!(matchacake, 167, 1.8, 36), food!(mushroom, 22, 3.1, 100),
69 food!(oil, 884, 0.0, 100),
70 food!(onion, 41, 1.3, 100),
71 food!(shallot, 72, 2.5, 100),
72 food!(peanut, 567, 25.8, 100),
73 food!(peanutpowder, 50, 5.0, 12),
74 food!(pepper, 20, 0.9, 100),
75 food!(popcorn, 130, 4.0, 40),
76 food!(potato, 79, 2.1, 100),
77 food!(spinach, 23, 2.9, 100),
78 food!(strawberry, 32, 0.7, 100),
79 food!(sugar, 385, 0.0, 100),
80 food!(tofu, 94, 10.0, 124),
81 food!(thigh, 149, 18.6, 100),
82 food!(tomato, 22, 0.7, 100),
83 food!(turkey, 64, 7.7, 57),
84 food!(veg, 20, 1.5, 57),
85 food!(whiskey, 250, 0.0, 100),
86];
87
88fn parse_custom(s: &str) -> Option<Food> {
93 let (cp, z) = s.split_once('/')?;
94 let (c, p) = cp.split_once(',')?;
95
96 let kcal: f64 = c.parse().ok()?;
97 let protein: f64 = p.parse().ok()?;
98 let hundreds: f64 = z.parse::<Portion>().ok()?.convert_to(Unit::Gram).number / 100.0;
99
100 Some(Food {
101 kcal: kcal / hundreds,
102 protein: protein / hundreds,
103 })
104}
105
106fn pluralize(s: &str) -> String {
108 if let Some(base) = s.strip_suffix('y') {
109 base.to_owned() + "ies"
110 } else if s.ends_with('o') {
111 s.to_owned() + "es"
112 } else {
113 s.to_owned() + "s"
114 }
115}
116
117impl FromStr for Food {
118 type Err = BadFood;
119 fn from_str(s: &str) -> Result<Self, Self::Err> {
120 if let Some(food) = FOODS
121 .iter()
122 .find_map(|(slug, food)| (*slug == s).then_some(food))
123 {
124 Ok(food.clone())
125 } else if let Some(food) = FOODS
126 .iter()
127 .find_map(|(slug, food)| (pluralize(slug) == s).then_some(food))
128 {
129 Ok(food.clone())
130 } else if let Some(food) = parse_custom(s) {
131 Ok(food)
132 } else {
133 Err(BadFood(s.to_string()))
134 }
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn pluralize_works() {
144 for (singular, want) in [
145 ("blueberry", "blueberries"),
146 ("tomato", "tomatoes"),
147 ("fig", "figs"),
148 ] {
149 assert_eq!(pluralize(singular), want);
150 }
151 }
152}