const DAYS_PER_400_YEAR_ERAL: i64 = 146_097;
const DAYS_FROM_CIVIL_TO_UNIX_EPOCH: i64 = 719_468;
const YARS_PER_ERA: i64 = 400;
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct CivilDate {
pub year: i32,
pub month: u8,
pub day: u8,
}
impl CivilDate {
#[inline]
#[must_use]
pub const fn new(
year: i32,
month: u8,
day: u8,
) -> Self {
CivilDate { year, month, day }
}
#[inline]
#[must_use]
pub const fn days_from_unix(self) -> i64 {
days_from_unix_impl(self.year, self.month as i32, self.day as i32)
}
#[inline]
#[must_use]
pub const fn days_until(
self,
other: CivilDate,
) -> i64 {
other.days_from_unix() - self.days_from_unix()
}
#[inline]
#[must_use]
pub const fn seconds_until(
self,
other: CivilDate,
) -> i64 {
self.days_until(other) * 86_400
}
#[inline]
#[must_use]
pub const fn nanos_until(
self,
other: CivilDate,
) -> i64 {
self.seconds_until(other) * 1_000_000_000
}
}
const fn days_from_unix_impl(
y: i32,
m: i32,
d: i32,
) -> i64 {
let (y, m) = if m <= 2 { (y - 1, m + 9) } else { (y, m - 3) };
let y = y as i64;
let era = if y >= 0 {
y / YARS_PER_ERA
} else {
(y - (YARS_PER_ERA - 1)) / YARS_PER_ERA
};
let yoe = y - era * YARS_PER_ERA;
let doy = (153 * m as i64 + 2) / 5 + d as i64 - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
era * DAYS_PER_400_YEAR_ERAL + doe - DAYS_FROM_CIVIL_TO_UNIX_EPOCH
}
pub const TAI_EPOCH: CivilDate = CivilDate::new(1958, 1, 1);
pub const UNIX_EPOCH: CivilDate = CivilDate::new(1970, 1, 1);
pub const UTC_CIVIL_EPOCH: CivilDate = CivilDate::new(1972, 1, 1);
pub const GPS_EPOCH: CivilDate = CivilDate::new(1980, 1, 6);
pub const GLONASS_EPOCH: CivilDate = CivilDate::new(1996, 1, 1);
pub const GALILEO_EPOCH: CivilDate = CivilDate::new(1999, 8, 22);
pub const BEIDOU_EPOCH: CivilDate = CivilDate::new(2006, 1, 1);
pub const UTC_EPOCH_UNIX_OFFSET_S: i64 = UNIX_EPOCH.seconds_until(UTC_CIVIL_EPOCH);
pub const UTC_EPOCH_UNIX_OFFSET_NS: i64 = UTC_EPOCH_UNIX_OFFSET_S * 1_000_000_000;
pub const GPS_EPOCH_UNIX_S: i64 = UNIX_EPOCH.seconds_until(GPS_EPOCH);
pub const LEAP_SECONDS_AT_GPS_EPOCH: i64 = 19;
pub const LEAP_SECONDS_AT_GLONASS_EPOCH: i64 = 30;
pub const LEAP_SECONDS_AT_GALILEO_EPOCH: i64 = 32;
pub const LEAP_SECONDS_AT_BEIDOU_EPOCH: i64 = 33;
pub const DAYS_GPS_TO_GALILEO: i64 = GPS_EPOCH.days_until(GALILEO_EPOCH);
pub const DAYS_GPS_TO_BEIDOU: i64 = GPS_EPOCH.days_until(BEIDOU_EPOCH);
pub const DAYS_GPS_TO_GLONASS: i64 = GPS_EPOCH.days_until(GLONASS_EPOCH);
pub const DAYS_UNIX_TO_GPS: i64 = UNIX_EPOCH.days_until(GPS_EPOCH);
pub const NANOS_GPS_TO_GALILEO_EPOCH: i64 = GPS_EPOCH.nanos_until(GALILEO_EPOCH);
pub const NANOS_GPS_TO_BEIDOU_EPOCH_CALENDAR: i64 = GPS_EPOCH.nanos_until(BEIDOU_EPOCH);
const _VERIFY_GALILEO: () = {
let s = NANOS_GPS_TO_GALILEO_EPOCH / 1_000_000_000;
assert!(s == 619_315_200, "Galileo epoch offset check failed");
};
const _VERIFY_BEIDOU: () = {
let s = NANOS_GPS_TO_BEIDOU_EPOCH_CALENDAR / 1_000_000_000;
assert!(s == 820_108_800, "BeiDou epoch offset check failed");
};
const _VERIFY_GPS_UNIX: () = {
assert!(DAYS_UNIX_TO_GPS == 3657, "GPS Unix offset check failed");
};
const _VERIFY_GLONASS: () = {
assert!(
DAYS_GPS_TO_GLONASS == 5839,
"GLONASS epoch offset check failed"
);
};
const _VERIFY_GPS_UNIX_S: () = {
assert!(
GPS_EPOCH_UNIX_S == 315_964_800,
"GPS_EPOCH_UNIX_S must equal 315_964_800"
);
};
const _VERIFY_UTC_UNIX_OFFSET: () = {
assert!(
UTC_EPOCH_UNIX_OFFSET_S == 63_072_000,
"UTC_EPOCH_UNIX_OFFSET_S must equal 63_072_000 (730 days)"
);
};
const _VERIFY_UTC_UNIX_OFFSET_NS: () = {
assert!(
UTC_EPOCH_UNIX_OFFSET_NS == 63_072_000_000_000_000,
"UTC_EPOCH_UNIX_OFFSET_NS must equal 63_072_000_000_000_000"
);
};
impl core::fmt::Display for CivilDate {
fn fmt(
&self,
f: &mut core::fmt::Formatter<'_>,
) -> core::fmt::Result {
write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_unix_epoch_is_day_zero() {
assert_eq!(UNIX_EPOCH.days_from_unix(), 0);
}
#[test]
fn test_gps_epoch_is_3657_days_from_unix() {
assert_eq!(GPS_EPOCH.days_from_unix(), 3657);
}
#[test]
fn test_galileo_epoch_days_from_unix() {
assert_eq!(GALILEO_EPOCH.days_from_unix(), 10825);
}
#[test]
fn test_beidou_epoch_days_from_unix() {
assert_eq!(BEIDOU_EPOCH.days_from_unix(), 13149);
}
#[test]
fn test_glonass_epoch_days_from_unix() {
assert_eq!(GLONASS_EPOCH.days_from_unix(), 9496);
}
#[test]
fn test_gps_to_galileo_is_7168_days() {
assert_eq!(DAYS_GPS_TO_GALILEO, 7168);
}
#[test]
fn test_gps_to_beidou_is_9492_days() {
assert_eq!(DAYS_GPS_TO_BEIDOU, 9492);
}
#[test]
fn test_gps_to_glonass_is_5839_days() {
assert_eq!(DAYS_GPS_TO_GLONASS, 5839);
}
#[test]
fn test_galileo_minus_gps_is_619315200_seconds() {
assert_eq!(GPS_EPOCH.seconds_until(GALILEO_EPOCH), 619_315_200);
}
#[test]
fn test_beidou_minus_gps_calendar_is_820108800_seconds() {
assert_eq!(GPS_EPOCH.seconds_until(BEIDOU_EPOCH), 820_108_800);
}
#[test]
fn test_glonass_minus_gps_is_505123200_seconds() {
let expected = 5839_i64 * 86_400;
assert_eq!(GPS_EPOCH.seconds_until(GLONASS_EPOCH), expected);
}
#[test]
fn test_days_until_is_antisymmetric() {
let a = CivilDate::new(2000, 1, 1);
let b = CivilDate::new(2001, 1, 1);
assert_eq!(a.days_until(b), -b.days_until(a));
}
#[test]
fn test_days_until_self_is_zero() {
assert_eq!(GPS_EPOCH.days_until(GPS_EPOCH), 0);
}
#[test]
fn test_year_2000_is_leap_year() {
let feb29 = CivilDate::new(2000, 2, 29);
let mar01 = CivilDate::new(2000, 3, 1);
assert_eq!(feb29.days_until(mar01), 1);
}
#[test]
fn test_year_1900_is_not_leap_year() {
let feb28 = CivilDate::new(1900, 2, 28);
let mar01 = CivilDate::new(1900, 3, 1);
assert_eq!(feb28.days_until(mar01), 1);
}
#[test]
fn test_epoch_dates_are_correct() {
assert_eq!(GPS_EPOCH, CivilDate::new(1980, 1, 6));
assert_eq!(GLONASS_EPOCH, CivilDate::new(1996, 1, 1));
assert_eq!(GALILEO_EPOCH, CivilDate::new(1999, 8, 22));
assert_eq!(BEIDOU_EPOCH, CivilDate::new(2006, 1, 1));
assert_eq!(TAI_EPOCH, CivilDate::new(1958, 1, 1));
assert_eq!(UNIX_EPOCH, CivilDate::new(1970, 1, 1));
}
#[test]
fn test_leap_seconds_at_epochs_match_official_values() {
assert_eq!(LEAP_SECONDS_AT_GPS_EPOCH, 19);
assert_eq!(LEAP_SECONDS_AT_GLONASS_EPOCH, 30);
assert_eq!(LEAP_SECONDS_AT_BEIDOU_EPOCH, 33);
}
#[test]
fn test_nanos_gps_to_galileo_matches_known_value() {
assert_eq!(NANOS_GPS_TO_GALILEO_EPOCH, 619_315_200_000_000_000_i64);
}
#[test]
fn test_nanos_gps_to_beidou_calendar_matches_known_value() {
assert_eq!(
NANOS_GPS_TO_BEIDOU_EPOCH_CALENDAR,
820_108_800_000_000_000_i64
);
}
#[test]
fn test_utc_epoch_unix_offset_is_63072000_seconds() {
assert_eq!(UTC_EPOCH_UNIX_OFFSET_S, 63_072_000);
}
#[test]
fn test_utc_epoch_unix_offset_is_730_days() {
assert_eq!(UTC_EPOCH_UNIX_OFFSET_S / 86_400, 730);
}
#[test]
fn test_utc_epoch_unix_offset_matches_calendar() {
assert_eq!(
UNIX_EPOCH.seconds_until(UTC_CIVIL_EPOCH),
UTC_EPOCH_UNIX_OFFSET_S
);
}
#[test]
fn test_utc_epoch_unix_offset_ns_is_correct() {
assert_eq!(UTC_EPOCH_UNIX_OFFSET_NS, 63_072_000_000_000_000_i64);
}
#[test]
fn test_gps_epoch_unix_s_is_315964800() {
assert_eq!(GPS_EPOCH_UNIX_S, 315_964_800);
}
#[test]
fn test_gps_epoch_unix_s_is_3657_days() {
assert_eq!(GPS_EPOCH_UNIX_S / 86_400, 3657);
}
#[test]
fn test_gps_epoch_unix_s_matches_calendar() {
assert_eq!(UNIX_EPOCH.seconds_until(GPS_EPOCH), GPS_EPOCH_UNIX_S);
}
#[test]
fn test_unix_before_utc_epoch_is_negative_in_utc() {
let unix_s: i64 = 0;
let utc_from_1972 = unix_s + UTC_EPOCH_UNIX_OFFSET_S; let utc_from_1972_correct = unix_s - UTC_EPOCH_UNIX_OFFSET_S;
assert!(utc_from_1972_correct < 0);
let _ = utc_from_1972; }
#[test]
fn test_unix_at_utc_epoch_gives_zero_offset() {
let unix_s = UTC_EPOCH_UNIX_OFFSET_S;
let utc_s_from_1972 = unix_s - UTC_EPOCH_UNIX_OFFSET_S;
assert_eq!(utc_s_from_1972, 0);
}
#[test]
fn test_pre_unix_date_is_negative() {
let date = CivilDate::new(1960, 1, 1);
assert!(date.days_from_unix() < 0);
}
#[test]
fn test_invalid_date_does_not_panic() {
let date = CivilDate::new(2024, 13, 40);
let _ = date.days_from_unix(); }
#[test]
fn test_ordering_is_consistent() {
let a = CivilDate::new(2000, 1, 1);
let b = CivilDate::new(2001, 1, 1);
assert!(a.days_from_unix() < b.days_from_unix());
}
#[test]
fn test_seconds_until_matches_days() {
let a = CivilDate::new(2000, 1, 1);
let b = CivilDate::new(2000, 1, 2);
assert_eq!(a.seconds_until(b), 86_400);
}
#[test]
fn test_nanos_until_matches_seconds() {
let a = CivilDate::new(2000, 1, 1);
let b = CivilDate::new(2000, 1, 2);
assert_eq!(a.nanos_until(b), 86_400_000_000_000);
}
#[test]
fn test_gps_epoch_days_constant_is_stable() {
assert_eq!(GPS_EPOCH.days_from_unix(), 3657);
}
#[test]
fn test_monotonicity_property() {
let a = CivilDate::new(2000, 1, 1);
let b = CivilDate::new(2000, 1, 2);
let c = CivilDate::new(2000, 1, 3);
assert!(a.days_from_unix() < b.days_from_unix());
assert!(b.days_from_unix() < c.days_from_unix());
}
}