pub mod abbreviations;
pub mod leap;
pub mod plan;
pub mod posix_footer;
pub mod transitions;
pub use leap::apply_leaps;
use crate::diagnostics::{Diagnostic, DiagnosticCode};
use crate::error::{Error, Result};
use crate::model::{Database, Save, ZoneEra, ZoneRecord, ZoneRules};
use crate::tzif::{LocalTimeType, TzifData};
pub fn compile_zone(db: &Database, name: &str) -> Result<TzifData> {
compile_zone_styled(db, name, crate::EmitOptions::default())
}
pub fn compile_zone_styled(
db: &Database,
name: &str,
opts: crate::EmitOptions,
) -> Result<TzifData> {
let zone = db
.zone(name)
.ok_or_else(|| Error::message(format!("no zone named {name:?} in the input")))?;
if zone.eras.len() != 1 {
return transitions::compile_multi_era(zone, db, opts);
}
let era = &zone.eras[0];
let data = match &era.rules {
ZoneRules::None => {
let mut d = compile_fixed_offset(zone, era)?;
if let Some(range) = opts.range {
transitions::apply_range(&mut d, range);
}
d
}
ZoneRules::Named(rule_name) => {
let rules = db.rules.get(rule_name).ok_or_else(|| {
unsupported(
zone,
format!("zone references unknown rule set {rule_name:?}"),
)
})?;
transitions::compile_rule_zone(zone, era, rules, opts)?
}
ZoneRules::Save(save) => {
let mut d = compile_inline_save(zone, era, *save)?;
if let Some(range) = opts.range {
transitions::apply_range(&mut d, range);
}
d
}
};
Ok(data)
}
fn compile_inline_save(zone: &ZoneRecord, era: &ZoneEra, save: Save) -> Result<TzifData> {
let fmt = &era.format;
if fmt.contains("%s") || fmt.contains('/') {
return Err(unsupported(
zone,
format!("inline-save FORMAT {fmt:?}: only literal and %z are supported yet (no %s / STD/DST slash)"),
));
}
let utoff = era.stdoff.0 + save.seconds;
let abbr = abbreviations::render(fmt, "", save.is_dst, utoff);
let footer = posix_footer::fixed_offset(&abbr, utoff);
Ok(TzifData {
types: vec![LocalTimeType {
utoff,
is_dst: save.is_dst,
abbr,
}],
transitions: Vec::new(),
footer,
version: b'2',
leaps: Vec::new(),
})
}
fn compile_fixed_offset(zone: &ZoneRecord, era: &ZoneEra) -> Result<TzifData> {
let fmt = &era.format;
if fmt.contains("%s") || fmt.contains('/') {
return Err(unsupported(
zone,
format!(
"FORMAT {fmt:?}: %s / STD-DST slash need rule context, but this era has no rules"
),
));
}
let utoff = era.stdoff.0;
let abbr = abbreviations::render(fmt, "", false, utoff);
let footer = posix_footer::fixed_offset(&abbr, utoff);
Ok(TzifData::fixed(utoff, abbr, footer))
}
fn unsupported(zone: &ZoneRecord, msg: impl Into<String>) -> Error {
Error::from(Diagnostic::error(
DiagnosticCode::UnsupportedDirective,
msg,
&zone.origin.file,
zone.origin.line,
))
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn db(src: &str) -> Database {
let mut db = Database::default();
crate::source::parse_into(src.as_bytes(), &PathBuf::from("t.zi"), &mut db).unwrap();
db
}
#[test]
fn compiles_utc() {
let d = compile_zone(&db("Zone Etc/UTC 0 - UTC\n"), "Etc/UTC").unwrap();
assert_eq!(d.types.len(), 1);
assert_eq!(d.types[0].utoff, 0);
assert_eq!(d.types[0].abbr, "UTC");
assert_eq!(d.footer, "UTC0");
assert!(d.transitions.is_empty());
}
#[test]
fn compiles_fixed_offset() {
let d = compile_zone(&db("Zone Test/Fixed -5:00 - EST\n"), "Test/Fixed").unwrap();
assert_eq!(d.types[0].utoff, -18000);
assert_eq!(d.footer, "EST5");
}
#[test]
fn finite_rule_zone_compiles() {
let src = "Rule X 2020 only - Mar Sun>=8 2:00 1:00 D\n\
Rule X 2020 only - Nov Sun>=1 2:00 0 S\n\
Zone Test/Simple -5:00 X E%sT\n";
let d = compile_zone(&db(src), "Test/Simple").unwrap();
assert_eq!(d.transitions.len(), 2);
assert_eq!(d.footer, "EST5");
let spring = &d.types[d.transitions[0].type_index as usize];
let fall = &d.types[d.transitions[1].type_index as usize];
assert_eq!(
(spring.utoff, spring.is_dst, spring.abbr.as_str()),
(-14400, true, "EDT")
);
assert_eq!(
(fall.utoff, fall.is_dst, fall.abbr.as_str()),
(-18000, false, "EST")
);
assert_eq!(d.transitions[0].at, 1583650800);
assert_eq!(d.transitions[1].at, 1604210400);
}
#[test]
fn recurring_rule_zone_compiles_with_posix_footer() {
let src = "Rule US 2007 max - Mar Sun>=8 2:00 1:00 D\n\
Rule US 2007 max - Nov Sun>=1 2:00 0 S\n\
Zone Test/Eastern -5:00 US E%sT\n";
let d = compile_zone(&db(src), "Test/Eastern").unwrap();
assert_eq!(d.footer, "EST5EDT,M3.2.0,M11.1.0");
assert!(
d.transitions.len() > 2,
"explicit transitions across the window"
);
}
#[test]
fn recurring_sun_leq_25_uses_extended_v3_footer() {
let src = "Rule W 2000 max - Mar Sun>=8 2:00 1:00 D\n\
Rule W 2000 max - Oct Sun<=25 2:00 0 S\n\
Zone Z -5:00 W E%sT\n";
let d = compile_zone(&db(src), "Z").unwrap();
assert_eq!(d.footer, "EST5EDT,M3.2.0,M10.3.3/98");
assert_eq!(d.version, b'3');
}
#[test]
fn percent_format_without_rules_fails_closed() {
let e = compile_zone(&db("Zone Z -5:00 - E%sT\n"), "Z").unwrap_err();
assert!(e.diagnostic().is_some());
}
}