use serde::{Deserialize, Serialize};
use crate::{
composition::{Carbohydrates, Composition, Fats, Fibers, IntoComposition, PAC, Solids, SolidsBreakdown, Sugars},
error::Result,
validate::{assert_are_positive, assert_is_subset, assert_within_100_percent},
};
#[allow(clippy::doc_markdown)] #[doc = include_str!("../../docs/bibs/101.md")]
#[derive(PartialEq, Serialize, Deserialize, Copy, Clone, Debug)]
#[serde(deny_unknown_fields)]
pub struct FruitSpec {
pub water: f64,
pub energy: Option<f64>,
pub protein: Option<f64>,
pub fat: Option<f64>,
pub carbohydrate: Option<f64>,
pub fiber: Option<f64>,
pub sugars: Sugars,
}
impl IntoComposition for FruitSpec {
fn into_composition(self) -> Result<Composition> {
let Self {
water,
energy,
protein,
fat,
carbohydrate,
fiber,
sugars,
} = self;
let protein = protein.unwrap_or(0.0);
let fat = fat.unwrap_or(0.0);
let fiber = fiber.unwrap_or(0.0);
let carbohydrate = carbohydrate.unwrap_or(fiber + sugars.total());
assert_is_subset(fiber + sugars.total(), carbohydrate, "fiber + sugars <= carbohydrate")?;
assert_are_positive(&[water, protein, fat, carbohydrate, fiber, sugars.total()])?;
assert_within_100_percent(water + protein + fat + carbohydrate)?;
let solids = SolidsBreakdown::new()
.fats(Fats::new().total(fat))
.carbohydrates(
Carbohydrates::new()
.sugars(sugars)
.fiber(Fibers::new().other(fiber))
.others_from_total(carbohydrate)?,
)
.proteins(protein)
.others_from_total(100.0 - water)?;
let energy = energy.unwrap_or(solids.energy()?);
assert_are_positive(&[energy])?;
Ok(Composition::new()
.energy(energy)
.solids(Solids::new().other(solids))
.pod(solids.carbohydrates.to_pod()?)
.pac(PAC::new().sugars(solids.carbohydrates.to_pac()?)))
}
}
#[cfg(test)]
#[cfg_attr(coverage, coverage(off))]
#[allow(clippy::unwrap_used, clippy::float_cmp)]
pub(crate) mod tests {
use std::sync::LazyLock;
use crate::tests::asserts::shadow_asserts::assert_eq;
use crate::tests::asserts::*;
use super::*;
use crate::{composition::CompKey, ingredient::Category, specs::IngredientSpec};
pub(crate) const ING_SPEC_FRUIT_STRAWBERRY_STR: &str = r#"{
"name": "Strawberry",
"category": "Fruit",
"FruitSpec": {
"water": 91,
"energy": 32,
"protein": 0.67,
"fat": 0.3,
"carbohydrate": 7.68,
"fiber": 2,
"sugars": {
"glucose": 1.99,
"fructose": 2.44,
"sucrose": 0.47
}
}
}"#;
pub(crate) static ING_SPEC_FRUIT_STRAWBERRY: LazyLock<IngredientSpec> = LazyLock::new(|| IngredientSpec {
name: "Strawberry".to_string(),
category: Category::Fruit,
spec: FruitSpec {
water: 91.0,
energy: Some(32.0),
protein: Some(0.67),
fat: Some(0.3),
carbohydrate: Some(7.68),
fiber: Some(2.0),
sugars: Sugars::new().glucose(1.99).fructose(2.44).sucrose(0.47),
}
.into(),
});
#[test]
#[expect(clippy::approx_constant)]
fn into_composition_fruit_spec_strawberry() {
let comp = ING_SPEC_FRUIT_STRAWBERRY.spec.into_composition().unwrap();
assert_eq_flt_test!(comp.get(CompKey::Energy), 32.0);
assert_eq!(comp.get(CompKey::TotalFats), 0.3);
assert_eq!(comp.get(CompKey::TotalProteins), 0.67);
assert_eq!(comp.get(CompKey::Fiber), 2.0);
assert_eq!(comp.get(CompKey::Glucose), 1.99);
assert_eq!(comp.get(CompKey::Fructose), 2.44);
assert_eq!(comp.get(CompKey::Sucrose), 0.47);
assert_eq_flt_test!(comp.get(CompKey::TotalSugars), 4.9);
assert_eq!(comp.get(CompKey::TotalCarbohydrates), 7.68);
assert_eq_flt_test!(comp.get(CompKey::TotalSweeteners), 4.90);
assert_eq_flt_test!(comp.get(CompKey::TotalSNFS), 3.8);
assert_eq_flt_test!(comp.get(CompKey::TotalSolids), 9.0);
assert_eq_flt_test!(comp.get(CompKey::POD), 6.2832);
assert_eq!(comp.get(CompKey::PACsgr), 8.887);
}
pub(crate) const ING_SPEC_FRUIT_NAVEL_ORANGE_AUTO_ENERGY_STR: &str = r#"{
"name": "Navel Orange",
"category": "Fruit",
"FruitSpec": {
"water": 86.7,
"protein": 0.91,
"fat": 0.15,
"fiber": 2,
"sugars": {
"glucose": 2.02,
"fructose": 2.36,
"sucrose": 4.19
}
}
}"#;
pub(crate) static ING_SPEC_FRUIT_NAVEL_ORANGE_AUTO_ENERGY: LazyLock<IngredientSpec> =
LazyLock::new(|| IngredientSpec {
name: "Navel Orange".to_string(),
category: Category::Fruit,
spec: FruitSpec {
water: 86.7,
energy: None,
protein: Some(0.91),
fat: Some(0.15),
carbohydrate: None,
fiber: Some(2.0),
sugars: Sugars::new().glucose(2.02).fructose(2.36).sucrose(4.19),
}
.into(),
});
#[test]
fn into_composition_fruit_spec_navel_orange_auto_energy() {
let comp = ING_SPEC_FRUIT_NAVEL_ORANGE_AUTO_ENERGY.spec.into_composition().unwrap();
assert_eq_flt_test!(comp.get(CompKey::Energy), 39.27);
assert_eq!(comp.get(CompKey::TotalFats), 0.15);
assert_eq!(comp.get(CompKey::TotalProteins), 0.91);
assert_eq!(comp.get(CompKey::Fiber), 2.0);
assert_eq!(comp.get(CompKey::Glucose), 2.02);
assert_eq!(comp.get(CompKey::Fructose), 2.36);
assert_eq!(comp.get(CompKey::Sucrose), 4.19);
assert_eq_flt_test!(comp.get(CompKey::TotalSugars), 8.57);
assert_eq!(comp.get(CompKey::TotalCarbohydrates), 10.57);
assert_eq_flt_test!(comp.get(CompKey::TotalSweeteners), 8.57);
assert_eq_flt_test!(comp.get(CompKey::TotalSNFS), 4.58);
assert_eq_flt_test!(comp.get(CompKey::TotalSolids), 13.3);
assert_eq_flt_test!(comp.get(CompKey::POD), 9.8888);
assert_eq!(comp.get(CompKey::PACsgr), 12.512);
}
pub(crate) static INGREDIENT_ASSETS_TABLE_FRUIT: LazyLock<Vec<(&str, IngredientSpec, Option<Composition>)>> =
LazyLock::new(|| {
vec![
(ING_SPEC_FRUIT_STRAWBERRY_STR, ING_SPEC_FRUIT_STRAWBERRY.clone(), None),
(ING_SPEC_FRUIT_NAVEL_ORANGE_AUTO_ENERGY_STR, ING_SPEC_FRUIT_NAVEL_ORANGE_AUTO_ENERGY.clone(), None),
]
});
}