Skip to main content

haystack_core/kinds/
tz.rs

1use std::collections::HashMap;
2use std::sync::LazyLock;
3
4static TZ_MAP: LazyLock<HashMap<String, String>> = LazyLock::new(build_tz_map);
5
6fn build_tz_map() -> HashMap<String, String> {
7    let mut map = HashMap::new();
8
9    // Load from generated data file
10    let data = include_str!("../../data/tz_map.txt");
11    for line in data.lines() {
12        let line = line.trim();
13        if line.is_empty() || line.starts_with('#') {
14            continue;
15        }
16        if let Some((city, iana)) = line.split_once('=') {
17            map.insert(city.trim().to_string(), iana.trim().to_string());
18        }
19    }
20
21    // Special Haystack mappings (inserted after file data to override)
22    map.insert("UTC".to_string(), "UTC".to_string());
23    map.insert("GMT".to_string(), "Etc/GMT".to_string());
24    map.insert("Rel".to_string(), "UTC".to_string());
25
26    map
27}
28
29/// Resolve a Haystack timezone name to an IANA identifier.
30///
31/// Tries city name lookup first (e.g., "New_York" -> "America/New_York"),
32/// then checks if the input is already a valid IANA path (contains `/`).
33pub fn tz_for(name: &str) -> Option<&'static str> {
34    // City name lookup (most common case)
35    if let Some(iana) = TZ_MAP.get(name) {
36        return Some(iana.as_str());
37    }
38    // Check if it's already a full IANA path that's in our values
39    if name.contains('/') {
40        for v in TZ_MAP.values() {
41            if v == name {
42                return Some(v.as_str());
43            }
44        }
45    }
46    None
47}
48
49/// Get the full timezone map.
50pub fn tz_map() -> &'static HashMap<String, String> {
51    &TZ_MAP
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57
58    #[test]
59    fn tz_map_loaded() {
60        let map = tz_map();
61        assert!(
62            map.len() > 100,
63            "expected 100+ timezone mappings, got {}",
64            map.len()
65        );
66    }
67
68    #[test]
69    fn tz_utc() {
70        assert_eq!(tz_for("UTC"), Some("UTC"));
71    }
72
73    #[test]
74    fn tz_gmt() {
75        assert_eq!(tz_for("GMT"), Some("Etc/GMT"));
76    }
77
78    #[test]
79    fn tz_rel() {
80        assert_eq!(tz_for("Rel"), Some("UTC"));
81    }
82
83    #[test]
84    fn tz_new_york() {
85        let result = tz_for("New_York");
86        assert!(result.is_some(), "New_York should resolve");
87        assert_eq!(result.unwrap(), "America/New_York");
88    }
89
90    #[test]
91    fn tz_london() {
92        let result = tz_for("London");
93        assert!(result.is_some(), "London should resolve");
94        assert_eq!(result.unwrap(), "Europe/London");
95    }
96
97    #[test]
98    fn tz_unknown() {
99        assert_eq!(tz_for("Nonexistent_City"), None);
100    }
101
102    #[test]
103    fn tz_full_iana_path() {
104        let result = tz_for("America/New_York");
105        assert!(result.is_some(), "Full IANA path should resolve");
106    }
107}