use crate::compile::posix_footer::{self, Recurring};
use crate::diagnostics::{Diagnostic, DiagnosticCode};
use crate::error::{Error, Result};
use crate::model::calendar::{days_from_civil, resolve_on_day, year_of_unix};
use crate::model::time::{TimeOfDay, TimeRef};
use crate::model::{Database, RuleRecord, Save, Until, YearBound, ZoneEra, ZoneRecord, ZoneRules};
use crate::tzif::{LocalTimeType, Transition, TzifData};
use super::abbreviations::render;
const MAX_TRANSITIONS: usize = 100_000;
const RECUR_HI: i32 = 2037;
fn ensure_strictly_increasing(transitions: &[Transition], zone: &ZoneRecord) -> Result<()> {
if transitions.windows(2).any(|w| w[0].at >= w[1].at) {
return Err(Error::from(Diagnostic::error(
DiagnosticCode::SimultaneousTransition,
"two rules produce a transition at the same instant",
&zone.origin.file,
zone.origin.line,
)));
}
Ok(())
}
pub fn compile_rule_zone(
zone: &ZoneRecord,
era: &ZoneEra,
rules: &[RuleRecord],
opts: crate::EmitOptions,
) -> Result<TzifData> {
let stdoff = era.stdoff.0;
if rules.is_empty() {
return Err(unsupported(zone, "named rule set has no rules"));
}
let has_recurring = rules.iter().any(|r| matches!(r.to, YearBound::Max));
let lo = rules.iter().map(|r| r.from).min().expect("non-empty");
let hi = if has_recurring {
RECUR_HI
} else {
rules
.iter()
.filter_map(|r| match r.to {
YearBound::Year(y) => Some(y),
YearBound::Max => None,
})
.max()
.expect("finite set has a concrete TO")
};
let activations = expand_activations(zone, rules, lo, hi)?;
if activations.len() > MAX_TRANSITIONS {
return Err(located(
zone,
DiagnosticCode::TooManyTransitions,
format!(
"zone produces {} transitions, exceeding the limit of {MAX_TRANSITIONS}",
activations.len()
),
));
}
let mut types: Vec<LocalTimeType> = Vec::new();
let std_letter = standard_letter(rules);
let initial = LocalTimeType {
utoff: stdoff,
is_dst: false,
abbr: render(&era.format, &std_letter, false, stdoff),
};
let _ = intern(&mut types, initial);
let mut transitions: Vec<Transition> = Vec::new();
let mut from_recurring: Vec<bool> = Vec::new();
let mut save_prev: i32 = 0;
let mut last_type_index: u8 = 0;
for act in &activations {
let ut = match act.at_ref {
TimeRef::Wall => act.local_seconds - stdoff as i64 - save_prev as i64,
TimeRef::Standard => act.local_seconds - stdoff as i64,
TimeRef::Universal => act.local_seconds,
};
let utoff = stdoff + act.save;
let ty = LocalTimeType {
utoff,
is_dst: act.is_dst,
abbr: render(&era.format, &act.letter, act.is_dst, utoff),
};
let type_index = intern(&mut types, ty);
transitions.push(Transition { at: ut, type_index });
from_recurring.push(act.from_recurring);
last_type_index = type_index;
save_prev = act.save;
}
let (mut footer, version) = if has_recurring {
build_recurring_footer(zone, era, stdoff, rules)?
} else {
let tail = &types[last_type_index as usize];
(posix_footer::fixed_offset(&tail.abbr, tail.utoff), b'2')
};
if let Some(range) = opts.range {
apply_range_to_stream(
&mut types,
&mut transitions,
&mut from_recurring,
&mut footer,
range,
);
}
finalize_emit_style(opts, &mut types, &mut transitions, &from_recurring);
ensure_strictly_increasing(&transitions, zone)?;
Ok(TzifData {
types,
transitions,
footer,
version,
leaps: Vec::new(),
})
}
struct Activation {
local_seconds: i64,
at_ref: TimeRef,
save: i32,
is_dst: bool,
letter: String,
from_recurring: bool,
}
fn rule_active_in(rule: &RuleRecord, year: i32) -> bool {
let to = match rule.to {
YearBound::Year(y) => y,
YearBound::Max => i32::MAX,
};
rule.from <= year && year <= to
}
fn standard_letter(rules: &[RuleRecord]) -> String {
rules
.iter()
.find(|r| !r.save.is_dst)
.map(|r| r.letter.clone())
.unwrap_or_default()
}
fn build_recurring_footer(
zone: &ZoneRecord,
era: &ZoneEra,
stdoff: i32,
rules: &[RuleRecord],
) -> Result<(String, u8)> {
let perpetual: Vec<&RuleRecord> = rules
.iter()
.filter(|r| matches!(r.to, YearBound::Max))
.collect();
let dst_rules: Vec<&&RuleRecord> = perpetual.iter().filter(|r| r.save.is_dst).collect();
let std_rules: Vec<&&RuleRecord> = perpetual.iter().filter(|r| !r.save.is_dst).collect();
if dst_rules.len() != 1 || std_rules.len() != 1 {
return Err(unsupported(
zone,
"recurring footer needs exactly one perpetual DST rule and one standard rule",
));
}
let dst = dst_rules[0];
let std = std_rules[0];
let dst_save = dst.save.seconds;
let std_utoff = stdoff;
let dst_utoff = stdoff + dst_save;
let std_abbr = render(&era.format, &std.letter, false, std_utoff);
let dst_abbr = render(&era.format, &dst.letter, true, dst_utoff);
let dst_at_wall = at_to_wall(dst.at, stdoff, 0);
let std_at_wall = at_to_wall(std.at, stdoff, dst_save);
let rec = Recurring {
std_abbr: &std_abbr,
std_utoff,
dst_abbr: &dst_abbr,
dst_utoff,
dst_month: dst.in_month,
dst_on: dst.on,
dst_at_wall,
std_month: std.in_month,
std_on: std.on,
std_at_wall,
};
posix_footer::recurring(&rec).ok_or_else(|| {
unsupported(
zone,
"recurring rule day form is not POSIX-expressible (only nth/last weekday) — \
refusing rather than emitting an approximate footer",
)
})
}
fn at_to_wall(at: TimeOfDay, stdoff: i32, save_before: i32) -> i32 {
match at.reference {
TimeRef::Wall => at.seconds,
TimeRef::Standard => at.seconds + save_before,
TimeRef::Universal => at.seconds + stdoff + save_before,
}
}
fn intern(types: &mut Vec<LocalTimeType>, ty: LocalTimeType) -> u8 {
if let Some(i) = types.iter().position(|t| *t == ty) {
return i as u8;
}
types.push(ty);
(types.len() - 1) as u8
}
fn expand_activations(
zone: &ZoneRecord,
rules: &[RuleRecord],
lo: i32,
hi: i32,
) -> Result<Vec<Activation>> {
let mut activations: Vec<Activation> = Vec::new();
for year in lo..=hi {
for rule in rules {
if !rule_active_in(rule, year) {
continue;
}
let day = resolve_on_day(rule.on, year, rule.in_month)
.map_err(|(code, msg)| located(zone, code, msg))?;
let local_seconds = days_from_civil(day.year, day.month, day.day as u8) * 86400
+ rule.at.seconds as i64;
activations.push(Activation {
local_seconds,
at_ref: rule.at.reference,
save: rule.save.seconds,
is_dst: rule.save.is_dst,
letter: rule.letter.clone(),
from_recurring: matches!(rule.to, YearBound::Max),
});
}
}
activations.sort_by_key(|a| a.local_seconds);
Ok(activations)
}
#[derive(Clone)]
struct RunState {
save: i32,
is_dst: bool,
letter: String,
}
pub fn compile_multi_era(
zone: &ZoneRecord,
db: &Database,
opts: crate::EmitOptions,
) -> Result<TzifData> {
let mut types: Vec<LocalTimeType> = Vec::new();
let mut transitions: Vec<Transition> = Vec::new();
let mut from_recurring: Vec<bool> = Vec::new();
let mut last_ut = i64::MIN;
let first = &zone.eras[0];
let (first_rules, first_kind) = resolve_era_rules(zone, db, first)?;
validate_fixed_era_format(zone, first, &first_kind)?;
let initial = era_type(
first.stdoff.0,
&seed_run(&first_kind, first_rules),
&first.format,
);
let mut prevailing = intern(&mut types, initial);
let mut start_ut: Option<i64> = None;
let mut boundary_until: Option<(i64, TimeRef)> = None;
for era in &zone.eras {
let stdoff = era.stdoff.0;
let (rules, kind) = resolve_era_rules(zone, db, era)?;
validate_fixed_era_format(zone, era, &kind)?;
let is_fixed = !matches!(kind, EraKind::Ruled);
let recurring = rules.iter().any(|r| matches!(r.to, YearBound::Max));
let mut run = seed_run(&kind, rules);
if !is_fixed {
if let Some(s) = start_ut {
let from_lo = rules
.iter()
.map(|r| r.from)
.min()
.unwrap_or_else(|| year_of_unix(s));
for act in &expand_activations(zone, rules, from_lo, year_of_unix(s) + 1)? {
let ut = convert_at(act.local_seconds, stdoff, run.save, act.at_ref);
if ut <= s {
run = act.into();
} else {
break;
}
}
}
}
if era.until.is_none() && recurring {
let era_start_year = start_ut.map(year_of_unix);
let finite_active_in_era = rules.iter().any(|r| match r.to {
YearBound::Year(y) => match era_start_year {
Some(sy) => y >= sy,
None => true, },
YearBound::Max => false,
});
if !finite_active_in_era {
if let Some(s) = start_ut {
let sy = year_of_unix(s);
for act in &expand_activations(zone, rules, sy - 1, sy + 1)? {
let ut = convert_at(act.local_seconds, stdoff, run.save, act.at_ref);
if ut <= s {
run = act.into();
} else {
break;
}
}
if s > last_ut {
let ty = era_type(stdoff, &run, &era.format);
let idx = intern(&mut types, ty);
transitions.push(Transition {
at: s,
type_index: idx,
});
from_recurring.push(false); prevailing = idx;
last_ut = s;
}
}
let (mut footer, version) = build_recurring_footer(zone, era, stdoff, rules)?;
if let Some(range) = opts.range {
if let Some(s) = start_ut {
let sy = year_of_unix(s);
for act in &expand_activations(zone, rules, sy - 1, RECUR_HI)? {
let ut = convert_at(act.local_seconds, stdoff, run.save, act.at_ref);
run = act.into();
if ut <= s {
continue;
}
let ty = era_type(stdoff, &run, &era.format);
if push(
&mut types,
&mut transitions,
&mut prevailing,
&mut last_ut,
ut,
ty,
) {
from_recurring.push(true);
}
}
}
apply_range_to_stream(
&mut types,
&mut transitions,
&mut from_recurring,
&mut footer,
range,
);
finalize_emit_style(opts, &mut types, &mut transitions, &from_recurring);
}
ensure_strictly_increasing(&transitions, zone)?;
return Ok(TzifData {
types,
transitions,
footer,
version,
leaps: Vec::new(),
});
}
}
let until_local = match &era.until {
Some(u) => Some(until_local_seconds(zone, u)?),
None => None,
};
let lo = match start_ut {
Some(s) => year_of_unix(s) - 1,
None => rules.iter().map(|r| r.from).min().unwrap_or(0),
};
let last_finite = rules.iter().filter_map(|r| match r.to {
YearBound::Year(y) => Some(y),
YearBound::Max => None,
});
let hi = match &until_local {
Some((ul, _)) => year_of_unix(*ul) + 1,
None if recurring => RECUR_HI.max(last_finite.max().map_or(RECUR_HI, |y| y + 1)),
None => last_finite
.max()
.unwrap_or_else(|| year_of_unix(start_ut.unwrap_or(0))),
};
let acts = if is_fixed {
Vec::new()
} else {
expand_activations(zone, rules, lo, hi)?
};
let mut boundary_done = start_ut.is_none();
for act in &acts {
let ut = convert_at(act.local_seconds, stdoff, run.save, act.at_ref);
if let Some((ul, uref)) = until_local {
if ut >= convert_at(ul, stdoff, run.save, uref) {
break;
}
}
if let Some(s) = start_ut {
let boundary_coincident = match boundary_until {
Some((bl, br)) => {
act.local_seconds == bl
&& convert_at(bl, stdoff, run.save, br)
== convert_at(bl, stdoff, run.save, act.at_ref)
}
None => false,
};
if ut <= s || boundary_coincident {
run = act.into();
continue;
}
if !boundary_done {
let ty = era_type(stdoff, &run, &era.format);
if push(
&mut types,
&mut transitions,
&mut prevailing,
&mut last_ut,
s,
ty,
) {
from_recurring.push(false); }
boundary_done = true;
}
}
run = act.into();
let ty = era_type(stdoff, &run, &era.format);
if push(
&mut types,
&mut transitions,
&mut prevailing,
&mut last_ut,
ut,
ty,
) {
from_recurring.push(act.from_recurring);
}
}
if let Some(s) = start_ut {
if !boundary_done {
let ty = era_type(stdoff, &run, &era.format);
if push(
&mut types,
&mut transitions,
&mut prevailing,
&mut last_ut,
s,
ty,
) {
from_recurring.push(false); }
}
}
match until_local {
Some((ul, uref)) => {
start_ut = Some(convert_at(ul, stdoff, run.save, uref));
boundary_until = Some((ul, uref));
}
None => {
let (mut footer, version) = if recurring {
build_recurring_footer(zone, era, stdoff, rules)?
} else {
let t = &types[prevailing as usize];
(posix_footer::fixed_offset(&t.abbr, t.utoff), b'2')
};
if let Some(range) = opts.range {
apply_range_to_stream(
&mut types,
&mut transitions,
&mut from_recurring,
&mut footer,
range,
);
}
finalize_emit_style(opts, &mut types, &mut transitions, &from_recurring);
ensure_strictly_increasing(&transitions, zone)?;
return Ok(TzifData {
types,
transitions,
footer,
version,
leaps: Vec::new(),
});
}
}
}
Err(Error::message(
"multi-era zone has no terminating era (internal invariant violated)",
))
}
impl From<&Activation> for RunState {
fn from(a: &Activation) -> Self {
RunState {
save: a.save,
is_dst: a.is_dst,
letter: a.letter.clone(),
}
}
}
enum EraKind {
Literal,
InlineSave(Save),
Ruled,
}
fn resolve_era_rules<'a>(
zone: &ZoneRecord,
db: &'a Database,
era: &ZoneEra,
) -> Result<(&'a [RuleRecord], EraKind)> {
match &era.rules {
ZoneRules::None => Ok((&[], EraKind::Literal)),
ZoneRules::Save(save) => Ok((&[], EraKind::InlineSave(*save))),
ZoneRules::Named(name) => {
let rules = db
.rules
.get(name)
.ok_or_else(|| unsupported(zone, format!("unknown rule set {name:?}")))?;
Ok((rules.as_slice(), EraKind::Ruled))
}
}
}
fn seed_run(kind: &EraKind, rules: &[RuleRecord]) -> RunState {
match kind {
EraKind::Literal => RunState {
save: 0,
is_dst: false,
letter: String::new(),
},
EraKind::InlineSave(s) => RunState {
save: s.seconds,
is_dst: s.is_dst,
letter: String::new(),
},
EraKind::Ruled => RunState {
save: 0,
is_dst: false,
letter: standard_letter(rules),
},
}
}
fn validate_fixed_era_format(zone: &ZoneRecord, era: &ZoneEra, kind: &EraKind) -> Result<()> {
let fmt = &era.format;
match kind {
EraKind::Literal => {
if fmt.contains("%s") || fmt.contains('/') {
return Err(unsupported(
zone,
format!("FORMAT {fmt:?}: %s / STD-DST slash need rule context but the era has no rules"),
));
}
}
EraKind::InlineSave(_s) => {
if fmt.contains("%s") {
return Err(unsupported(
zone,
format!("inline-save FORMAT {fmt:?}: %s has no LETTER in an inline-save era"),
));
}
if fmt.contains('/') {
return Err(unsupported(
zone,
format!("inline-save FORMAT {fmt:?}: STD/DST slash form is not supported yet"),
));
}
}
EraKind::Ruled => {}
}
Ok(())
}
fn era_type(stdoff: i32, run: &RunState, format: &str) -> LocalTimeType {
let utoff = stdoff + run.save;
LocalTimeType {
utoff,
is_dst: run.is_dst,
abbr: render(format, &run.letter, run.is_dst, utoff),
}
}
fn convert_at(local: i64, stdoff: i32, save: i32, reference: TimeRef) -> i64 {
match reference {
TimeRef::Wall => local - stdoff as i64 - save as i64,
TimeRef::Standard => local - stdoff as i64,
TimeRef::Universal => local,
}
}
fn until_local_seconds(zone: &ZoneRecord, until: &Until) -> Result<(i64, TimeRef)> {
let day = resolve_on_day(until.day, until.year, until.month)
.map_err(|(code, msg)| located(zone, code, msg))?;
let local =
days_from_civil(day.year, day.month, day.day as u8) * 86400 + until.time.seconds as i64;
Ok((local, until.time.reference))
}
fn push(
types: &mut Vec<LocalTimeType>,
transitions: &mut Vec<Transition>,
prevailing: &mut u8,
last_ut: &mut i64,
at: i64,
ty: LocalTimeType,
) -> bool {
let idx = intern(types, ty);
if idx == *prevailing {
return false; }
if at <= *last_ut {
return false; }
transitions.push(Transition {
at,
type_index: idx,
});
*prevailing = idx;
*last_ut = at;
true
}
fn finalize_emit_style(
opts: crate::EmitOptions,
types: &mut Vec<LocalTimeType>,
transitions: &mut Vec<Transition>,
from_recurring: &[bool],
) {
if !matches!(opts.style, crate::EmitStyle::ZicSlim) {
return;
}
debug_assert_eq!(transitions.len(), from_recurring.len());
let non_tz = transitions
.iter()
.zip(from_recurring)
.filter(|(_, &rec)| !rec)
.map(|(t, _)| t.at)
.max();
let tz_start = match non_tz {
Some(n) => transitions.iter().map(|t| t.at).filter(|&a| a > n).min(),
None => transitions.first().map(|t| t.at),
};
let Some(mut keep_at_max) = tz_start else {
return; };
if let Some(hi) = opts.redundant_until {
keep_at_max = keep_at_max.max(hi);
}
let before = transitions.len();
transitions.retain(|t| t.at <= keep_at_max);
if transitions.len() != before {
prune_types(types, transitions);
}
}
fn unspecified_type() -> LocalTimeType {
LocalTimeType {
utoff: 0,
is_dst: false,
abbr: "-00".to_string(),
}
}
pub(crate) fn apply_range(data: &mut TzifData, range: crate::RangeSpec) {
let mut from_recurring = vec![false; data.transitions.len()];
apply_range_to_stream(
&mut data.types,
&mut data.transitions,
&mut from_recurring,
&mut data.footer,
range,
);
}
fn apply_range_to_stream(
types: &mut Vec<LocalTimeType>,
transitions: &mut Vec<Transition>,
from_recurring: &mut Vec<bool>,
footer: &mut String,
range: crate::RangeSpec,
) {
debug_assert_eq!(transitions.len(), from_recurring.len());
if let Some(lo) = range.lo {
let prevailing_old = transitions
.iter()
.rev()
.find(|t| t.at <= lo)
.map(|t| t.type_index)
.unwrap_or(0);
let mut new_types = Vec::with_capacity(types.len() + 1);
new_types.push(unspecified_type());
new_types.append(types);
*types = new_types;
let prevailing = prevailing_old + 1;
let mut kept_t = vec![Transition {
at: lo,
type_index: prevailing,
}];
let mut kept_fr = vec![false]; for (t, &fr) in transitions.iter().zip(from_recurring.iter()) {
if t.at > lo {
kept_t.push(Transition {
at: t.at,
type_index: t.type_index + 1,
});
kept_fr.push(fr);
}
}
*transitions = kept_t;
*from_recurring = kept_fr;
}
if let Some(hi) = range.hi {
let unspec_idx = if range.lo.is_some() {
0u8 } else {
types.push(unspecified_type());
(types.len() - 1) as u8
};
let mut kept_t = Vec::new();
let mut kept_fr = Vec::new();
for (t, &fr) in transitions.iter().zip(from_recurring.iter()) {
if t.at < hi {
kept_t.push(*t);
kept_fr.push(fr);
}
}
kept_t.push(Transition {
at: hi,
type_index: unspec_idx,
});
kept_fr.push(false);
*transitions = kept_t;
*from_recurring = kept_fr;
footer.clear();
}
if range.lo.is_some() || range.hi.is_some() {
prune_types(types, transitions);
debug_assert!(
types.iter().any(|t| t.abbr == "-00"),
"range truncation must retain the `-00` unspecified type"
);
}
}
fn prune_types(types: &mut Vec<LocalTimeType>, transitions: &mut [Transition]) {
let mut used = vec![false; types.len()];
if !used.is_empty() {
used[0] = true;
}
for t in transitions.iter() {
used[t.type_index as usize] = true;
}
let mut remap = vec![0u8; types.len()];
let mut kept: Vec<LocalTimeType> = Vec::new();
for (i, ty) in types.iter().enumerate() {
if used[i] {
remap[i] = kept.len() as u8;
kept.push(ty.clone());
}
}
for t in transitions.iter_mut() {
t.type_index = remap[t.type_index as usize];
}
*types = kept;
}
fn unsupported(zone: &ZoneRecord, msg: impl Into<String>) -> Error {
located(zone, DiagnosticCode::UnsupportedDirective, msg)
}
fn located(zone: &ZoneRecord, code: DiagnosticCode, msg: impl Into<String>) -> Error {
Error::from(Diagnostic::error(
code,
msg,
&zone.origin.file,
zone.origin.line,
))
}