#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
pub mod csv_enrich;
pub mod interpret;
#[cfg(feature = "leap")]
pub mod leap;
#[cfg(feature = "lunisolar")]
pub mod lunisolar;
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("unknown timezone: {0} (expected UTC, a fixed offset like +08:00, or an IANA name like America/New_York)")]
UnknownZone(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())
}
#[must_use]
pub fn render(self, zone: &RenderZone) -> Option<String> {
let ts = jiff::Timestamp::from_nanosecond(self.0).ok()?;
match zone {
RenderZone::Utc => Some(ts.to_string()),
RenderZone::Fixed(offset) => Some(ts.display_with_offset(*offset).to_string()),
RenderZone::Named(tz) => {
let offset = tz.to_offset(ts);
Some(ts.display_with_offset(offset).to_string())
}
}
}
}
#[derive(Debug, Clone)]
pub enum RenderZone {
Utc,
Fixed(jiff::tz::Offset),
Named(jiff::tz::TimeZone),
}
impl RenderZone {
pub fn parse(spec: &str) -> Result<Self, ChronoError> {
let s = spec.trim();
if s.is_empty() || s.eq_ignore_ascii_case("utc") || s.eq_ignore_ascii_case("z") {
return Ok(Self::Utc);
}
if matches!(s.as_bytes().first(), Some(b'+' | b'-')) {
return parse_offset(s)
.map(Self::Fixed)
.ok_or_else(|| ChronoError::UnknownZone(s.to_string()));
}
jiff::tz::TimeZone::get(s)
.map(Self::Named)
.map_err(|_| ChronoError::UnknownZone(s.to_string()))
}
}
fn parse_offset(s: &str) -> Option<jiff::tz::Offset> {
let (sign, rest) = match s.as_bytes().first()? {
b'+' => (1i32, &s[1..]),
b'-' => (-1i32, &s[1..]),
_ => return None,
};
let digits: String = rest.chars().filter(|c| *c != ':').collect();
if !digits.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
let (hh, mm) = match digits.len() {
1 | 2 => (digits.parse::<i32>().ok()?, 0),
4 => (digits[..2].parse().ok()?, digits[2..].parse::<i32>().ok()?),
_ => return None,
};
if hh > 23 || mm > 59 {
return None;
}
jiff::tz::Offset::from_seconds(sign * (hh * 3600 + mm * 60)).ok()
}
#[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,
},
Embedded {
epoch_ns: i128,
shift_bits: u32,
unit: Unit,
},
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 {
#[must_use]
pub fn storage_bytes(&self) -> u8 {
match self.strategy {
Strategy::Packed(_) => 4,
Strategy::Embedded { .. } | Strategy::LinearFloat { .. } => 8,
Strategy::LinearInt { unit, .. } => match unit {
Unit::Seconds | Unit::Days => 4,
Unit::Millis | Unit::Micros | Unit::HundredNanos | Unit::Nanos => 8,
},
}
}
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::Embedded {
epoch_ns,
shift_bits,
unit,
} => {
let raw = u64::try_from(value).map_err(|_| ChronoError::OutOfRange {
what: "embedded-id (negative)",
value: i128::from(value),
})?;
let ticks = i128::from(raw >> shift_bits);
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::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::Embedded { .. } | 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::Embedded { .. } => 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()))
}