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!(salmon, 121, 17.0, 85),
78 food!(spinach, 23, 2.9, 100),
79 food!(strawberry, 32, 0.7, 100),
80 food!(sugar, 385, 0.0, 100),
81 food!(tuna, 282, 39.0, 198),
82 food!(tofu, 94, 10.0, 124),
83 food!(thigh, 149, 18.6, 100),
84 food!(tomato, 22, 0.7, 100),
85 food!(turkey, 64, 7.7, 57),
86 food!(veg, 20, 1.5, 57),
87 food!(whiskey, 250, 0.0, 100),
88];
89
90fn parse_custom(s: &str) -> Option<Food> {
95 let (cp, z) = s.split_once('/')?;
96 let (c, p) = cp.split_once(',')?;
97
98 let kcal: f64 = c.parse().ok()?;
99 let protein: f64 = p.parse().ok()?;
100 let hundreds: f64 = z.parse::<Portion>().ok()?.convert_to(Unit::Gram).number / 100.0;
101
102 Some(Food {
103 kcal: kcal / hundreds,
104 protein: protein / hundreds,
105 })
106}
107
108fn pluralize(s: &str) -> String {
110 if let Some(base) = s.strip_suffix('y') {
111 base.to_owned() + "ies"
112 } else if s.ends_with('o') {
113 s.to_owned() + "es"
114 } else {
115 s.to_owned() + "s"
116 }
117}
118
119impl FromStr for Food {
120 type Err = BadFood;
121 fn from_str(s: &str) -> Result<Self, Self::Err> {
122 if let Some(food) = FOODS
123 .iter()
124 .find_map(|(slug, food)| (*slug == s).then_some(food))
125 {
126 Ok(food.clone())
127 } else if let Some(food) = FOODS
128 .iter()
129 .find_map(|(slug, food)| (pluralize(slug) == s).then_some(food))
130 {
131 Ok(food.clone())
132 } else if let Some(food) = parse_custom(s) {
133 Ok(food)
134 } else {
135 Err(BadFood(s.to_string()))
136 }
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 #[test]
145 fn pluralize_works() {
146 for (singular, want) in [
147 ("blueberry", "blueberries"),
148 ("tomato", "tomatoes"),
149 ("fig", "figs"),
150 ] {
151 assert_eq!(pluralize(singular), want);
152 }
153 }
154}