haystack_core/kinds/
units.rs1use std::collections::HashMap;
2use std::sync::LazyLock;
3
4#[derive(Debug, Clone, PartialEq, Eq, Hash)]
6pub struct Unit {
7 pub name: String,
8 pub symbols: Vec<String>,
9 pub quantity: String,
10}
11
12struct UnitsRegistry {
13 by_name: HashMap<String, Unit>,
14 by_symbol: HashMap<String, Unit>,
15}
16
17static UNITS: LazyLock<UnitsRegistry> = LazyLock::new(|| {
18 let data = include_str!("../../data/units.txt");
19 parse_units(data)
20});
21
22fn parse_units(data: &str) -> UnitsRegistry {
23 let mut by_name = HashMap::new();
24 let mut by_symbol = HashMap::new();
25 let mut current_quantity = String::new();
26
27 for line in data.lines() {
28 let line = line.trim();
29 if line.is_empty() || line.starts_with("//") {
30 continue;
31 }
32 if line.starts_with("-- ") && line.ends_with(" --") {
33 current_quantity = line[3..line.len() - 3].to_string();
34 continue;
35 }
36 let parts: Vec<&str> = line.split(',').collect();
37 if parts.is_empty() {
38 continue;
39 }
40 let name = parts[0].trim().to_string();
41 let symbols: Vec<String> = parts[1..]
42 .iter()
43 .map(|s| s.trim().to_string())
44 .filter(|s| !s.is_empty())
45 .collect();
46
47 let unit = Unit {
48 name: name.clone(),
49 symbols: symbols.clone(),
50 quantity: current_quantity.clone(),
51 };
52
53 by_name.insert(name, unit.clone());
54 for sym in &symbols {
55 by_symbol.insert(sym.clone(), unit.clone());
56 }
57 }
58
59 UnitsRegistry { by_name, by_symbol }
60}
61
62pub fn unit_for(s: &str) -> Option<&'static Unit> {
64 UNITS.by_name.get(s).or_else(|| UNITS.by_symbol.get(s))
65}
66
67pub fn units_by_name() -> &'static HashMap<String, Unit> {
69 &UNITS.by_name
70}
71
72pub fn units_by_symbol() -> &'static HashMap<String, Unit> {
74 &UNITS.by_symbol
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80
81 #[test]
82 fn units_loaded() {
83 let by_name = units_by_name();
84 assert!(!by_name.is_empty(), "units should be loaded from units.txt");
85 assert!(
87 by_name.len() > 100,
88 "expected 100+ units, got {}",
89 by_name.len()
90 );
91 }
92
93 #[test]
94 fn unit_lookup_by_name() {
95 let u = unit_for("fahrenheit");
96 assert!(u.is_some(), "fahrenheit should exist");
97 let u = u.unwrap();
98 assert_eq!(u.name, "fahrenheit");
99 assert!(u.symbols.contains(&"°F".to_string()));
100 }
101
102 #[test]
103 fn unit_lookup_by_symbol() {
104 let u = unit_for("°F");
105 assert!(u.is_some(), "°F should resolve");
106 assert_eq!(u.unwrap().name, "fahrenheit");
107 }
108
109 #[test]
110 fn unit_lookup_celsius() {
111 let u = unit_for("celsius");
112 assert!(u.is_some());
113 assert!(u.unwrap().symbols.contains(&"°C".to_string()));
114 }
115
116 #[test]
117 fn unit_not_found() {
118 assert!(unit_for("nonexistent_unit_xyz").is_none());
119 }
120
121 #[test]
122 fn unit_has_quantity() {
123 let u = unit_for("fahrenheit").unwrap();
124 assert!(
125 !u.quantity.is_empty(),
126 "unit should have a quantity category"
127 );
128 }
129}