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