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!(plum,            60,  0.8, 130), // each
72    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
92/// Parses foods in the format C,P/Z.
93/// * C is the number of kilocalories per serving
94/// * P is the number of grams of protein per serving
95/// * Z is the serving size
96fn 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
110/// TODO: Support multiple plurals per singular.
111fn 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}