use crate::ChronoError;
use hifitime::Epoch;
#[derive(Debug, Clone, serde::Serialize)]
pub struct LeapReading {
pub scale: &'static str,
pub citation: &'static str,
pub utc_rfc3339: String,
pub assumptions: Vec<String>,
}
const SECS_1900_TO_1970: i64 = 2_208_988_800;
const TAI64_BASE: u64 = 1 << 62;
fn utc_rfc3339(epoch: Epoch) -> String {
let (y, mo, d, h, mi, s, ns) = epoch.to_gregorian_utc();
if ns == 0 {
format!("{y:04}-{mo:02}-{d:02}T{h:02}:{mi:02}:{s:02}Z")
} else {
format!("{y:04}-{mo:02}-{d:02}T{h:02}:{mi:02}:{s:02}.{ns:09}Z")
}
}
#[must_use]
pub fn from_gps_seconds(seconds: f64) -> LeapReading {
LeapReading {
scale: "GPS",
citation: "IS-GPS-200 (GPS system time; epoch 1980-01-06)",
utc_rfc3339: utc_rfc3339(Epoch::from_gpst_seconds(seconds)),
assumptions: vec![
"consistent with GPS system time — a reading, not a determination".to_string(),
"GPS↔UTC via the leap-second table; GPS itself has no leap seconds".to_string(),
],
}
}
pub fn from_tai64(label: u64) -> Result<LeapReading, ChronoError> {
let s = label
.checked_sub(TAI64_BASE)
.ok_or(ChronoError::OutOfRange {
what: "TAI64 label below 2^62 (pre-1970 era not supported)",
value: i128::from(label),
})?;
let tai_since_1970 = i64::try_from(s).map_err(|_| ChronoError::OutOfRange {
what: "TAI64 seconds offset too large",
value: i128::from(s),
})?;
let secs_since_1900 =
tai_since_1970
.checked_add(SECS_1900_TO_1970)
.ok_or(ChronoError::OutOfRange {
what: "TAI seconds overflow",
value: i128::from(tai_since_1970),
})?;
Ok(LeapReading {
scale: "TAI64",
citation: "D. J. Bernstein libtai TAI64 (label = 2^62 + TAI seconds since 1970)",
utc_rfc3339: utc_rfc3339(Epoch::from_tai_seconds(secs_since_1900 as f64)),
assumptions: vec![
"consistent with a TAI64 label — a reading, not a determination".to_string(),
"TAI→UTC applies the leap-second table (TAI−UTC = 37 s since 2017)".to_string(),
],
})
}
pub fn from_ntp_seconds(seconds: u64) -> Result<LeapReading, ChronoError> {
let unix = i64::try_from(seconds)
.map(|s| s - SECS_1900_TO_1970)
.map_err(|_| ChronoError::OutOfRange {
what: "NTP seconds too large",
value: i128::from(seconds),
})?;
let ts = jiff::Timestamp::from_second(unix).map_err(|e| ChronoError::Render(e.to_string()))?;
Ok(LeapReading {
scale: "NTP",
citation: "RFC 5905 (NTPv4; prime epoch 1900-01-01)",
utc_rfc3339: ts.to_string(),
assumptions: vec![
"consistent with an NTP timestamp — a reading, not a determination".to_string(),
"NTP follows UTC (no leap-second counting); conversion is additive".to_string(),
"era 0 assumed — the 32-bit seconds field wraps in 2036; RFC 5905's \
128-bit date format carries an explicit era to disambiguate"
.to_string(),
],
})
}
#[must_use]
pub fn decode(id: &str, value: i64) -> Option<Result<LeapReading, ChronoError>> {
match id {
"gps" => Some(Ok(from_gps_seconds(value as f64))),
"tai64" => Some(
u64::try_from(value)
.map_err(|_| ChronoError::OutOfRange {
what: "TAI64 label (negative)",
value: i128::from(value),
})
.and_then(from_tai64),
),
"ntp" => Some(
u64::try_from(value)
.map_err(|_| ChronoError::OutOfRange {
what: "NTP seconds (negative)",
value: i128::from(value),
})
.and_then(from_ntp_seconds),
),
_ => None,
}
}