calendrical_calculations/
iso.rs

1// This file is part of ICU4X.
2//
3// The contents of this file implement algorithms from Calendrical Calculations
4// by Reingold & Dershowitz, Cambridge University Press, 4th edition (2018),
5// which have been released as Lisp code at <https://github.com/EdReingold/calendar-code2/>
6// under the Apache-2.0 license. Accordingly, this file is released under
7// the Apache License, Version 2.0 which can be found at the calendrical_calculations
8// package root or at http://www.apache.org/licenses/LICENSE-2.0.
9
10use crate::helpers::{i64_to_i32, I32CastError};
11use crate::rata_die::RataDie;
12
13// The Gregorian epoch is equivalent to first day in fixed day measurement
14const EPOCH: RataDie = RataDie::new(1);
15
16/// Whether or not `year` is a leap year
17pub const fn is_leap_year(year: i32) -> bool {
18    year % 4 == 0 && (year % 400 == 0 || year % 100 != 0)
19}
20
21// Fixed is day count representation of calendars starting from Jan 1st of year 1.
22// The fixed calculations algorithms are from the Calendrical Calculations book.
23//
24/// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/1ee51ecfaae6f856b0d7de3e36e9042100b4f424/calendar.l#L1167-L1189>
25pub const fn const_fixed_from_iso(year: i32, month: u8, day: u8) -> RataDie {
26    let prev_year = (year as i64) - 1;
27    // Calculate days per year
28    let mut fixed: i64 = (EPOCH.to_i64_date() - 1) + 365 * prev_year;
29    // Calculate leap year offset
30    let offset = prev_year.div_euclid(4) - prev_year.div_euclid(100) + prev_year.div_euclid(400);
31    // Adjust for leap year logic
32    fixed += offset;
33    // Days of current year
34    fixed += (367 * (month as i64) - 362).div_euclid(12);
35    // Leap year adjustment for the current year
36    fixed += if month <= 2 {
37        0
38    } else if is_leap_year(year) {
39        -1
40    } else {
41        -2
42    };
43    // Days passed in current month
44    fixed += day as i64;
45    RataDie::new(fixed)
46}
47
48/// Non-const version of [`const_fixed_from_iso`]
49pub fn fixed_from_iso(year: i32, month: u8, day: u8) -> RataDie {
50    const_fixed_from_iso(year, month, day)
51}
52
53/// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/1ee51ecfaae6f856b0d7de3e36e9042100b4f424/calendar.l#L1191-L1217>
54pub(crate) const fn iso_year_from_fixed(date: RataDie) -> i64 {
55    // Shouldn't overflow because it's not possbile to construct extreme values of RataDie
56    let date = date.until(EPOCH);
57
58    // 400 year cycles have 146097 days
59    let (n_400, date) = (date.div_euclid(146097), date.rem_euclid(146097));
60
61    // 100 year cycles have 36524 days
62    let (n_100, date) = (date.div_euclid(36524), date.rem_euclid(36524));
63
64    // 4 year cycles have 1461 days
65    let (n_4, date) = (date.div_euclid(1461), date.rem_euclid(1461));
66
67    let n_1 = date.div_euclid(365);
68
69    let year = 400 * n_400 + 100 * n_100 + 4 * n_4 + n_1;
70
71    if n_100 == 4 || n_1 == 4 {
72        year
73    } else {
74        year + 1
75    }
76}
77
78fn iso_new_year(year: i32) -> RataDie {
79    fixed_from_iso(year, 1, 1)
80}
81
82/// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/1ee51ecfaae6f856b0d7de3e36e9042100b4f424/calendar.l#L1525-L1540>
83pub fn iso_from_fixed(date: RataDie) -> Result<(i32, u8, u8), I32CastError> {
84    let year = iso_year_from_fixed(date);
85    let year = i64_to_i32(year)?;
86    // Calculates the prior days of the adjusted year, then applies a correction based on leap year conditions for the correct ISO date conversion.
87    let prior_days = date - iso_new_year(year);
88    let correction = if date < fixed_from_iso(year, 3, 1) {
89        0
90    } else if is_leap_year(year) {
91        1
92    } else {
93        2
94    };
95    let month = (12 * (prior_days + correction) + 373).div_euclid(367) as u8; // in 1..12 < u8::MAX
96    let day = (date - fixed_from_iso(year, month, 1) + 1) as u8; // <= days_in_month < u8::MAX
97    Ok((year, month, day))
98}