Skip to main content

kcal/
food.rs

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    /// kilocalories per 100g of this food
17    pub kcal: f64,
18    /// grams of protein per 100g of this food
19    pub protein: f64,
20}
21
22/// # TODO
23///
24/// * Load data dynamically, rather than using a macro.
25/// * Support non-identifier names, such as "matcha-cake".
26/// * Support singular and plural (e.g., egg and eggs) in a single row.
27macro_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), // each
46    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), // each
58    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), // each
68    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
91/// Parses foods in the format C,P/Z.
92/// * C is the number of kilocalories per serving
93/// * P is the number of grams of protein per serving
94/// * Z is the serving size
95fn 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
109/// TODO: Support multiple plurals per singular.
110fn 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}