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