1pub fn parse_number(s: &str) -> Option<f64> {
11 let t: String = s.trim().chars().filter(|c| !matches!(c, ',' | '$' | '£' | '€' | '%' | ' ' | '\u{00A0}')).collect();
12 if t.is_empty() {
13 return None;
14 }
15 t.parse::<f64>().ok()
16}
17
18pub fn parse_quantity(s: &str) -> Option<(String, f64)> {
21 let s = s.trim();
22 let mut split = 0;
24 for (i, ch) in s.char_indices() {
25 if ch.is_ascii_digit() || ch == '.' || ch == ',' || ch == '-' || ch == '+' {
26 split = i + ch.len_utf8();
27 } else if i == 0 || split == 0 {
28 return None; } else {
30 break;
31 }
32 }
33 let num: f64 = s[..split].chars().filter(|c| *c != ',').collect::<String>().parse().ok()?;
34 let unit = s[split..].trim().to_lowercase();
35 let unit = unit.trim_end_matches(|c: char| c == '.' || c == ')' || c == ']');
36
37 let (field, si) = match unit {
39 "m" | "metre" | "metres" | "meter" | "meters" => ("qty-length", num),
41 "km" | "kilometre" | "kilometres" => ("qty-length", num * 1000.0),
42 "cm" => ("qty-length", num * 0.01),
43 "mm" => ("qty-length", num * 0.001),
44 "mi" | "mile" | "miles" => ("qty-length", num * 1609.344),
45 "kg" | "kilogram" | "kilograms" => ("qty-mass", num),
47 "g" | "gram" | "grams" => ("qty-mass", num * 0.001),
48 "mg" => ("qty-mass", num * 1e-6),
49 "t" | "tonne" | "tonnes" | "ton" => ("qty-mass", num * 1000.0),
50 "lb" | "lbs" | "pound" | "pounds" => ("qty-mass", num * 0.453592),
51 "m/s" | "mps" => ("qty-speed", num),
53 "km/h" | "kmh" | "kph" => ("qty-speed", num / 3.6),
54 "mph" => ("qty-speed", num * 0.44704),
55 "pa" => ("qty-pressure", num),
57 "kpa" => ("qty-pressure", num * 1000.0),
58 "mpa" => ("qty-pressure", num * 1e6),
59 "bar" => ("qty-pressure", num * 1e5),
60 "s" | "sec" | "secs" | "second" | "seconds" => ("qty-time", num),
62 "ms" => ("qty-time", num * 0.001),
63 "min" | "mins" | "minute" | "minutes" => ("qty-time", num * 60.0),
64 "h" | "hr" | "hrs" | "hour" | "hours" => ("qty-time", num * 3600.0),
65 "w" | "watt" | "watts" => ("qty-power", num),
67 "kw" => ("qty-power", num * 1000.0),
68 "wh" => ("qty-energy", num),
69 "kwh" => ("qty-energy", num * 1000.0),
70 "°c" | "℃" | "c" => ("qty-temp", num),
72 "°f" | "℉" | "f" => ("qty-temp", (num - 32.0) * 5.0 / 9.0),
73 "k" | "kelvin" => ("qty-temp", num - 273.15),
74 _ => return None,
75 };
76 Some((field.to_string(), si))
77}
78
79pub fn cmp_op(value: f64, op: &str, target: f64) -> bool {
81 match op {
82 "ge" | ">=" => value >= target,
83 "gt" | ">" => value > target,
84 "le" | "<=" => value <= target,
85 "lt" | "<" => value < target,
86 "eq" | "==" | "=" => (value - target).abs() < 1e-9,
87 "ne" | "!=" => (value - target).abs() >= 1e-9,
88 _ => false,
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 #[test]
97 fn numbers() {
98 assert_eq!(parse_number("3,500"), Some(3500.0));
99 assert_eq!(parse_number("$28000"), Some(28000.0));
100 assert_eq!(parse_number("12.5%"), Some(12.5));
101 assert_eq!(parse_number("hello"), None);
102 }
103
104 #[test]
105 fn quantities() {
106 assert_eq!(parse_quantity("70 m"), Some(("qty-length".into(), 70.0)));
107 let (f, v) = parse_quantity("100 km/h").unwrap();
108 assert_eq!(f, "qty-speed");
109 assert!((v - 27.777).abs() < 0.01);
110 assert_eq!(parse_quantity("12 MPa"), Some(("qty-pressure".into(), 12_000_000.0)));
111 let (_, c) = parse_quantity("205 ℃").unwrap();
112 assert!((c - 205.0).abs() < 1e-6);
113 assert_eq!(parse_quantity("not a qty"), None);
114 }
115
116 #[test]
117 fn ops() {
118 assert!(cmp_op(70.0, "le", 70.0));
119 assert!(cmp_op(800.0, "ge", 500.0));
120 assert!(!cmp_op(38.0, "gt", 70.0));
121 }
122}