feat: fallback nutrient estimation for potassium/calcium/magnesium
OFF data is sparse for fresh produce — spinach, oats, most frozen fruit have no potassium. Added a curated fallback table (114 foods, USDA-sourced) that injects estimated K/Ca/Mg at log time when OFF has no data. How it works: - data/fallback_nutrients.json: 114 foods with mg/100g values - FallbackTable: case-insensitive exact + substring name matching - log_food: injects _est keys (potassium_100g_est etc.) when OFF missing - sum_nutriments: tries OFF value first (grams×1000), then _est (already mg) Results for today's smoothie + lunch: - Potassium: 691mg → 2,530mg (47% of 5400mg goal) - Calcium: 14mg → 304mg (30% of 1000mg goal) - Magnesium: 61mg → 330mg (73% of 450mg goal) 38 tests passing (7 new: fallback lookup, est fallback, OFF vs est priority)
This commit is contained in:
parent
a072c6ba5e
commit
d0b7b003f9
4 changed files with 281 additions and 5 deletions
116
data/fallback_nutrients.json
Normal file
116
data/fallback_nutrients.json
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
{
|
||||||
|
"spinach": { "potassium_100g": 558, "calcium_100g": 99, "magnesium_100g": 79 },
|
||||||
|
"banana": { "potassium_100g": 358, "calcium_100g": 5, "magnesium_100g": 27 },
|
||||||
|
"oats": { "potassium_100g": 429, "calcium_100g": 54, "magnesium_100g": 177 },
|
||||||
|
"oat milk": { "potassium_100g": 180, "calcium_100g": 120, "magnesium_100g": 20 },
|
||||||
|
"strawberry": { "potassium_100g": 153, "calcium_100g": 16, "magnesium_100g": 13 },
|
||||||
|
"strawberries": { "potassium_100g": 153, "calcium_100g": 16, "magnesium_100g": 13 },
|
||||||
|
"blueberry": { "potassium_100g": 77, "calcium_100g": 6, "magnesium_100g": 6 },
|
||||||
|
"blueberries": { "potassium_100g": 77, "calcium_100g": 6, "magnesium_100g": 6 },
|
||||||
|
"pineapple": { "potassium_100g": 109, "calcium_100g": 13, "magnesium_100g": 12 },
|
||||||
|
"cherry": { "potassium_100g": 222, "calcium_100g": 13, "magnesium_100g": 11 },
|
||||||
|
"cherries": { "potassium_100g": 222, "calcium_100g": 13, "magnesium_100g": 11 },
|
||||||
|
"egg": { "potassium_100g": 138, "calcium_100g": 56, "magnesium_100g": 12 },
|
||||||
|
"eggs": { "potassium_100g": 138, "calcium_100g": 56, "magnesium_100g": 12 },
|
||||||
|
"cheddar": { "potassium_100g": 76, "calcium_100g": 721, "magnesium_100g": 28 },
|
||||||
|
"flatbread": { "potassium_100g": 120, "calcium_100g": 100, "magnesium_100g": 30 },
|
||||||
|
"chicken": { "potassium_100g": 256, "calcium_100g": 11, "magnesium_100g": 28 },
|
||||||
|
"chicken breast": { "potassium_100g": 256, "calcium_100g": 11, "magnesium_100g": 28 },
|
||||||
|
"salmon": { "potassium_100g": 363, "calcium_100g": 9, "magnesium_100g": 30 },
|
||||||
|
"avocado": { "potassium_100g": 485, "calcium_100g": 12, "magnesium_100g": 29 },
|
||||||
|
"potato": { "potassium_100g": 421, "calcium_100g": 12, "magnesium_100g": 23 },
|
||||||
|
"sweet potato": { "potassium_100g": 337, "calcium_100g": 30, "magnesium_100g": 25 },
|
||||||
|
"broccoli": { "potassium_100g": 316, "calcium_100g": 47, "magnesium_100g": 21 },
|
||||||
|
"yogurt": { "potassium_100g": 155, "calcium_100g": 121, "magnesium_100g": 12 },
|
||||||
|
"greek yogurt": { "potassium_100g": 141, "calcium_100g": 115, "magnesium_100g": 11 },
|
||||||
|
"rice": { "potassium_100g": 86, "calcium_100g": 10, "magnesium_100g": 12 },
|
||||||
|
"brown rice": { "potassium_100g": 86, "calcium_100g": 10, "magnesium_100g": 12 },
|
||||||
|
"tomato": { "potassium_100g": 237, "calcium_100g": 10, "magnesium_100g": 11 },
|
||||||
|
"tomatoes": { "potassium_100g": 237, "calcium_100g": 10, "magnesium_100g": 11 },
|
||||||
|
"mushroom": { "potassium_100g": 318, "calcium_100g": 3, "magnesium_100g": 9 },
|
||||||
|
"mushrooms": { "potassium_100g": 318, "calcium_100g": 3, "magnesium_100g": 9 },
|
||||||
|
"onion": { "potassium_100g": 146, "calcium_100g": 23, "magnesium_100g": 10 },
|
||||||
|
"garlic": { "potassium_100g": 401, "calcium_100g": 181, "magnesium_100g": 25 },
|
||||||
|
"carrot": { "potassium_100g": 320, "calcium_100g": 33, "magnesium_100g": 12 },
|
||||||
|
"carrots": { "potassium_100g": 320, "calcium_100g": 33, "magnesium_100g": 12 },
|
||||||
|
"cucumber": { "potassium_100g": 147, "calcium_100g": 16, "magnesium_100g": 13 },
|
||||||
|
"pepper": { "potassium_100g": 211, "calcium_100g": 10, "magnesium_100g": 10 },
|
||||||
|
"bell pepper": { "potassium_100g": 211, "calcium_100g": 10, "magnesium_100g": 10 },
|
||||||
|
"courgette": { "potassium_100g": 261, "calcium_100g": 16, "magnesium_100g": 18 },
|
||||||
|
"zucchini": { "potassium_100g": 261, "calcium_100g": 16, "magnesium_100g": 18 },
|
||||||
|
"kale": { "potassium_100g": 491, "calcium_100g": 150, "magnesium_100g": 47 },
|
||||||
|
"lettuce": { "potassium_100g": 194, "calcium_100g": 36, "magnesium_100g": 13 },
|
||||||
|
"cabbage": { "potassium_100g": 170, "calcium_100g": 40, "magnesium_100g": 12 },
|
||||||
|
"cauliflower": { "potassium_100g": 299, "calcium_100g": 22, "magnesium_100g": 15 },
|
||||||
|
"peas": { "potassium_100g": 244, "calcium_100g": 25, "magnesium_100g": 33 },
|
||||||
|
"green beans": { "potassium_100g": 209, "calcium_100g": 37, "magnesium_100g": 25 },
|
||||||
|
"green peas": { "potassium_100g": 244, "calcium_100g": 25, "magnesium_100g": 33 },
|
||||||
|
"beans": { "potassium_100g": 405, "calcium_100g": 143, "magnesium_100g": 140 },
|
||||||
|
"kidney beans": { "potassium_100g": 405, "calcium_100g": 143, "magnesium_100g": 140 },
|
||||||
|
"chickpeas": { "potassium_100g": 291, "calcium_100g": 49, "magnesium_100g": 48 },
|
||||||
|
"lentils": { "potassium_100g": 369, "calcium_100g": 35, "magnesium_100g": 47 },
|
||||||
|
"almond": { "potassium_100g": 733, "calcium_100g": 269, "magnesium_100g": 270 },
|
||||||
|
"almonds": { "potassium_100g": 733, "calcium_100g": 269, "magnesium_100g": 270 },
|
||||||
|
"walnut": { "potassium_100g": 441, "calcium_100g": 98, "magnesium_100g": 158 },
|
||||||
|
"walnuts": { "potassium_100g": 441, "calcium_100g": 98, "magnesium_100g": 158 },
|
||||||
|
"cashew": { "potassium_100g": 660, "calcium_100g": 37, "magnesium_100g": 292 },
|
||||||
|
"cashews": { "potassium_100g": 660, "calcium_100g": 37, "magnesium_100g": 292 },
|
||||||
|
"peanut": { "potassium_100g": 705, "calcium_100g": 92, "magnesium_100g": 168 },
|
||||||
|
"peanuts": { "potassium_100g": 705, "calcium_100g": 92, "magnesium_100g": 168 },
|
||||||
|
"peanut butter": { "potassium_100g": 649, "calcium_100g": 49, "magnesium_100g": 154 },
|
||||||
|
"beef": { "potassium_100g": 271, "calcium_100g": 6, "magnesium_100g": 19 },
|
||||||
|
"pork": { "potassium_100g": 287, "calcium_100g": 5, "magnesium_100g": 22 },
|
||||||
|
"lamb": { "potassium_100g": 247, "calcium_100g": 6, "magnesium_100g": 22 },
|
||||||
|
"turkey": { "potassium_100g": 239, "calcium_100g": 7, "magnesium_100g": 26 },
|
||||||
|
"tuna": { "potassium_100g": 252, "calcium_100g": 4, "magnesium_100g": 27 },
|
||||||
|
"cod": { "potassium_100g": 228, "calcium_100g": 6, "magnesium_100g": 24 },
|
||||||
|
"prawn": { "potassium_100g": 253, "calcium_100g": 70, "magnesium_100g": 22 },
|
||||||
|
"shrimp": { "potassium_100g": 253, "calcium_100g": 70, "magnesium_100g": 22 },
|
||||||
|
"milk": { "potassium_100g": 150, "calcium_100g": 120, "magnesium_100g": 11 },
|
||||||
|
"whole milk": { "potassium_100g": 150, "calcium_100g": 120, "magnesium_100g": 11 },
|
||||||
|
"cheese": { "potassium_100g": 76, "calcium_100g": 721, "magnesium_100g": 28 },
|
||||||
|
"mozzarella": { "potassium_100g": 75, "calcium_100g": 505, "magnesium_100g": 20 },
|
||||||
|
"feta": { "potassium_100g": 62, "calcium_100g": 493, "magnesium_100g": 10 },
|
||||||
|
"bread": { "potassium_100g": 115, "calcium_100g": 114, "magnesium_100g": 25 },
|
||||||
|
"whole wheat bread": { "potassium_100g": 220, "calcium_100g": 107, "magnesium_100g": 76 },
|
||||||
|
"pasta": { "potassium_100g": 44, "calcium_100g": 7, "magnesium_100g": 18 },
|
||||||
|
"noodle": { "potassium_100g": 44, "calcium_100g": 7, "magnesium_100g": 18 },
|
||||||
|
"quinoa": { "potassium_100g": 172, "calcium_100g": 47, "magnesium_100g": 197 },
|
||||||
|
"buckwheat": { "potassium_100g": 460, "calcium_100g": 18, "magnesium_100g": 231 },
|
||||||
|
"orange": { "potassium_100g": 181, "calcium_100g": 40, "magnesium_100g": 10 },
|
||||||
|
"apple": { "potassium_100g": 107, "calcium_100g": 6, "magnesium_100g": 5 },
|
||||||
|
"pear": { "potassium_100g": 116, "calcium_100g": 9, "magnesium_100g": 7 },
|
||||||
|
"grape": { "potassium_100g": 191, "calcium_100g": 10, "magnesium_100g": 7 },
|
||||||
|
"grapes": { "potassium_100g": 191, "calcium_100g": 10, "magnesium_100g": 7 },
|
||||||
|
"mango": { "potassium_100g": 168, "calcium_100g": 11, "magnesium_100g": 10 },
|
||||||
|
"kiwi": { "potassium_100g": 312, "calcium_100g": 34, "magnesium_100g": 17 },
|
||||||
|
"watermelon": { "potassium_100g": 112, "calcium_100g": 7, "magnesium_100g": 10 },
|
||||||
|
"raspberry": { "potassium_100g": 151, "calcium_100g": 20, "magnesium_100g": 22 },
|
||||||
|
"raspberries": { "potassium_100g": 151, "calcium_100g": 20, "magnesium_100g": 22 },
|
||||||
|
"blackberry": { "potassium_100g": 162, "calcium_100g": 29, "magnesium_100g": 20 },
|
||||||
|
"blackberries": { "potassium_100g": 162, "calcium_100g": 29, "magnesium_100g": 20 },
|
||||||
|
"cranberry": { "potassium_100g": 85, "calcium_100g": 8, "magnesium_100g": 6 },
|
||||||
|
"pomegranate": { "potassium_100g": 236, "calcium_100g": 10, "magnesium_100g": 12 },
|
||||||
|
"fig": { "potassium_100g": 232, "calcium_100g": 35, "magnesium_100g": 17 },
|
||||||
|
"date": { "potassium_100g": 656, "calcium_100g": 64, "magnesium_100g": 54 },
|
||||||
|
"dates": { "potassium_100g": 656, "calcium_100g": 64, "magnesium_100g": 54 },
|
||||||
|
"raisin": { "potassium_100g": 749, "calcium_100g": 50, "magnesium_100g": 32 },
|
||||||
|
"raisins": { "potassium_100g": 749, "calcium_100g": 50, "magnesium_100g": 32 },
|
||||||
|
"apricot": { "potassium_100g": 259, "calcium_100g": 13, "magnesium_100g": 10 },
|
||||||
|
"peach": { "potassium_100g": 190, "calcium_100g": 6, "magnesium_100g": 9 },
|
||||||
|
"plum": { "potassium_100g": 157, "calcium_100g": 6, "magnesium_100g": 7 },
|
||||||
|
"lemon": { "potassium_100g": 138, "calcium_100g": 26, "magnesium_100g": 8 },
|
||||||
|
"lime": { "potassium_100g": 102, "calcium_100g": 33, "magnesium_100g": 6 },
|
||||||
|
"coconut": { "potassium_100g": 356, "calcium_100g": 14, "magnesium_100g": 32 },
|
||||||
|
"coconut water": { "potassium_100g": 250, "calcium_100g": 24, "magnesium_100g": 25 },
|
||||||
|
"dark chocolate": { "potassium_100g": 559, "calcium_100g": 73, "magnesium_100g": 146 },
|
||||||
|
"cocoa": { "potassium_100g": 1524, "calcium_100g": 128, "magnesium_100g": 499 },
|
||||||
|
"tofu": { "potassium_100g": 121, "calcium_100g": 350, "magnesium_100g": 30 },
|
||||||
|
"tempeh": { "potassium_100g": 412, "calcium_100g": 111, "magnesium_100g": 81 },
|
||||||
|
"hummus": { "potassium_100g": 228, "calcium_100g": 38, "magnesium_100g": 36 },
|
||||||
|
"falafel": { "potassium_100g": 385, "calcium_100g": 54, "magnesium_100g": 46 },
|
||||||
|
"olive oil": { "potassium_100g": 1, "calcium_100g": 1, "magnesium_100g": 0 },
|
||||||
|
"butter": { "potassium_100g": 24, "calcium_100g": 24, "magnesium_100g": 2 },
|
||||||
|
"honey": { "potassium_100g": 52, "calcium_100g": 6, "magnesium_100g": 2 },
|
||||||
|
"maple syrup": { "potassium_100g": 204, "calcium_100g": 102, "magnesium_100g": 12 }
|
||||||
|
}
|
||||||
57
src/api.rs
57
src/api.rs
|
|
@ -1,16 +1,67 @@
|
||||||
// OpenFood Facts API client: Search-a-licious + v3 barcode + SQLite caching.
|
// OpenFood Facts API client: Search-a-licious + v3 barcode + SQLite caching.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::db::DbPool;
|
use crate::db::DbPool;
|
||||||
|
|
||||||
|
/// Fallback nutrient estimates (mg per 100g) for fresh produce where OFF data is sparse.
|
||||||
|
/// Bundled at compile time from data/fallback_nutrients.json.
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct FallbackTable {
|
||||||
|
foods: HashMap<String, FallbackNutrients>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct FallbackNutrients {
|
||||||
|
#[serde(default)]
|
||||||
|
pub potassium_100g: Option<f64>, // mg
|
||||||
|
#[serde(default)]
|
||||||
|
pub calcium_100g: Option<f64>, // mg
|
||||||
|
#[serde(default)]
|
||||||
|
pub magnesium_100g: Option<f64>, // mg
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FallbackTable {
|
||||||
|
/// Load from the bundled JSON file at compile time.
|
||||||
|
pub fn load() -> Self {
|
||||||
|
let json = include_str!("../data/fallback_nutrients.json");
|
||||||
|
let foods: HashMap<String, FallbackNutrients> =
|
||||||
|
serde_json::from_str(json).unwrap_or_default();
|
||||||
|
Self { foods }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to find fallback nutrients for a product name.
|
||||||
|
/// Does case-insensitive exact match first, then substring match.
|
||||||
|
pub fn lookup(&self, product_name: &str) -> Option<&FallbackNutrients> {
|
||||||
|
let name_lower = product_name.to_lowercase();
|
||||||
|
// Exact match first
|
||||||
|
if let Some(n) = self.foods.get(&name_lower) {
|
||||||
|
return Some(n);
|
||||||
|
}
|
||||||
|
// Substring match (e.g. "Leaf spinach" matches "spinach")
|
||||||
|
// Try longer keys first to prefer more specific matches
|
||||||
|
let mut best: Option<(&String, &FallbackNutrients)> = None;
|
||||||
|
for (key, nutrients) in &self.foods {
|
||||||
|
if name_lower.contains(key.as_str()) {
|
||||||
|
if best.is_none_or(|(k, _)| k.len() < key.len()) {
|
||||||
|
best = Some((key, nutrients));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
best.map(|(_, n)| n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// OFF API client for food lookups.
|
/// OFF API client for food lookups.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct OffClient {
|
pub struct OffClient {
|
||||||
http: reqwest::Client,
|
http: reqwest::Client,
|
||||||
user_agent: String,
|
user_agent: String,
|
||||||
|
fallback: FallbackTable,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A cached product from OFF (stored in SQLite products table).
|
/// A cached product from OFF (stored in SQLite products table).
|
||||||
|
|
@ -81,9 +132,15 @@ impl OffClient {
|
||||||
Self {
|
Self {
|
||||||
http,
|
http,
|
||||||
user_agent: user_agent.to_string(),
|
user_agent: user_agent.to_string(),
|
||||||
|
fallback: FallbackTable::load(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Access the fallback nutrient table (for use at log time).
|
||||||
|
pub fn fallback(&self) -> &FallbackTable {
|
||||||
|
&self.fallback
|
||||||
|
}
|
||||||
|
|
||||||
/// Search products by name using Search-a-licious.
|
/// Search products by name using Search-a-licious.
|
||||||
/// Returns up to `page_size` results.
|
/// Returns up to `page_size` results.
|
||||||
/// Note: Search-a-licious doesn't return nutriments in search hits,
|
/// Note: Search-a-licious doesn't return nutriments in search hits,
|
||||||
|
|
|
||||||
48
src/tools.rs
48
src/tools.rs
|
|
@ -229,11 +229,25 @@ pub fn sum_nutriments(entries_nutriments: &[Value]) -> Value {
|
||||||
let mut sum = 0.0;
|
let mut sum = 0.0;
|
||||||
let mut has_data = false;
|
let mut has_data = false;
|
||||||
let is_mg = MG_NUTRIENTS.contains(tracked_key);
|
let is_mg = MG_NUTRIENTS.contains(tracked_key);
|
||||||
|
let est_key = format!("{}_est", tracked_key);
|
||||||
|
|
||||||
for entry_nutriments in entries_nutriments {
|
for entry_nutriments in entries_nutriments {
|
||||||
|
// Try main key first (OFF value, in grams for mg nutrients)
|
||||||
if let Some(val) = entry_nutriments.get(tracked_key).and_then(|v| v.as_f64()) {
|
if let Some(val) = entry_nutriments.get(tracked_key).and_then(|v| v.as_f64()) {
|
||||||
// OFF stores potassium/calcium/magnesium in grams; convert to mg
|
if val > 0.0 {
|
||||||
sum += if is_mg { val * 1000.0 } else { val };
|
sum += if is_mg { val * 1000.0 } else { val };
|
||||||
has_data = true;
|
has_data = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Try fallback estimate (already in mg, no conversion needed)
|
||||||
|
if is_mg {
|
||||||
|
if let Some(val) = entry_nutriments.get(&est_key).and_then(|v| v.as_f64()) {
|
||||||
|
if val > 0.0 {
|
||||||
|
sum += val;
|
||||||
|
has_data = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
totals.insert(
|
totals.insert(
|
||||||
|
|
@ -416,8 +430,36 @@ impl NutritionServer {
|
||||||
100.0 // default to 100g
|
100.0 // default to 100g
|
||||||
};
|
};
|
||||||
|
|
||||||
// Scale nutriments
|
// Scale nutriments — inject fallback estimates for missing K/Ca/Mg first
|
||||||
let nutriments: Value = serde_json::from_str(&product.nutriments).unwrap_or(json!({}));
|
let mut nutriments: Value = serde_json::from_str(&product.nutriments).unwrap_or(json!({}));
|
||||||
|
|
||||||
|
// Check if potassium/calcium/magnesium are missing or zero from OFF
|
||||||
|
let needs_k = nutriments.get("potassium_100g").and_then(|v| v.as_f64()).map_or(true, |v| v <= 0.0);
|
||||||
|
let needs_ca = nutriments.get("calcium_100g").and_then(|v| v.as_f64()).map_or(true, |v| v <= 0.0);
|
||||||
|
let needs_mg = nutriments.get("magnesium_100g").and_then(|v| v.as_f64()).map_or(true, |v| v <= 0.0);
|
||||||
|
|
||||||
|
if needs_k || needs_ca || needs_mg {
|
||||||
|
if let Some(fb) = self.off_client.fallback().lookup(&product.product_name) {
|
||||||
|
if let Some(obj) = nutriments.as_object_mut() {
|
||||||
|
if needs_k {
|
||||||
|
if let Some(k) = fb.potassium_100g {
|
||||||
|
obj.insert("potassium_100g_est".to_string(), json!(k));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if needs_ca {
|
||||||
|
if let Some(ca) = fb.calcium_100g {
|
||||||
|
obj.insert("calcium_100g_est".to_string(), json!(ca));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if needs_mg {
|
||||||
|
if let Some(mg) = fb.magnesium_100g {
|
||||||
|
obj.insert("magnesium_100g_est".to_string(), json!(mg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let scaled = scale_nutriments(&nutriments, grams);
|
let scaled = scale_nutriments(&nutriments, grams);
|
||||||
|
|
||||||
// Insert entry
|
// Insert entry
|
||||||
|
|
|
||||||
|
|
@ -249,6 +249,67 @@ mod tests {
|
||||||
assert_eq!(totals["energy_kcal"], json!(200.0));
|
assert_eq!(totals["energy_kcal"], json!(200.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sum_nutriments_uses_est_fallback() {
|
||||||
|
// Entry has only _est potassium (from fallback table, already in mg)
|
||||||
|
let entries = vec![json!({"potassium_100g_est": 558.0})];
|
||||||
|
let totals = tools::sum_nutriments(&entries);
|
||||||
|
assert_eq!(totals["potassium"], json!(558.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sum_nutriments_prefers_off_over_est() {
|
||||||
|
// Entry has both OFF value (0.32g = 320mg) and _est (558mg)
|
||||||
|
// OFF value should win
|
||||||
|
let entries = vec![json!({"potassium_100g": 0.32, "potassium_100g_est": 558.0})];
|
||||||
|
let totals = tools::sum_nutriments(&entries);
|
||||||
|
assert_eq!(totals["potassium"], json!(320.0)); // 0.32g × 1000 = 320mg
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sum_nutriments_mixed_off_and_est() {
|
||||||
|
// Entry 1 has OFF potassium (0.32g = 320mg)
|
||||||
|
// Entry 2 has only _est potassium (558mg from fallback)
|
||||||
|
let entries = vec![
|
||||||
|
json!({"potassium_100g": 0.32}),
|
||||||
|
json!({"potassium_100g_est": 558.0}),
|
||||||
|
];
|
||||||
|
let totals = tools::sum_nutriments(&entries);
|
||||||
|
assert_eq!(totals["potassium"], json!(878.0)); // 320 + 558
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fallback_lookup_exact() {
|
||||||
|
let table = nutrition_mcp::api::FallbackTable::load();
|
||||||
|
let result = table.lookup("spinach");
|
||||||
|
assert!(result.is_some());
|
||||||
|
assert_eq!(result.unwrap().potassium_100g, Some(558.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fallback_lookup_substring() {
|
||||||
|
let table = nutrition_mcp::api::FallbackTable::load();
|
||||||
|
// "Leaf spinach" should match "spinach" via substring
|
||||||
|
let result = table.lookup("Leaf spinach");
|
||||||
|
assert!(result.is_some());
|
||||||
|
assert_eq!(result.unwrap().potassium_100g, Some(558.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fallback_lookup_no_match() {
|
||||||
|
let table = nutrition_mcp::api::FallbackTable::load();
|
||||||
|
let result = table.lookup("alien space dust");
|
||||||
|
assert!(result.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_fallback_lookup_case_insensitive() {
|
||||||
|
let table = nutrition_mcp::api::FallbackTable::load();
|
||||||
|
let result = table.lookup("BANANA");
|
||||||
|
assert!(result.is_some());
|
||||||
|
assert_eq!(result.unwrap().potassium_100g, Some(358.0));
|
||||||
|
}
|
||||||
|
|
||||||
// ── Date/meal tests ──────────────────────────────────────────
|
// ── Date/meal tests ──────────────────────────────────────────
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue