use crate::error::{Error, Result};
use crate::model::LeapTable;
use crate::tzif::{LeapRecord, LocalTimeType, Transition, TzifData};
use crate::RangeSpec;
const SECS_PER_DAY: i64 = 86_400;
pub fn apply_leaps(data: &mut TzifData, table: &LeapTable, range: Option<RangeSpec>) -> Result<()> {
if range.is_some() && (table.expires.is_some() || !table.entries.is_empty()) {
if table.entries.iter().any(|e| e.rolling) {
return Err(Error::config(
"Rolling leap seconds are not supported with -r (range truncation)",
));
}
return Err(Error::message(
"leap-second compilation with -r (range truncation) is not yet implemented",
));
}
let mut out = Vec::with_capacity(table.entries.len());
let mut last: i32 = 0; let mut prevtrans: i64 = 0; for e in &table.entries {
if e.trans - prevtrans < 28 * SECS_PER_DAY {
return Err(Error::message("Leap seconds too close together"));
}
prevtrans = e.trans;
let adjusted = e.trans + last as i64; let corr = last + e.correction; let trans = if e.rolling {
adjusted - utoff_in_effect(&data.transitions, &data.types, adjusted) as i64
} else {
adjusted
};
out.push(LeapRecord { trans, corr });
last += e.correction;
}
if let Some(raw_expires) = table.expires {
let adj_expires = raw_expires + last as i64;
if let Some(prev) = out.last() {
if prev.trans >= adj_expires {
return Err(Error::message(
"last Leap time does not precede Expires time",
));
}
}
out.push(LeapRecord {
trans: adj_expires,
corr: last, });
data.version = b'4';
}
if !data.transitions.is_empty() && !table.entries.is_empty() {
for tr in &mut data.transitions {
let corr: i32 = table
.entries
.iter()
.filter(|e| e.trans <= tr.at)
.map(|e| e.correction)
.sum();
tr.at += corr as i64;
}
}
data.leaps = out;
Ok(())
}
fn utoff_in_effect(transitions: &[Transition], types: &[LocalTimeType], t: i64) -> i32 {
if let Some(tr) = transitions.iter().rev().find(|tr| tr.at <= t) {
types[tr.type_index as usize].utoff
} else {
types
.iter()
.find(|ty| !ty.is_dst)
.or_else(|| types.first())
.map(|ty| ty.utoff)
.unwrap_or(0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{LeapSecond, LeapTable};
use crate::tzif::TzifData;
fn stationary(trans: i64) -> LeapSecond {
LeapSecond {
trans,
correction: 1,
rolling: false,
}
}
#[test]
fn cumulative_corrections_and_shifted_trans() {
let t = LeapTable {
entries: vec![
stationary(78_796_800), stationary(94_694_400), stationary(126_230_400), ],
expires: None,
};
let mut d = TzifData::fixed(0, "UTC", "");
apply_leaps(&mut d, &t, None).unwrap();
assert_eq!(d.leaps.len(), 3);
assert_eq!(d.leaps[0].corr, 1);
assert_eq!(d.leaps[1].corr, 2);
assert_eq!(d.leaps[2].corr, 3);
assert_eq!(d.leaps[0].trans, 78_796_800);
assert_eq!(d.leaps[1].trans, 94_694_400 + 1);
assert_eq!(d.leaps[2].trans, 126_230_400 + 2);
assert!(d.transitions.is_empty());
assert_eq!(d.version, b'2');
}
#[test]
fn right_profile_shifts_transitions_by_cumulative_leap_correction() {
let leaps = LeapTable {
entries: vec![stationary(3_000_000), stationary(6_000_000)], expires: None,
};
let mut d = TzifData::fixed(0, "X", ""); d.transitions = vec![
crate::tzif::Transition {
at: 1_000_000,
type_index: 0,
}, crate::tzif::Transition {
at: 4_000_000,
type_index: 0,
}, crate::tzif::Transition {
at: 9_000_000,
type_index: 0,
}, ];
apply_leaps(&mut d, &leaps, None).unwrap();
assert_eq!(
d.transitions[0].at, 1_000_000,
"pre-leap transition is unshifted"
);
assert_eq!(d.transitions[1].at, 4_000_000 + 1, "one leap precedes → +1");
assert_eq!(d.transitions[2].at, 9_000_000 + 2, "two leaps precede → +2");
assert_eq!(d.leaps.len(), 2); }
#[test]
fn rolling_subtracts_offset_in_effect() {
let raw = 1_483_228_800; let mut roll = TzifData::fixed(18_000, "E5T", ""); apply_leaps(
&mut roll,
&LeapTable {
entries: vec![LeapSecond {
trans: raw,
correction: 1,
rolling: true,
}],
expires: None,
},
None,
)
.unwrap();
assert_eq!(roll.leaps[0].trans, raw - 18_000, "Rolling = local-wall");
assert_eq!(roll.leaps[0].corr, 1);
let mut stat = TzifData::fixed(18_000, "E5T", "");
apply_leaps(
&mut stat,
&LeapTable {
entries: vec![stationary(raw)],
expires: None,
},
None,
)
.unwrap();
assert_eq!(stat.leaps[0].trans, raw, "Stationary = UT-as-written");
assert_eq!(stat.leaps[0].trans - roll.leaps[0].trans, 18_000);
}
#[test]
fn rolling_with_range_fails_closed() {
let mut d = TzifData::fixed(0, "UTC", "");
let table = LeapTable {
entries: vec![LeapSecond {
trans: 78_796_800,
correction: 1,
rolling: true,
}],
expires: None,
};
assert!(apply_leaps(&mut d.clone(), &table, None).is_ok());
let range = Some(RangeSpec {
lo: Some(0),
hi: None,
});
assert!(
apply_leaps(&mut d, &table, range).is_err(),
"Rolling + -r is a hard error"
);
}
#[test]
fn expires_appends_noop_and_sets_v4() {
let mut d = TzifData::fixed(0, "UTC", "");
let raw_leap = 78_796_800; let raw_exp = 1_700_000_000;
apply_leaps(
&mut d,
&LeapTable {
entries: vec![stationary(raw_leap)],
expires: Some(raw_exp),
},
None,
)
.unwrap();
assert_eq!(d.version, b'4', "Expires is content-triggered v4");
assert_eq!(d.leaps.len(), 2, "one real leap + the no-op expiry marker");
assert_eq!(d.leaps[1].trans, raw_exp + 1);
assert_eq!(d.leaps[1].corr, d.leaps[0].corr, "no-op: corr unchanged");
assert_eq!(d.leaps[1].corr, 1);
}
#[test]
fn expires_before_last_leap_rejected() {
let mut d = TzifData::fixed(0, "UTC", "");
let t = LeapTable {
entries: vec![stationary(1_500_000_000)],
expires: Some(1_400_000_000), };
assert!(apply_leaps(&mut d, &t, None).is_err());
}
#[test]
fn too_close_rejected() {
let t = LeapTable {
entries: vec![stationary(78_796_800), stationary(78_796_800 + 86_400)],
expires: None,
};
let mut d = TzifData::fixed(0, "UTC", "");
assert!(apply_leaps(&mut d, &t, None).is_err());
}
}