pub fn parse_number(s: &str) -> Option<f64> {
let t: String = s.trim().chars().filter(|c| !matches!(c, ',' | '$' | '£' | '€' | '%' | ' ' | '\u{00A0}')).collect();
if t.is_empty() {
return None;
}
t.parse::<f64>().ok()
}
pub fn parse_quantity(s: &str) -> Option<(String, f64)> {
let s = s.trim();
let mut split = 0;
for (i, ch) in s.char_indices() {
if ch.is_ascii_digit() || ch == '.' || ch == ',' || ch == '-' || ch == '+' {
split = i + ch.len_utf8();
} else if i == 0 || split == 0 {
return None; } else {
break;
}
}
let num: f64 = s[..split].chars().filter(|c| *c != ',').collect::<String>().parse().ok()?;
let unit = s[split..].trim().to_lowercase();
let unit = unit.trim_end_matches(|c: char| c == '.' || c == ')' || c == ']');
let (field, si) = match unit {
"m" | "metre" | "metres" | "meter" | "meters" => ("qty-length", num),
"km" | "kilometre" | "kilometres" => ("qty-length", num * 1000.0),
"cm" => ("qty-length", num * 0.01),
"mm" => ("qty-length", num * 0.001),
"mi" | "mile" | "miles" => ("qty-length", num * 1609.344),
"kg" | "kilogram" | "kilograms" => ("qty-mass", num),
"g" | "gram" | "grams" => ("qty-mass", num * 0.001),
"mg" => ("qty-mass", num * 1e-6),
"t" | "tonne" | "tonnes" | "ton" => ("qty-mass", num * 1000.0),
"lb" | "lbs" | "pound" | "pounds" => ("qty-mass", num * 0.453592),
"m/s" | "mps" => ("qty-speed", num),
"km/h" | "kmh" | "kph" => ("qty-speed", num / 3.6),
"mph" => ("qty-speed", num * 0.44704),
"pa" => ("qty-pressure", num),
"kpa" => ("qty-pressure", num * 1000.0),
"mpa" => ("qty-pressure", num * 1e6),
"bar" => ("qty-pressure", num * 1e5),
"s" | "sec" | "secs" | "second" | "seconds" => ("qty-time", num),
"ms" => ("qty-time", num * 0.001),
"min" | "mins" | "minute" | "minutes" => ("qty-time", num * 60.0),
"h" | "hr" | "hrs" | "hour" | "hours" => ("qty-time", num * 3600.0),
"w" | "watt" | "watts" => ("qty-power", num),
"kw" => ("qty-power", num * 1000.0),
"wh" => ("qty-energy", num),
"kwh" => ("qty-energy", num * 1000.0),
"°c" | "℃" | "c" => ("qty-temp", num),
"°f" | "℉" | "f" => ("qty-temp", (num - 32.0) * 5.0 / 9.0),
"k" | "kelvin" => ("qty-temp", num - 273.15),
_ => return None,
};
Some((field.to_string(), si))
}
pub fn cmp_op(value: f64, op: &str, target: f64) -> bool {
match op {
"ge" | ">=" => value >= target,
"gt" | ">" => value > target,
"le" | "<=" => value <= target,
"lt" | "<" => value < target,
"eq" | "==" | "=" => (value - target).abs() < 1e-9,
"ne" | "!=" => (value - target).abs() >= 1e-9,
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn numbers() {
assert_eq!(parse_number("3,500"), Some(3500.0));
assert_eq!(parse_number("$28000"), Some(28000.0));
assert_eq!(parse_number("12.5%"), Some(12.5));
assert_eq!(parse_number("hello"), None);
}
#[test]
fn quantities() {
assert_eq!(parse_quantity("70 m"), Some(("qty-length".into(), 70.0)));
let (f, v) = parse_quantity("100 km/h").unwrap();
assert_eq!(f, "qty-speed");
assert!((v - 27.777).abs() < 0.01);
assert_eq!(parse_quantity("12 MPa"), Some(("qty-pressure".into(), 12_000_000.0)));
let (_, c) = parse_quantity("205 ℃").unwrap();
assert!((c - 205.0).abs() < 1e-6);
assert_eq!(parse_quantity("not a qty"), None);
}
#[test]
fn ops() {
assert!(cmp_op(70.0, "le", 70.0));
assert!(cmp_op(800.0, "ge", 500.0));
assert!(!cmp_op(38.0, "gt", 70.0));
}
}