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