Skip to main content

haystack_core/kinds/
units.rs

1use std::collections::HashMap;
2use std::sync::LazyLock;
3
4/// A Haystack unit definition.
5#[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
62/// Look up a unit by name or symbol.
63pub fn unit_for(s: &str) -> Option<&'static Unit> {
64    UNITS.by_name.get(s).or_else(|| UNITS.by_symbol.get(s))
65}
66
67/// Get all units indexed by name.
68pub fn units_by_name() -> &'static HashMap<String, Unit> {
69    &UNITS.by_name
70}
71
72/// Get all units indexed by symbol.
73pub 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        // Should have hundreds of units
86        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}