#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CalendarDay {
pub year: i32,
pub month: u32,
pub day: u32,
}
impl CalendarDay {
pub fn new(year: i32, month: u32, day: u32) -> Self {
let month = month.clamp(1, 12);
Self {
year,
month,
day: day.clamp(1, days_in_month(year, month)),
}
}
pub fn today() -> Self {
let (year, month, day) = local_today();
Self::new(year, month, day)
}
pub fn add_days(self, n: i64) -> Self {
from_days(to_days(self) + n)
}
pub fn add_months(self, n: i32) -> Self {
let m = Month::of(self).shift(n);
Self::new(m.year, m.month, self.day)
}
pub fn weekday(self) -> u32 {
(to_days(self) + 4).rem_euclid(7) as u32
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) struct Month {
pub year: i32,
pub month: u32,
}
impl Month {
pub fn of(day: CalendarDay) -> Self {
Self {
year: day.year,
month: day.month,
}
}
pub fn shift(self, n: i32) -> Self {
let total = self.year * 12 + self.month as i32 - 1 + n;
Self {
year: total.div_euclid(12),
month: total.rem_euclid(12) as u32 + 1,
}
}
pub fn first(self) -> CalendarDay {
CalendarDay {
year: self.year,
month: self.month,
day: 1,
}
}
pub fn grid(self, week_start: u32) -> impl Iterator<Item = CalendarDay> {
let first = self.first();
let lead = (first.weekday() + 7 - week_start) % 7;
(0..42).map(move |i| first.add_days(i - lead as i64))
}
}
pub(crate) fn days_in_month(year: i32, month: u32) -> u32 {
match month {
2 if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) => 29,
2 => 28,
4 | 6 | 9 | 11 => 30,
_ => 31,
}
}
fn to_days(d: CalendarDay) -> i64 {
let y = d.year as i64 - (d.month <= 2) as i64;
let era = y.div_euclid(400);
let yoe = y - era * 400;
let m = d.month as i64;
let doy = (153 * (if m > 2 { m - 3 } else { m + 9 }) + 2) / 5 + d.day as i64 - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
era * 146_097 + doe - 719_468
}
fn from_days(z: i64) -> CalendarDay {
let z = z + 719_468;
let era = z.div_euclid(146_097);
let doe = z - era * 146_097;
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let day = (doy - (153 * mp + 2) / 5 + 1) as u32;
let month = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
let year = (yoe + era * 400 + (month <= 2) as i64) as i32;
CalendarDay { year, month, day }
}
#[cfg(all(unix, not(target_arch = "wasm32")))]
fn local_today() -> (i32, u32, u32) {
unsafe {
let now = libc::time(std::ptr::null_mut());
let mut tm: libc::tm = std::mem::zeroed();
if libc::localtime_r(&now, &mut tm).is_null() {
return utc_today();
}
(tm.tm_year + 1900, tm.tm_mon as u32 + 1, tm.tm_mday as u32)
}
}
#[cfg(windows)]
fn local_today() -> (i32, u32, u32) {
use windows_sys::Win32::Foundation::SYSTEMTIME;
let t: SYSTEMTIME = unsafe {
let mut t = std::mem::zeroed();
windows_sys::Win32::System::SystemInformation::GetLocalTime(&mut t);
t
};
(t.wYear as i32, t.wMonth as u32, t.wDay as u32)
}
#[cfg(target_arch = "wasm32")]
fn local_today() -> (i32, u32, u32) {
let now = js_sys::Date::new_0();
(
now.get_full_year() as i32,
now.get_month() + 1,
now.get_date(),
)
}
#[cfg(not(any(unix, windows, target_arch = "wasm32")))]
fn local_today() -> (i32, u32, u32) {
utc_today()
}
#[allow(dead_code)]
fn utc_today() -> (i32, u32, u32) {
let secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
let d = from_days(secs.div_euclid(86_400));
(d.year, d.month, d.day)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn days_round_trip_across_eras() {
for z in [-800_000, -719_468, -1, 0, 1, 11_016, 19_000, 2_932_896] {
assert_eq!(to_days(from_days(z)), z);
}
assert_eq!(from_days(0), CalendarDay::new(1970, 1, 1));
assert_eq!(to_days(CalendarDay::new(2000, 3, 1)), 11_017);
}
#[test]
fn weekdays() {
assert_eq!(CalendarDay::new(1970, 1, 1).weekday(), 4);
assert_eq!(CalendarDay::new(2026, 11, 1).weekday(), 0);
assert_eq!(CalendarDay::new(2024, 2, 29).weekday(), 4);
}
#[test]
fn month_lengths_and_leap_years() {
assert_eq!(days_in_month(2024, 2), 29);
assert_eq!(days_in_month(1900, 2), 28);
assert_eq!(days_in_month(2000, 2), 29);
assert_eq!(days_in_month(2026, 4), 30);
}
#[test]
fn adding_crosses_months_and_years() {
let d = CalendarDay::new(2026, 12, 31);
assert_eq!(d.add_days(1), CalendarDay::new(2027, 1, 1));
assert_eq!(d.add_days(-365), CalendarDay::new(2025, 12, 31));
assert_eq!(
CalendarDay::new(2026, 1, 31).add_months(1),
CalendarDay::new(2026, 2, 28)
);
assert_eq!(
CalendarDay::new(2026, 1, 15).add_months(-1),
CalendarDay::new(2025, 12, 15)
);
}
#[test]
fn a_grid_starts_on_the_week_start() {
let nov = Month {
year: 2026,
month: 11,
};
let sunday: Vec<_> = nov.grid(0).collect();
assert_eq!(sunday[0], CalendarDay::new(2026, 11, 1));
let monday: Vec<_> = nov.grid(1).collect();
assert_eq!(monday[0], CalendarDay::new(2026, 10, 26));
assert_eq!(monday.len(), 42);
}
}