use crate::util::DisplayBuffer;
use chrono::{DateTime, NaiveDate, NaiveDateTime, TimeDelta, Utc};
use std::fmt::Write;
#[derive(Copy, Hash)]
#[derive_const(Clone, Default, PartialEq, Eq, PartialOrd, Ord)]
#[repr(transparent)]
pub struct LongDateTime(i64);
const EPOCH_NAIVE: NaiveDateTime =
NaiveDate::from_ymd_opt(1904, 1, 1).unwrap().and_hms_opt(0, 0, 0).unwrap();
impl LongDateTime {
pub const EPOCH: DateTime<Utc> = EPOCH_NAIVE.and_utc();
pub const MIN: Self = Self(i64::MIN);
pub const MAX: Self = Self(i64::MAX);
pub const fn new(datetime: DateTime<Utc>) -> Self {
let delta = datetime.naive_utc().signed_duration_since(EPOCH_NAIVE);
Self::from_epoch_seconds(delta.num_seconds())
}
pub const fn datetime(&self) -> Option<DateTime<Utc>> {
let delta = TimeDelta::try_seconds(self.epoch_seconds())?;
Some(EPOCH_NAIVE.checked_add_signed(delta)?.and_utc())
}
pub const fn from_epoch_seconds(secs: i64) -> Self {
Self(secs)
}
pub const fn epoch_seconds(self) -> i64 {
self.0
}
pub const fn ymd_hms_utc(&self) -> (i64, u8, u8, u8, u8, u8) {
const SECS_PER_DAY: i64 = 24 * 60 * 60;
let epoch_days = self.0.div_euclid(SECS_PER_DAY);
let secs = self.0.rem_euclid(SECS_PER_DAY);
let (year, month, day) = ymd_from_days(epoch_days - 24107);
const fn ymd_from_days(unix_days: i64) -> (i64, u8, u8) {
let z = unix_days + 719468;
let era = if z >= 0 { z } else { z - 146096 } / 146097;
let doe = (z - era * 146097) as u64;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
let y = yoe as i64 + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
(y + (m <= 2) as i64, m as u8, d as u8)
}
(year, month, day, (secs / 3600) as u8, ((secs % 3600) / 60) as u8, (secs % 60) as u8)
}
pub const fn from_be_bytes(bytes: [u8; 8]) -> Self {
Self(i64::from_be_bytes(bytes))
}
pub const fn to_be_bytes(self) -> [u8; 8] {
self.0.to_be_bytes()
}
}
const impl From<DateTime<Utc>> for LongDateTime {
fn from(value: DateTime<Utc>) -> Self {
Self::new(value)
}
}
const impl TryFrom<LongDateTime> for DateTime<Utc> {
type Error = ();
fn try_from(value: LongDateTime) -> Result<Self, Self::Error> {
value.datetime().ok_or(())
}
}
impl std::fmt::Display for LongDateTime {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
self._fmt_shared(false, f)
}
}
impl std::fmt::Debug for LongDateTime {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
self._fmt_shared(true, f)
}
}
impl LongDateTime {
fn _fmt_shared(&self, rfc_iso: bool, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let (year, month, day, hour, minute, second) = self.ymd_hms_utc();
let mut buf = DisplayBuffer::<32>::new();
if year > 9999 {
buf.write_byte_unchecked(b'+');
} else if year < 0 {
buf.write_byte_unchecked(b'-');
}
let year = year.unsigned_abs();
if matches!(year, 0..=9999) {
buf.write_two_digits_unchecked((year / 100) as u8);
buf.write_two_digits_unchecked((year % 100) as u8);
} else {
write!(buf, "{:04}", year)?;
}
buf.write_byte_unchecked(b'-');
buf.write_two_digits_unchecked(month);
buf.write_byte_unchecked(b'-');
buf.write_two_digits_unchecked(day);
buf.write_byte_unchecked(if rfc_iso { b'T' } else { b' ' });
buf.write_two_digits_unchecked(hour);
buf.write_byte_unchecked(b':');
buf.write_two_digits_unchecked(minute);
buf.write_byte_unchecked(b':');
buf.write_two_digits_unchecked(second);
if rfc_iso {
buf.write_byte_unchecked(b'Z');
} else {
buf.write_str_unchecked(" UTC");
}
f.write_str(buf.as_str())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn longdatetimes() {
let dates: [(u64, &'static str); _] = [
(0x8000000000000000, "-292277022723-01-25T08:29:52Z"),
(0xFFFFF00000000000, "-555571-10-24T14:19:44Z"),
(0xFFFFFF0000000000, "-32939-11-10T23:23:44Z"),
(0xFFFFFFF000000000, "-0274-05-13T16:27:44Z"),
(0xFFFFFFF200D00100, "-0001-01-01T00:00:00Z"),
(0xFFFFFFF202B13480, "0000-01-01T00:00:00Z"),
(0xFFFFFFF20493B980, "0001-01-01T00:00:00Z"),
(0x0000000000000000, "1904-01-01T00:00:00Z"),
(0x00000000E3D1B1DE, "2025-02-12T02:03:10Z"),
(0x00000000E6B686E6, "2026-08-28T00:29:26Z"),
(0x00000000FFFFFFFF, "2040-02-06T06:28:15Z"),
(0x000000FFFFFFFFFF, "+36746-02-19T00:36:15Z"),
(0x0000FFFFFFFFFFFF, "+8921490-12-06T10:44:15Z"),
(0x00FFFFFFFFFFFFFF, "+2283416158-11-23T12:52:15Z"),
(0x7FFFFFFFFFFFFFFF, "+292277026530-12-04T15:30:07Z"),
];
for (stamp, iso_date) in dates {
let stamp = LongDateTime::from_epoch_seconds(stamp as i64);
assert_eq!(format!("{:?}", stamp), iso_date);
if let Some(chrono_date) = stamp.datetime() {
assert_eq!(format!("{:?}", chrono_date), iso_date);
assert_eq!(stamp, LongDateTime::new(chrono_date));
}
}
}
}