dpp_rules/textiles/
fibre.rs1use alloc::{format, string::String, vec::Vec};
4
5use crate::common::country::country_code_valid;
6
7pub const FIBRE_SUM_TOLERANCE: f64 = 2.0;
12
13#[derive(Debug, Clone, Copy)]
15pub struct FibreInput<'a> {
16 pub fibre: &'a str,
17 pub pct: f64,
18 pub country_of_origin: Option<&'a str>,
19}
20
21#[must_use]
26pub fn fibre_sum_ok(pcts: &[f64]) -> bool {
27 let total: f64 = pcts.iter().copied().sum();
28 let delta = total - 100.0;
29 (-FIBRE_SUM_TOLERANCE..=FIBRE_SUM_TOLERANCE).contains(&delta)
30}
31
32pub fn validate_fibre_composition(fibres: &[FibreInput<'_>]) -> Result<(), String> {
36 if fibres.is_empty() {
37 return Err(String::from("fibre_composition must not be empty"));
38 }
39 for f in fibres {
40 if !f.pct.is_finite() || f.pct < 0.0 || f.pct > 100.0 {
41 return Err(format!(
42 "fibre '{}' has invalid pct {} — must be a finite value in 0–100",
43 f.fibre, f.pct
44 ));
45 }
46 if let Some(co) = f.country_of_origin
47 && !country_code_valid(co)
48 {
49 return Err(format!(
50 "fibre '{}' has invalid country_of_origin '{}' — must be ISO 3166-1 alpha-2",
51 f.fibre, co
52 ));
53 }
54 }
55 let pcts: Vec<f64> = fibres.iter().map(|f| f.pct).collect();
56 if !fibre_sum_ok(&pcts) {
57 let total: f64 = pcts.iter().copied().sum();
58 return Err(format!(
59 "fibreComposition percentages sum to {total:.1}, expected 100.0 (± {FIBRE_SUM_TOLERANCE:.1})"
60 ));
61 }
62 Ok(())
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68
69 fn fibre<'a>(name: &'a str, pct: f64) -> FibreInput<'a> {
70 FibreInput {
71 fibre: name,
72 pct,
73 country_of_origin: None,
74 }
75 }
76
77 #[test]
78 fn fibre_sum_within_tolerance() {
79 assert!(fibre_sum_ok(&[60.0, 40.0]));
80 assert!(fibre_sum_ok(&[98.5, 1.0])); assert!(!fibre_sum_ok(&[60.0, 30.0])); assert!(!fibre_sum_ok(&[])); }
84
85 #[test]
86 fn fibre_composition_valid_passes() {
87 assert!(
88 validate_fibre_composition(&[fibre("cotton", 60.0), fibre("polyester", 40.0)]).is_ok()
89 );
90 }
91
92 #[test]
93 fn fibre_sum_invalid_message_has_total() {
94 let err = validate_fibre_composition(&[fibre("cotton", 60.0), fibre("polyester", 30.0)])
95 .unwrap_err();
96 assert!(err.contains("90.0"), "unexpected: {err}");
97 }
98
99 #[test]
100 fn fibre_invalid_country_rejected() {
101 let entry = FibreInput {
102 fibre: "cotton",
103 pct: 100.0,
104 country_of_origin: Some("india"),
105 };
106 let err = validate_fibre_composition(&[entry]).unwrap_err();
107 assert!(err.contains("country_of_origin"), "unexpected: {err}");
108 }
109
110 #[test]
111 fn fibre_empty_rejected() {
112 assert!(validate_fibre_composition(&[]).is_err());
113 }
114
115 #[test]
116 fn nan_pct_rejected() {
117 let err = validate_fibre_composition(&[fibre("cotton", f64::NAN)]).unwrap_err();
118 assert!(
119 err.contains("NaN") || err.contains("finite"),
120 "unexpected: {err}"
121 );
122 }
123
124 #[test]
125 fn infinity_pct_rejected() {
126 let err = validate_fibre_composition(&[fibre("cotton", f64::INFINITY)]).unwrap_err();
127 assert!(
128 err.contains("inf") || err.contains("finite"),
129 "unexpected: {err}"
130 );
131 }
132}