use approx::AbsDiffEq;
use serde::{Deserialize, Serialize};
use struct_iterable::Iterable;
use strum_macros::EnumIter;
use crate::{
composition::{Alcohol, Micro, PAC, Solids},
error::Result,
validate::assert_are_positive,
};
#[cfg(feature = "wasm")]
use wasm_bindgen::prelude::*;
#[cfg(doc)]
use crate::{
composition::{ArtificialSweeteners, Carbohydrates, Polyols, Sugars},
error::Error,
specs::ChocolateSpec,
};
pub trait IntoComposition {
#[allow(clippy::missing_errors_doc)] fn into_composition(self) -> Result<Composition>;
}
pub trait ScaleComponents {
#[must_use]
fn scale(&self, factor: f64) -> Self;
#[must_use]
fn add(&self, other: &Self) -> Self;
}
#[cfg_attr(feature = "wasm", wasm_bindgen)]
#[derive(Iterable, PartialEq, Serialize, Deserialize, Copy, Clone, Debug)]
pub struct Composition {
pub energy: f64,
pub solids: Solids,
pub micro: Micro,
pub alcohol: Alcohol,
pub pod: f64,
pub pac: PAC,
}
#[cfg_attr(feature = "wasm", wasm_bindgen)]
#[derive(EnumIter, Hash, PartialEq, Eq, Serialize, Deserialize, Copy, Clone, Debug)]
pub enum CompKey {
Energy,
MilkFat,
MSNF,
MilkSNFS,
MilkProteins,
MilkSolids,
#[doc = include_str!("../../docs/bibs/20.md")]
CocoaButter,
CocoaSolids,
CacaoSolids,
NutFat,
NutSNF,
NutSolids,
EggFat,
EggSNF,
EggSolids,
OtherFats,
OtherSNFS,
TotalFats,
TotalSNF,
TotalSNFS,
TotalProteins,
TotalSolids,
Water,
Fiber,
Glucose,
Fructose,
Galactose,
Sucrose,
Lactose,
Maltose,
Trehalose,
TotalSugars,
Erythritol,
TotalPolyols,
TotalCarbohydrates,
TotalArtificial,
TotalSweeteners,
Alcohol,
ABV,
Salt,
Lecithin,
Emulsifiers,
Stabilizers,
EmulsifiersPerFat,
StabilizersPerWater,
POD,
PACsgr,
PACslt,
PACmlk,
PACalc,
PACtotal,
AbsPAC,
HF,
}
impl Composition {
#[must_use]
pub fn empty() -> Self {
Self {
energy: 0.0,
solids: Solids::empty(),
micro: Micro::empty(),
alcohol: Alcohol::empty(),
pod: 0.0,
pac: PAC::empty(),
}
}
#[must_use]
pub fn energy(self, energy: f64) -> Self {
Self { energy, ..self }
}
#[must_use]
pub fn solids(self, solids: Solids) -> Self {
Self { solids, ..self }
}
#[must_use]
pub fn micro(self, micro: Micro) -> Self {
Self { micro, ..self }
}
#[must_use]
pub fn alcohol(self, alcohol: Alcohol) -> Self {
Self { alcohol, ..self }
}
#[must_use]
pub fn pod(self, pod: f64) -> Self {
Self { pod, ..self }
}
#[must_use]
pub fn pac(self, pac: PAC) -> Self {
Self { pac, ..self }
}
pub fn from_combination(compositions: &[(Composition, f64)]) -> Result<Self> {
assert_are_positive(&compositions.iter().map(|line| line.1).collect::<Vec<_>>())?;
let total_amount: f64 = compositions.iter().map(|line| line.1).sum();
if total_amount == 0.0 {
return Ok(Composition::empty());
}
compositions.iter().try_fold(Composition::empty(), |acc, line| {
let mut mix_comp = acc;
let weight = line.1 / total_amount;
mix_comp = mix_comp.add(&line.0.scale(weight));
Ok(mix_comp)
})
}
}
#[cfg_attr(feature = "wasm", wasm_bindgen)]
impl Composition {
#[cfg_attr(feature = "wasm", wasm_bindgen(constructor))]
#[must_use]
pub fn new() -> Self {
Self::empty()
}
#[must_use]
pub fn water(&self) -> f64 {
100.0 - self.solids.total() - self.alcohol.by_weight
}
#[must_use]
pub fn emulsifiers_per_fat(&self) -> f64 {
if self.solids.all().fats.total > 0.0 {
(self.micro.emulsifiers / self.solids.all().fats.total) * 100.0
} else {
f64::NAN
}
}
#[must_use]
pub fn stabilizers_per_water(&self) -> f64 {
if self.water() > 0.0 {
(self.micro.stabilizers / self.water()) * 100.0
} else {
f64::NAN
}
}
#[must_use]
pub fn absolute_pac(&self) -> f64 {
if self.water() > 0.0 {
(self.pac.total() / self.water()) * 100.0
} else {
f64::NAN
}
}
#[must_use]
pub fn get(&self, key: CompKey) -> f64 {
match key {
CompKey::Energy => self.energy,
CompKey::MilkFat => self.solids.milk.fats.total,
CompKey::MSNF => self.solids.milk.snf(),
CompKey::MilkSNFS => self.solids.milk.snfs(),
CompKey::MilkProteins => self.solids.milk.proteins,
CompKey::MilkSolids => self.solids.milk.total(),
CompKey::CocoaButter => self.solids.cocoa.fats.total,
CompKey::CocoaSolids => self.solids.cocoa.snfs(),
CompKey::CacaoSolids => self.solids.cocoa.total() - self.solids.cocoa.carbohydrates.sugars.total(),
CompKey::NutFat => self.solids.nut.fats.total,
CompKey::NutSNF => self.solids.nut.snfs(),
CompKey::NutSolids => self.solids.nut.total() - self.solids.nut.carbohydrates.sugars.total(),
CompKey::EggFat => self.solids.egg.fats.total,
CompKey::EggSNF => self.solids.egg.snfs(),
CompKey::EggSolids => self.solids.egg.total() - self.solids.egg.carbohydrates.sugars.total(),
CompKey::OtherFats => self.solids.other.fats.total,
CompKey::OtherSNFS => self.solids.other.snfs(),
CompKey::TotalFats => self.solids.all().fats.total,
CompKey::TotalSNF => self.solids.all().snf(),
CompKey::TotalSNFS => self.solids.all().snfs(),
CompKey::TotalProteins => self.solids.all().proteins,
CompKey::TotalSolids => self.solids.total(),
CompKey::Water => self.water(),
CompKey::Fiber => self.solids.all().carbohydrates.fiber.total(),
CompKey::Glucose => self.solids.all().carbohydrates.sugars.glucose,
CompKey::Fructose => self.solids.all().carbohydrates.sugars.fructose,
CompKey::Galactose => self.solids.all().carbohydrates.sugars.galactose,
CompKey::Sucrose => self.solids.all().carbohydrates.sugars.sucrose,
CompKey::Lactose => self.solids.all().carbohydrates.sugars.lactose,
CompKey::Maltose => self.solids.all().carbohydrates.sugars.maltose,
CompKey::Trehalose => self.solids.all().carbohydrates.sugars.trehalose,
CompKey::TotalSugars => self.solids.all().carbohydrates.sugars.total(),
CompKey::Erythritol => self.solids.all().carbohydrates.polyols.erythritol,
CompKey::TotalPolyols => self.solids.all().carbohydrates.polyols.total(),
CompKey::TotalCarbohydrates => self.solids.all().carbohydrates.total(),
CompKey::TotalArtificial => self.solids.all().artificial_sweeteners.total(),
CompKey::TotalSweeteners => {
self.solids.all().carbohydrates.sugars.total()
+ self.solids.all().carbohydrates.polyols.total()
+ self.solids.all().artificial_sweeteners.total()
}
CompKey::Alcohol => self.alcohol.by_weight,
CompKey::ABV => self.alcohol.to_abv(),
CompKey::Salt => self.micro.salt,
CompKey::Lecithin => self.micro.lecithin,
CompKey::Emulsifiers => self.micro.emulsifiers,
CompKey::Stabilizers => self.micro.stabilizers,
CompKey::EmulsifiersPerFat => self.emulsifiers_per_fat(),
CompKey::StabilizersPerWater => self.stabilizers_per_water(),
CompKey::POD => self.pod,
CompKey::PACsgr => self.pac.sugars,
CompKey::PACslt => self.pac.salt,
CompKey::PACmlk => self.pac.msnf_ws_salts,
CompKey::PACalc => self.pac.alcohol,
CompKey::PACtotal => self.pac.total(),
CompKey::AbsPAC => self.absolute_pac(),
CompKey::HF => self.pac.hardness_factor,
}
}
}
impl ScaleComponents for Composition {
fn scale(&self, factor: f64) -> Self {
Self {
energy: self.energy * factor,
solids: self.solids.scale(factor),
micro: self.micro.scale(factor),
alcohol: self.alcohol.scale(factor),
pod: self.pod * factor,
pac: self.pac.scale(factor),
}
}
fn add(&self, other: &Self) -> Self {
Self {
energy: self.energy + other.energy,
solids: self.solids.add(&other.solids),
micro: self.micro.add(&other.micro),
alcohol: self.alcohol.add(&other.alcohol),
pod: self.pod + other.pod,
pac: self.pac.add(&other.pac),
}
}
}
impl AbsDiffEq for Composition {
type Epsilon = f64;
fn default_epsilon() -> Self::Epsilon {
f64::default_epsilon()
}
fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool {
self.energy.abs_diff_eq(&other.energy, epsilon)
&& self.solids.abs_diff_eq(&other.solids, epsilon)
&& self.micro.abs_diff_eq(&other.micro, epsilon)
&& self.alcohol.abs_diff_eq(&other.alcohol, epsilon)
&& self.pod.abs_diff_eq(&other.pod, epsilon)
&& self.pac.abs_diff_eq(&other.pac, epsilon)
}
}
impl Default for Composition {
fn default() -> Self {
Self::empty()
}
}
#[cfg(test)]
#[cfg_attr(coverage, coverage(off))]
#[allow(clippy::float_cmp)]
mod tests {
use std::collections::HashMap;
use strum::IntoEnumIterator;
use crate::tests::asserts::shadow_asserts::assert_eq;
use crate::tests::asserts::*;
use crate::tests::assets::*;
use super::*;
use crate::composition::*;
#[test]
fn composition_nan_values() {
let comp = Composition::new();
assert_eq!(comp.get(CompKey::Water), 100.0);
assert_eq!(comp.get(CompKey::TotalSolids), 0.0);
assert_eq!(comp.get(CompKey::TotalFats), 0.0);
assert!(comp.get(CompKey::EmulsifiersPerFat).is_nan());
assert_eq!(comp.get(CompKey::StabilizersPerWater), 0.0);
assert_eq!(comp.get(CompKey::AbsPAC), 0.0);
let comp = Composition::new().solids(Solids::new().other(SolidsBreakdown::new().others(100.0)));
assert_eq!(comp.get(CompKey::Water), 0.0);
assert_eq!(comp.get(CompKey::TotalSolids), 100.0);
assert!(comp.get(CompKey::EmulsifiersPerFat).is_nan());
assert!(comp.get(CompKey::StabilizersPerWater).is_nan());
assert!(comp.get(CompKey::AbsPAC).is_nan());
}
#[test]
fn composition_get() {
let expected = HashMap::from([
(CompKey::Energy, 49.5756),
(CompKey::MilkFat, 2.0),
(CompKey::MSNF, 8.82),
(CompKey::MilkSNFS, 4.0131),
(CompKey::MilkProteins, 3.087),
(CompKey::MilkSolids, 10.82),
(CompKey::TotalFats, 2.0),
(CompKey::TotalSNF, 8.82),
(CompKey::TotalSNFS, 4.0131),
(CompKey::TotalProteins, 3.087),
(CompKey::TotalSolids, 10.82),
(CompKey::Water, 89.18),
(CompKey::Lactose, 4.8069),
(CompKey::TotalSugars, 4.8069),
(CompKey::TotalArtificial, 0.0),
(CompKey::TotalSweeteners, 4.8069),
(CompKey::TotalCarbohydrates, 4.8069),
(CompKey::POD, 0.769_104),
(CompKey::PACsgr, 4.8069),
(CompKey::PACmlk, 3.2405),
(CompKey::PACtotal, 8.0474),
(CompKey::AbsPAC, 9.02377),
]);
CompKey::iter().for_each(|key| assert_eq_flt_test!(COMP_2_MILK.get(key), *expected.get(&key).unwrap_or(&0.0)));
}
}