#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
pub mod csv_enrich;
pub mod interpret;
#[cfg(feature = "leap")]
pub mod leap;
pub mod registry;
#[derive(Debug, thiserror::Error)]
pub enum ChronoError {
#[error("value out of representable range ({what}): {value}")]
OutOfRange {
what: &'static str,
value: i128,
},
#[error("unknown format id: {0}")]
UnknownFormat(String),
#[error("cannot render instant: {0}")]
Render(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
pub struct PosixNs(pub i128);
impl PosixNs {
pub const UNIX_EPOCH: Self = Self(0);
#[must_use]
pub fn to_rfc3339(self) -> Option<String> {
jiff::Timestamp::from_nanosecond(self.0)
.ok()
.map(|ts| ts.to_string())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Unit {
Seconds,
Millis,
Micros,
HundredNanos,
Nanos,
Days,
}
impl Unit {
#[must_use]
pub const fn nanos(self) -> i128 {
match self {
Self::Seconds => 1_000_000_000,
Self::Millis => 1_000_000,
Self::Micros => 1_000,
Self::HundredNanos => 100,
Self::Nanos => 1,
Self::Days => 86_400 * 1_000_000_000,
}
}
#[must_use]
pub const fn sub_second_digits(self) -> u32 {
match self {
Self::Seconds | Self::Days => 0,
Self::Millis => 3,
Self::Micros => 6,
Self::HundredNanos => 7,
Self::Nanos => 9,
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum Strategy {
LinearInt {
epoch_ns: i128,
unit: Unit,
},
LinearFloat {
epoch_ns: i128,
unit: Unit,
},
EmbeddedMillis {
epoch_ns: i128,
shift_bits: u32,
},
Packed(fn(i64) -> Result<PosixNs, ChronoError>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TzSemantics {
Utc,
LocalNaive,
OffsetEmbedded,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LeapSemantics {
PosixIgnored,
LeapAware,
}
#[derive(Debug, Clone, Copy)]
pub struct Format {
pub id: &'static str,
pub label: &'static str,
pub family: &'static str,
pub strategy: Strategy,
pub citation: &'static str,
pub tz: TzSemantics,
pub leap: LeapSemantics,
pub plausible: (i128, i128),
}
impl Format {
pub fn decode_int(&self, value: i64) -> Result<PosixNs, ChronoError> {
match self.strategy {
Strategy::LinearInt { epoch_ns, unit } => {
let ticks = i128::from(value);
let ns = ticks
.checked_mul(unit.nanos())
.and_then(|t| t.checked_add(epoch_ns))
.ok_or(ChronoError::OutOfRange {
what: "nanoseconds",
value: ticks,
})?;
Ok(PosixNs(ns))
}
Strategy::EmbeddedMillis {
epoch_ns,
shift_bits,
} => {
let raw = u64::try_from(value).map_err(|_| ChronoError::OutOfRange {
what: "embedded-id (negative)",
value: i128::from(value),
})?;
let ms = i128::from(raw >> shift_bits);
let ns = ms
.checked_mul(Unit::Millis.nanos())
.and_then(|t| t.checked_add(epoch_ns))
.ok_or(ChronoError::OutOfRange {
what: "nanoseconds",
value: ms,
})?;
Ok(PosixNs(ns))
}
Strategy::Packed(decode) => decode(value),
Strategy::LinearFloat { .. } => Err(ChronoError::OutOfRange {
what: "float-format decoded as integer",
value: i128::from(value),
}),
}
}
pub fn decode_float(&self, value: f64) -> Result<PosixNs, ChronoError> {
match self.strategy {
Strategy::LinearFloat { epoch_ns, unit } => {
if !value.is_finite() {
return Err(ChronoError::OutOfRange {
what: "non-finite float value",
value: 0,
});
}
let scaled = (value * unit.nanos() as f64).round();
if !scaled.is_finite() || scaled.abs() >= 1.0e38 {
return Err(ChronoError::OutOfRange {
what: "float value out of representable range",
value: 0,
});
}
let ns = (scaled as i128)
.checked_add(epoch_ns)
.ok_or(ChronoError::OutOfRange {
what: "nanoseconds",
value: scaled as i128,
})?;
Ok(PosixNs(ns))
}
Strategy::LinearInt { .. } | Strategy::EmbeddedMillis { .. } | Strategy::Packed(_) => {
Err(ChronoError::OutOfRange {
what: "integer format decoded as float",
value: 0,
})
}
}
}
pub fn encode_int(&self, instant: PosixNs) -> Result<i64, ChronoError> {
match self.strategy {
Strategy::LinearInt { epoch_ns, unit } => {
let rel = instant
.0
.checked_sub(epoch_ns)
.ok_or(ChronoError::OutOfRange {
what: "nanoseconds",
value: instant.0,
})?;
let ticks = rel / unit.nanos();
i64::try_from(ticks).map_err(|_| ChronoError::OutOfRange {
what: "ticks",
value: ticks,
})
}
Strategy::LinearFloat { .. } => Err(ChronoError::OutOfRange {
what: "float-format encoded as integer",
value: 0,
}),
Strategy::EmbeddedMillis { .. } => Err(ChronoError::OutOfRange {
what: "embedded-id format cannot be re-encoded from an instant",
value: 0,
}),
Strategy::Packed(_) => Err(ChronoError::OutOfRange {
what: "packed format cannot be re-encoded from an instant",
value: 0,
}),
}
}
}
pub fn format(id: &str) -> Result<&'static Format, ChronoError> {
registry::FORMATS
.iter()
.find(|f| f.id == id)
.ok_or_else(|| ChronoError::UnknownFormat(id.to_string()))
}