#[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 / 400 } else { (y - 399) / 400 };
let yoe = (y - era * 400) as u64;
let doy = ((153 * m as i64 + 2) / 5 + d as i64 - 1) as u64;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
era * 146_097 + doe as i64 - 719_468
}
pub const TAI_EPOCH: CivilDate = CivilDate::new(1958, 1, 1);
pub const UNIX_EPOCH: CivilDate = CivilDate::new(1970, 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 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"
);
};
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_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());
}
}