#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
pub mod cal;
#[cfg(feature = "lunisolar")]
pub mod cal_art;
pub mod cal_color;
pub mod cal_render;
pub mod calfmt;
pub mod carve;
pub mod compose;
#[cfg(feature = "csv")]
pub mod csv_enrich;
pub mod datefmt;
pub use datefmt::DateStyle;
#[cfg(feature = "holiday")]
pub mod holiday;
pub mod interpret;
pub mod localzone;
pub mod mcp;
pub use localzone::{resolve_local, LocalResolution};
#[cfg(feature = "leap")]
pub mod leap;
#[cfg(feature = "lunisolar")]
pub mod lunisolar;
pub mod registry;
pub mod scan;
pub mod secs;
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
#[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 unix_seconds(self) -> i64 {
self.0.div_euclid(1_000_000_000) as i64
}
#[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()
}
pub use forensicnomicon::temporal_formats::{
Encoding, LeapSemantics, PackedLayout, TimeFormat, TzSemantics, Unit,
};
#[derive(Debug, Clone, Copy)]
pub(crate) struct PackedCodec {
pub(crate) decode: fn(i64) -> Result<PosixNs, ChronoError>,
pub(crate) encode: Option<fn(PosixNs) -> Result<i64, ChronoError>>,
}
#[derive(Debug, Clone, Copy)]
pub struct Format {
pub meta: &'static TimeFormat,
pub(crate) packed: Option<PackedCodec>,
}
impl std::ops::Deref for Format {
type Target = TimeFormat;
fn deref(&self) -> &Self::Target {
self.meta
}
}
impl Format {
#[must_use]
pub fn storage_bytes(&self) -> u8 {
match self.meta.encoding {
Encoding::Packed(_) => 4,
Encoding::Embedded { .. } | Encoding::LinearFloat { .. } => 8,
Encoding::LinearInt { unit, .. } => match unit {
Unit::Seconds | Unit::Days => 4,
Unit::CentiSecond
| Unit::Millis
| Unit::Micros
| Unit::HundredNanos
| Unit::Nanos => 8,
},
}
}
pub fn decode_int(&self, value: i64) -> Result<PosixNs, ChronoError> {
match self.meta.encoding {
Encoding::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))
}
Encoding::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))
}
Encoding::Packed(_) => (self.packed_codec()?.decode)(value),
Encoding::LinearFloat { .. } => Err(ChronoError::OutOfRange {
what: "float-format decoded as integer",
value: i128::from(value),
}),
}
}
fn packed_codec(&self) -> Result<PackedCodec, ChronoError> {
self.packed.ok_or(ChronoError::OutOfRange {
what: "packed format has no engine codec (internal invariant broken)",
value: 0,
})
}
pub fn decode_float(&self, value: f64) -> Result<PosixNs, ChronoError> {
match self.meta.encoding {
Encoding::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))
}
Encoding::LinearInt { .. } | Encoding::Embedded { .. } | Encoding::Packed(_) => {
Err(ChronoError::OutOfRange {
what: "integer format decoded as float",
value: 0,
})
}
}
}
pub fn encode_int(&self, instant: PosixNs) -> Result<i64, ChronoError> {
match self.meta.encoding {
Encoding::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,
})
}
Encoding::LinearFloat { .. } => Err(ChronoError::OutOfRange {
what: "float-format encoded as integer",
value: 0,
}),
Encoding::Embedded {
epoch_ns,
shift_bits,
unit,
} => {
let rel = instant
.0
.checked_sub(epoch_ns)
.ok_or(ChronoError::OutOfRange {
what: "nanoseconds",
value: instant.0,
})?;
let shifted = (rel / unit.nanos()) << shift_bits;
i64::try_from(shifted).map_err(|_| ChronoError::OutOfRange {
what: "embedded-id ticks",
value: shifted,
})
}
Encoding::Packed(_) => match self.packed_codec()?.encode {
Some(f) => f(instant),
None => Err(ChronoError::OutOfRange {
what: "packed format cannot yet be re-encoded from an instant",
value: 0,
}),
},
}
}
pub fn encode_float(&self, instant: PosixNs) -> Result<f64, ChronoError> {
match self.meta.encoding {
Encoding::LinearFloat { epoch_ns, unit } => {
let rel = instant.0 - epoch_ns;
Ok(rel as f64 / unit.nanos() as f64)
}
_ => Err(ChronoError::OutOfRange {
what: "non-float format encoded as a float",
value: 0,
}),
}
}
pub fn encode(&self, instant: PosixNs) -> Result<Encoded, ChronoError> {
match self.meta.encoding {
Encoding::LinearFloat { .. } => self.encode_float(instant).map(Encoded::Float),
_ => self.encode_int(instant).map(Encoded::Int),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Encoded {
Int(i64),
Float(f64),
}
impl std::fmt::Display for Encoded {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Encoded::Int(i) => write!(f, "{i}"),
Encoded::Float(x) => write!(f, "{x}"),
}
}
}
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()))
}
#[must_use]
pub fn registry_digest() -> String {
let mut hash: u64 = 0xcbf2_9ce4_8422_2325; for f in forensicnomicon::temporal_formats::TIME_FORMATS {
for byte in f.id.bytes().chain(f.citation.bytes()) {
hash ^= u64::from(byte);
hash = hash.wrapping_mul(0x0000_0100_0000_01b3); }
}
format!("{hash:016x}")
}