Skip to main content

timezone_data/
meta.rs

1//! Per-zone metadata derived from `zone1970.tab` and `iso3166.tab`.
2//!
3//! Both tables are embedded in `zoneinfo.zip` and scanned on demand; no index
4//! is built and nothing is allocated.
5
6use crate::zipstore;
7use crate::ZONEINFO_ZIP;
8
9/// Metadata about a timezone: associated countries and principal coordinates.
10#[derive(Debug, Clone, Copy, PartialEq)]
11pub struct ZoneMeta<'a> {
12    /// Latitude of the principal location (degrees, north positive).
13    pub lat: f64,
14    /// Longitude of the principal location (degrees, east positive).
15    pub lon: f64,
16    /// Optional commentary (e.g. a region description); empty if none.
17    pub commentary: &'a str,
18    /// The raw comma-separated ISO 3166-1 alpha-2 country codes field.
19    codes: &'a str,
20}
21
22/// An ISO 3166 country associated with a timezone.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub struct Country<'a> {
25    /// ISO 3166-1 alpha-2 code (e.g. `US`).
26    pub code: &'a str,
27    /// Country name (e.g. `United States`); empty if not found.
28    pub name: &'a str,
29}
30
31impl ZoneMeta<'static> {
32    /// Iterates over the countries that overlap this timezone.
33    pub fn countries(&self) -> impl Iterator<Item = Country<'static>> {
34        self.codes.split(',').map(|code| Country {
35            code,
36            name: iso_name(code),
37        })
38    }
39}
40
41/// Returns metadata for the timezone named `name`, or `None` if unavailable.
42pub fn meta(name: &str) -> Option<ZoneMeta<'static>> {
43    let data = zipstore::find(ZONEINFO_ZIP, "zone1970.tab").ok()?;
44    let text = core::str::from_utf8(data).ok()?;
45    for line in text.split('\n') {
46        if line.is_empty() || line.starts_with('#') {
47            continue;
48        }
49        let mut fields = line.split('\t');
50        let codes = fields.next()?;
51        let coord = fields.next()?;
52        let zname = match fields.next() {
53            Some(z) => z,
54            None => continue,
55        };
56        if zname != name {
57            continue;
58        }
59        let commentary = fields.next().unwrap_or("");
60        let (lat, lon) = parse_iso6709(coord);
61        return Some(ZoneMeta {
62            lat,
63            lon,
64            commentary,
65            codes,
66        });
67    }
68    None
69}
70
71/// Looks up the country name for an ISO 3166-1 alpha-2 `code`.
72fn iso_name(code: &str) -> &'static str {
73    let data = match zipstore::find(ZONEINFO_ZIP, "iso3166.tab") {
74        Ok(d) => d,
75        Err(_) => return "",
76    };
77    let text = core::str::from_utf8(data).unwrap_or("");
78    for line in text.split('\n') {
79        if line.is_empty() || line.starts_with('#') {
80            continue;
81        }
82        let mut parts = line.splitn(2, '\t');
83        let c = parts.next().unwrap_or("");
84        if c == code {
85            return parts.next().unwrap_or("");
86        }
87    }
88    ""
89}
90
91/// Parses coordinates in ISO 6709 format `±DDMM±DDDMM` or `±DDMMSS±DDDMMSS`.
92pub fn parse_iso6709(s: &str) -> (f64, f64) {
93    let b = s.as_bytes();
94    // The latitude starts at index 0; the longitude starts at the second sign.
95    let mut lon_start = None;
96    for (i, &c) in b.iter().enumerate().skip(1) {
97        if c == b'+' || c == b'-' {
98            lon_start = Some(i);
99            break;
100        }
101    }
102    let Some(lon_start) = lon_start else {
103        return (0.0, 0.0);
104    };
105    let lat = parse_dms(&s[..lon_start], 2);
106    let lon = parse_dms(&s[lon_start..], 3);
107    (lat, lon)
108}
109
110/// Parses a `±DD[D]MM[SS]` string into decimal degrees, rounded to 4 places.
111/// `deg_digits` is 2 for latitude, 3 for longitude.
112fn parse_dms(s: &str, deg_digits: usize) -> f64 {
113    let b = s.as_bytes();
114    if b.len() < 1 + deg_digits + 2 {
115        return 0.0;
116    }
117    let neg = b[0] == b'-';
118    let mut i = 1; // skip sign
119
120    let deg = atoi(&b[i..i + deg_digits]);
121    i += deg_digits;
122    let min = atoi(&b[i..i + 2]);
123    i += 2;
124    let sec = if b.len() >= i + 2 {
125        atoi(&b[i..i + 2])
126    } else {
127        0
128    };
129
130    // Round to 4 decimal places using integer arithmetic (no std float methods).
131    let total_seconds = deg * 3600 + min * 60 + sec;
132    let val_e4 = (total_seconds * 10000 + 1800) / 3600;
133    let v = val_e4 as f64 / 10000.0;
134    if neg {
135        -v
136    } else {
137        v
138    }
139}
140
141fn atoi(b: &[u8]) -> i64 {
142    let mut n = 0i64;
143    for &c in b {
144        if c.is_ascii_digit() {
145            n = n * 10 + (c - b'0') as i64;
146        }
147    }
148    n
149}