zic-rs 0.1.0

A memory-safe Rust timezone compiler for IANA tzdata, producing TZif files with deterministic output and reference-zic comparison.
Documentation
//! The parsed, still-textual *semantic model* of a tzdata source: the records the parser
//! produces and the compiler consumes.
//!
//! These types mirror the three tzdata line kinds (`Rule`, `Zone`, `Link`) plus the
//! continuation structure of zones. They are intentionally close to the source — value
//! interpretation (calendar math, transition generation) happens later, in `compile`.

pub mod calendar;
pub mod leap;
pub mod time;

use std::collections::BTreeMap;
use std::path::{Path, PathBuf};

pub use leap::{LeapSecond, LeapTable};
pub use time::{Offset, Save, TimeOfDay, TimeRef};

/// Where a record came from, for diagnostics.
#[derive(Debug, Clone)]
pub struct Origin {
    pub file: PathBuf,
    pub line: usize,
}

impl Origin {
    pub fn new(file: &Path, line: usize) -> Self {
        Origin {
            file: file.to_path_buf(),
            line,
        }
    }
}

/// A `Rule` line. Stored verbatim-ish in T1 (only validated, not yet expanded); the
/// transition compiler in T2 consumes the typed fields.
#[derive(Debug, Clone)]
pub struct RuleRecord {
    pub name: String,
    /// `FROM` year.
    pub from: i32,
    /// `TO` year (after resolving `only`/`maximum`).
    pub to: YearBound,
    pub in_month: u8,
    pub on: calendar::OnDay,
    pub at: TimeOfDay,
    pub save: Save,
    /// `LETTER/S` — the variable part of the abbreviation (`-` becomes empty).
    pub letter: String,
    pub origin: Origin,
}

/// Upper bound of a rule's year range.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum YearBound {
    /// A concrete year.
    Year(i32),
    /// `maximum` — extends indefinitely.
    Max,
}

/// The `RULES` column of a zone era.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ZoneRules {
    /// `-` : standard time throughout this era.
    None,
    /// An inline saving (the column held a time like `1:00`), not a named ruleset.
    Save(Save),
    /// A named ruleset (`Rule NAME ...`).
    Named(String),
}

/// One era of a zone (one `Zone`/continuation line up to its `UNTIL`).
#[derive(Debug, Clone)]
pub struct ZoneEra {
    /// Standard UT offset for the era (`STDOFF`), seconds east of UTC.
    pub stdoff: Offset,
    pub rules: ZoneRules,
    /// Abbreviation template (`FORMAT`).
    pub format: String,
    /// `UNTIL` instant ending this era; `None` for the final era.
    pub until: Option<Until>,
    pub origin: Origin,
}

/// The `UNTIL` field: a partially-specified wall/standard/UT instant.
#[derive(Debug, Clone)]
pub struct Until {
    pub year: i32,
    pub month: u8,
    pub day: calendar::OnDay,
    pub time: TimeOfDay,
}

/// A `Zone` plus its continuation lines.
#[derive(Debug, Clone)]
pub struct ZoneRecord {
    pub name: String,
    pub eras: Vec<ZoneEra>,
    pub origin: Origin,
}

/// A `Link` line: `LINK-NAME` is an alias for `TARGET`.
#[derive(Debug, Clone)]
pub struct LinkRecord {
    pub target: String,
    pub link_name: String,
    pub origin: Origin,
}

/// The whole parsed source: zones, links, and rules keyed by name.
#[derive(Debug, Default, Clone)]
pub struct Database {
    pub zones: Vec<ZoneRecord>,
    pub links: Vec<LinkRecord>,
    pub rules: BTreeMap<String, Vec<RuleRecord>>,
}

impl Database {
    /// Find a zone by exact name.
    pub fn zone(&self, name: &str) -> Option<&ZoneRecord> {
        self.zones.iter().find(|z| z.name == name)
    }
}