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
6/// `zone1970.tab` and `iso3166.tab`, written alongside the generated zones by
7/// `xtask` and embedded as text.
8const ZONE1970_TAB: &str = include_str!("zone1970.tab");
9const ISO3166_TAB: &str = include_str!("iso3166.tab");
10
11/// Metadata about a timezone: associated countries and principal coordinates.
12#[derive(Debug, Clone, Copy, PartialEq)]
13pub struct ZoneMeta<'a> {
14    /// Latitude of the principal location (degrees, north positive).
15    pub lat: f64,
16    /// Longitude of the principal location (degrees, east positive).
17    pub lon: f64,
18    /// Optional commentary (e.g. a region description); empty if none.
19    pub commentary: &'a str,
20    /// The raw comma-separated ISO 3166-1 alpha-2 country codes field.
21    codes: &'a str,
22}
23
24/// An ISO 3166 country associated with a timezone.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub struct Country<'a> {
27    /// ISO 3166-1 alpha-2 code (e.g. `US`).
28    pub code: &'a str,
29    /// Country name (e.g. `United States`); empty if not found.
30    pub name: &'a str,
31}
32
33impl ZoneMeta<'static> {
34    /// Iterates over the countries that overlap this timezone.
35    pub fn countries(&self) -> impl Iterator<Item = Country<'static>> {
36        self.codes.split(',').map(|code| Country {
37            code,
38            name: iso_name(code),
39        })
40    }
41}
42
43/// Returns metadata for the timezone named `name`, or `None` if unavailable.
44pub fn meta(name: &str) -> Option<ZoneMeta<'static>> {
45    for line in ZONE1970_TAB.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    for line in ISO3166_TAB.split('\n') {
74        if line.is_empty() || line.starts_with('#') {
75            continue;
76        }
77        let mut parts = line.splitn(2, '\t');
78        let c = parts.next().unwrap_or("");
79        if c == code {
80            return parts.next().unwrap_or("");
81        }
82    }
83    ""
84}
85
86/// Parses coordinates in ISO 6709 format `±DDMM±DDDMM` or `±DDMMSS±DDDMMSS`.
87pub fn parse_iso6709(s: &str) -> (f64, f64) {
88    let b = s.as_bytes();
89    // The latitude starts at index 0; the longitude starts at the second sign.
90    let mut lon_start = None;
91    for (i, &c) in b.iter().enumerate().skip(1) {
92        if c == b'+' || c == b'-' {
93            lon_start = Some(i);
94            break;
95        }
96    }
97    let Some(lon_start) = lon_start else {
98        return (0.0, 0.0);
99    };
100    let lat = parse_dms(&s[..lon_start], 2);
101    let lon = parse_dms(&s[lon_start..], 3);
102    (lat, lon)
103}
104
105/// Parses a `±DD[D]MM[SS]` string into decimal degrees, rounded to 4 places.
106/// `deg_digits` is 2 for latitude, 3 for longitude.
107fn parse_dms(s: &str, deg_digits: usize) -> f64 {
108    let b = s.as_bytes();
109    if b.len() < 1 + deg_digits + 2 {
110        return 0.0;
111    }
112    let neg = b[0] == b'-';
113    let mut i = 1; // skip sign
114
115    let deg = atoi(&b[i..i + deg_digits]);
116    i += deg_digits;
117    let min = atoi(&b[i..i + 2]);
118    i += 2;
119    let sec = if b.len() >= i + 2 {
120        atoi(&b[i..i + 2])
121    } else {
122        0
123    };
124
125    // Round to 4 decimal places using integer arithmetic (no std float methods).
126    let total_seconds = deg * 3600 + min * 60 + sec;
127    let val_e4 = (total_seconds * 10000 + 1800) / 3600;
128    let v = val_e4 as f64 / 10000.0;
129    if neg {
130        -v
131    } else {
132        v
133    }
134}
135
136fn atoi(b: &[u8]) -> i64 {
137    let mut n = 0i64;
138    for &c in b {
139        if c.is_ascii_digit() {
140            n = n * 10 + (c - b'0') as i64;
141        }
142    }
143    n
144}