pub const MS_PER_SEC: i64 = 1_000;
pub const MS_PER_MIN: i64 = 60_000;
pub const MS_PER_HOUR: i64 = 3_600_000;
pub const MS_PER_DAY: i64 = 86_400_000;
pub fn is_leap(y: i64) -> bool {
(y % 4 == 0 && y % 100 != 0) || y % 400 == 0
}
pub fn days_in_month(y: i64, m: i64) -> i64 {
match m {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 => {
if is_leap(y) {
29
} else {
28
}
}
_ => 0,
}
}
pub fn days_in_year(y: i64) -> i64 {
if is_leap(y) {
366
} else {
365
}
}
pub fn days_from_civil(y: i64, m: i64, d: i64) -> i64 {
let y = y - i64::from(m <= 2);
let era = if y >= 0 { y } else { y - 399 } / 400;
let yoe = y - era * 400; let doy = (153 * (m + if m > 2 { -3 } else { 9 }) + 2) / 5 + d - 1; let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; era * 146097 + doe - 719468
}
pub fn civil_from_days(z: i64) -> (i64, i64, i64) {
let z = z + 719468;
let era = if z >= 0 { z } else { z - 146096 } / 146097;
let doe = z - era * 146097; let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; let y = yoe + 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 + i64::from(m <= 2), m, d)
}
pub fn iso_weekday(days: i64) -> i64 {
(days + 3).rem_euclid(7) + 1
}
pub fn day_of_year(y: i64, m: i64, d: i64) -> i64 {
days_from_civil(y, m, d) - days_from_civil(y, 1, 1) + 1
}
pub fn quarter_of_month(m: i64) -> i64 {
(m - 1) / 3 + 1
}
pub fn quarter_start_month(q: i64) -> i64 {
(q - 1) * 3 + 1
}
pub fn days_in_quarter(y: i64, q: i64) -> i64 {
let sm = quarter_start_month(q);
(sm..sm + 3).map(|m| days_in_month(y, m)).sum()
}
pub fn day_of_quarter(y: i64, m: i64, d: i64) -> i64 {
let q = quarter_of_month(m);
day_of_year(y, m, d) - day_of_year(y, quarter_start_month(q), 1) + 1
}
pub fn weeks_in_iso_year(y: i64) -> i64 {
fn p(y: i64) -> i64 {
(y + y.div_euclid(4) - y.div_euclid(100) + y.div_euclid(400)).rem_euclid(7)
}
if p(y) == 4 || p(y - 1) == 3 {
53
} else {
52
}
}
pub fn iso_week(y: i64, m: i64, d: i64) -> (i64, i64) {
let days = days_from_civil(y, m, d);
let wd = iso_weekday(days);
let doy = day_of_year(y, m, d);
let week = (doy - wd + 10) / 7;
if week < 1 {
(y - 1, weeks_in_iso_year(y - 1))
} else if week > weeks_in_iso_year(y) {
(y + 1, 1)
} else {
(y, week)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Naive {
pub y: i64,
pub mo: i64,
pub d: i64,
pub h: i64,
pub mi: i64,
pub s: i64,
pub ms: i64,
}
impl Naive {
pub fn new(y: i64, mo: i64, d: i64, h: i64, mi: i64, s: i64, ms: i64) -> Self {
Naive {
y,
mo,
d,
h,
mi,
s,
ms,
}
}
pub fn ordinal_ms(&self) -> i64 {
days_from_civil(self.y, self.mo, self.d) * MS_PER_DAY
+ self.h * MS_PER_HOUR
+ self.mi * MS_PER_MIN
+ self.s * MS_PER_SEC
+ self.ms
}
pub fn from_ordinal_ms(v: i64) -> Naive {
let days = v.div_euclid(MS_PER_DAY);
let rem = v.rem_euclid(MS_PER_DAY);
let (y, mo, d) = civil_from_days(days);
Naive {
y,
mo,
d,
h: rem / MS_PER_HOUR,
mi: (rem % MS_PER_HOUR) / MS_PER_MIN,
s: (rem % MS_PER_MIN) / MS_PER_SEC,
ms: rem % MS_PER_SEC,
}
}
pub fn add_days(&self, n: i64) -> Naive {
let (y, mo, d) = civil_from_days(days_from_civil(self.y, self.mo, self.d) + n);
Naive { y, mo, d, ..*self }
}
pub fn add_months_constrain(&self, n: i64) -> Naive {
let total = self.y * 12 + (self.mo - 1) + n;
let y = total.div_euclid(12);
let mo = total.rem_euclid(12) + 1;
let d = self.d.min(days_in_month(y, mo));
Naive { y, mo, d, ..*self }
}
pub fn add_years_constrain(&self, n: i64) -> Naive {
let y = self.y + n;
let d = self.d.min(days_in_month(y, self.mo));
Naive { y, d, ..*self }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn add_days_crosses_a_year_boundary() {
let n = Naive::new(2020, 12, 31, 5, 0, 0, 0).add_days(1);
assert_eq!((n.y, n.mo, n.d), (2021, 1, 1));
}
#[test]
fn leap_year_rule() {
assert!(is_leap(2000)); assert!(!is_leap(1900)); assert!(is_leap(2004)); assert!(!is_leap(2001)); assert!(!is_leap(2100)); }
#[test]
fn days_in_month_covers_every_month() {
let common = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
for (i, &n) in common.iter().enumerate() {
assert_eq!(days_in_month(2001, i as i64 + 1), n);
}
assert_eq!(days_in_month(2000, 2), 29); assert_eq!(days_in_year(2001), 365);
assert_eq!(days_in_year(2000), 366);
}
#[test]
fn days_in_month_rejects_out_of_range_months() {
assert_eq!(days_in_month(2000, 0), 0);
assert_eq!(days_in_month(2000, 13), 0);
}
#[test]
fn weekday_and_day_of_year() {
assert_eq!(iso_weekday(0), 4);
assert_eq!(iso_weekday(1), 5); assert_eq!(iso_weekday(-1), 3); assert_eq!(day_of_year(2020, 1, 1), 1);
assert_eq!(day_of_year(2020, 3, 1), 61); assert_eq!(day_of_year(2021, 3, 1), 60); assert_eq!(day_of_year(2020, 12, 31), 366);
}
#[test]
fn quarter_arithmetic() {
assert_eq!(quarter_of_month(1), 1);
assert_eq!(quarter_of_month(3), 1);
assert_eq!(quarter_of_month(4), 2);
assert_eq!(quarter_of_month(12), 4);
assert_eq!(quarter_start_month(1), 1);
assert_eq!(quarter_start_month(3), 7);
assert_eq!(quarter_start_month(4), 10);
assert_eq!(days_in_quarter(2020, 1), 91); assert_eq!(days_in_quarter(2021, 1), 90); assert_eq!(days_in_quarter(2021, 2), 91); assert_eq!(day_of_quarter(2020, 1, 1), 1);
assert_eq!(day_of_quarter(2020, 3, 31), 91);
assert_eq!(day_of_quarter(2021, 5, 1), 31); }
#[test]
fn iso_week_spills_into_the_neighbouring_year() {
assert_eq!(iso_week(2021, 1, 1), (2020, 53));
assert_eq!(iso_week(2022, 1, 1), (2021, 52));
assert_eq!(iso_week(2018, 12, 31), (2019, 1));
assert_eq!(iso_week(2019, 12, 30), (2020, 1));
assert_eq!(iso_week(2021, 6, 15), (2021, 24));
}
#[test]
fn iso_year_lengths() {
for y in [2004, 2009, 2015, 2020, 2026] {
assert_eq!(weeks_in_iso_year(y), 53, "{y}");
}
for y in [2019, 2021, 2022, 2023] {
assert_eq!(weeks_in_iso_year(y), 52, "{y}");
}
}
#[test]
fn naive_round_trip_and_field_split() {
let n = Naive::new(2023, 7, 15, 13, 47, 9, 123);
let v = n.ordinal_ms();
let back = Naive::from_ordinal_ms(v);
assert_eq!((back.y, back.mo, back.d), (2023, 7, 15));
assert_eq!((back.h, back.mi, back.s, back.ms), (13, 47, 9, 123));
let midnight = Naive::new(1970, 1, 1, 0, 0, 0, 0);
assert_eq!(midnight.ordinal_ms(), 0);
assert_eq!(Naive::new(1970, 1, 1, 1, 0, 0, 0).ordinal_ms(), MS_PER_HOUR);
assert_eq!(Naive::new(1970, 1, 1, 0, 1, 0, 0).ordinal_ms(), MS_PER_MIN);
assert_eq!(Naive::new(1970, 1, 1, 0, 0, 1, 0).ordinal_ms(), MS_PER_SEC);
assert_eq!(Naive::new(1970, 1, 2, 0, 0, 0, 0).ordinal_ms(), MS_PER_DAY);
}
#[test]
fn naive_calendar_arithmetic_constrains_overflow() {
let base = Naive::new(2021, 1, 31, 9, 0, 0, 0);
let feb = base.add_months_constrain(1);
assert_eq!((feb.y, feb.mo, feb.d, feb.h), (2021, 2, 28, 9));
let leap = Naive::new(2019, 12, 31, 0, 0, 0, 0).add_months_constrain(2);
assert_eq!((leap.y, leap.mo, leap.d), (2020, 2, 29));
let back = base.add_months_constrain(-1);
assert_eq!((back.y, back.mo, back.d), (2020, 12, 31));
let y = Naive::new(2020, 2, 29, 0, 0, 0, 0).add_years_constrain(1);
assert_eq!((y.y, y.mo, y.d), (2021, 2, 28));
let d = base.add_days(2);
assert_eq!((d.y, d.mo, d.d, d.h), (2021, 2, 2, 9));
}
#[test]
fn civil_conversions_span_the_epoch_and_year_zero() {
assert_eq!(civil_from_days(-1), (1969, 12, 31));
assert_eq!(days_from_civil(1969, 12, 31), -1);
assert_eq!(civil_from_days(days_from_civil(0, 1, 1)), (0, 1, 1));
assert_eq!(civil_from_days(days_from_civil(0, 2, 29)), (0, 2, 29)); }
}