hs_predict/rules/
matcher.rs1use crate::rules::static_table::HS_RULES;
5use crate::rules::types::{HsRule, ShapePattern};
6use crate::types::PhysicalForm;
7
8pub fn find_best_rule(
17 cas: &str,
18 physical_form: Option<&PhysicalForm>,
19 purity_pct: Option<f64>,
20) -> Option<&'static HsRule> {
21 let form = physical_form.unwrap_or(&PhysicalForm::Unknown);
22
23 let mut best: Option<(&'static HsRule, u8)> = None;
24
25 for rule in HS_RULES {
26 if rule.cas != cas {
27 continue;
28 }
29
30 if !matches_shape(form, &rule.shape) {
32 continue;
33 }
34
35 let purity_ok = match (&rule.purity_range, purity_pct) {
37 (None, _) => true,
38 (Some(range), Some(p)) => range.contains(&p),
39 (Some(_), None) => false, };
41 if !purity_ok {
42 continue;
43 }
44
45 let specificity = shape_specificity(&rule.shape)
46 + if rule.purity_range.is_some() { 2 } else { 0 };
47
48 if best.is_none() || specificity > best.unwrap().1 {
49 best = Some((rule, specificity));
50 }
51 }
52
53 best.map(|(rule, _)| rule)
54}
55
56pub fn matches_shape(form: &PhysicalForm, pattern: &ShapePattern) -> bool {
58 match (form, pattern) {
59 (_, ShapePattern::Any) => true,
60 (PhysicalForm::Solid, ShapePattern::Solid) => true,
61 (PhysicalForm::Powder { .. }, ShapePattern::Powder) => true,
62 (PhysicalForm::Granules, ShapePattern::Granules) => true,
63 (PhysicalForm::Granules, ShapePattern::Powder) => true, (PhysicalForm::Liquid, ShapePattern::Liquid) => true,
65 (PhysicalForm::Gas, ShapePattern::Gas) => true,
66 (PhysicalForm::Ingot, ShapePattern::Ingot) => true,
67 (PhysicalForm::Foil { .. }, ShapePattern::Foil) => true,
68 (PhysicalForm::Unknown, _) => false,
69 (
70 PhysicalForm::Solution { concentration_pct_ww, .. },
71 ShapePattern::Solution { concentration_range_pct },
72 ) => match (concentration_pct_ww, concentration_range_pct) {
73 (_, None) => true,
74 (None, Some(_)) => false,
75 (Some(c), Some(range)) => range.contains(c),
76 },
77 _ => false,
78 }
79}
80
81fn shape_specificity(pattern: &ShapePattern) -> u8 {
83 match pattern {
84 ShapePattern::Any => 0,
85 ShapePattern::Solution { concentration_range_pct: None } => 1,
86 ShapePattern::Solution { concentration_range_pct: Some(_) } => 3,
87 _ => 2,
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 #[test]
96 fn naoh_solid_matches_281511() {
97 let rule = find_best_rule("1310-73-2", Some(&PhysicalForm::Solid), None).unwrap();
98 assert_eq!(rule.hs_code, "281511");
99 }
100
101 #[test]
102 fn naoh_solution_matches_281512() {
103 let rule = find_best_rule(
104 "1310-73-2",
105 Some(&PhysicalForm::Solution { solvent: None, concentration_pct_ww: Some(50.0) }),
106 None,
107 )
108 .unwrap();
109 assert_eq!(rule.hs_code, "281512");
110 }
111
112 #[test]
113 fn hno3_fuming_matches_specific_code() {
114 let rule = find_best_rule(
115 "7697-37-2",
116 Some(&PhysicalForm::Solution { solvent: None, concentration_pct_ww: Some(99.0) }),
117 None,
118 )
119 .unwrap();
120 assert!(rule.confidence >= 0.85);
122 }
123
124 #[test]
125 fn unknown_cas_returns_none() {
126 let rule = find_best_rule("0000-00-0", Some(&PhysicalForm::Solid), None);
127 assert!(rule.is_none());
128 }
129}