calendrical_calculations/
julian.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// Julian epoch is equivalent to fixed_from_iso of December 30th of 0 year
14// 1st Jan of 1st year Julian is equivalent to December 30th of 0th year of ISO year
15const JULIAN_EPOCH: RataDie = RataDie::new(-1);
16
17/// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/1ee51ecfaae6f856b0d7de3e36e9042100b4f424/calendar.l#L1684-L1687>
18#[inline(always)]
19pub const fn is_leap_year(year: i32) -> bool {
20    year % 4 == 0
21}
22
23/// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/1ee51ecfaae6f856b0d7de3e36e9042100b4f424/calendar.l#L1689-L1709>
24pub const fn fixed_from_julian(year: i32, month: u8, day: u8) -> RataDie {
25    let mut fixed =
26        JULIAN_EPOCH.to_i64_date() - 1 + 365 * (year as i64 - 1) + (year as i64 - 1).div_euclid(4);
27    debug_assert!(month > 0 && month < 13, "Month should be in range 1..=12.");
28    fixed += match month {
29        1 => 0,
30        2 => 31,
31        3 => 59,
32        4 => 90,
33        5 => 120,
34        6 => 151,
35        7 => 181,
36        8 => 212,
37        9 => 243,
38        10 => 273,
39        11 => 304,
40        12 => 334,
41        _ => -1,
42    };
43    // Only add one if the month is after February (month > 2), since leap days are added to the end of February
44    if month > 2 && is_leap_year(year) {
45        fixed += 1;
46    }
47    RataDie::new(fixed + (day as i64))
48}
49
50/// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/1ee51ecfaae6f856b0d7de3e36e9042100b4f424/calendar.l#L1711-L1738>
51pub fn julian_from_fixed(date: RataDie) -> Result<(i32, u8, u8), I32CastError> {
52    let approx = (4 * date.to_i64_date() + 1464).div_euclid(1461);
53    let year = i64_to_i32(approx)?;
54    let prior_days = date
55        - fixed_from_julian(year, 1, 1)
56        - if is_leap_year(year) && date > fixed_from_julian(year, 2, 28) {
57            1
58        } else {
59            0
60        };
61    let adjusted_year = if prior_days >= 365 {
62        year.saturating_add(1)
63    } else {
64        year
65    };
66    let adjusted_prior_days = prior_days.rem_euclid(365);
67    debug_assert!((0..365).contains(&adjusted_prior_days));
68    let month = if adjusted_prior_days < 31 {
69        1
70    } else if adjusted_prior_days < 59 {
71        2
72    } else if adjusted_prior_days < 90 {
73        3
74    } else if adjusted_prior_days < 120 {
75        4
76    } else if adjusted_prior_days < 151 {
77        5
78    } else if adjusted_prior_days < 181 {
79        6
80    } else if adjusted_prior_days < 212 {
81        7
82    } else if adjusted_prior_days < 243 {
83        8
84    } else if adjusted_prior_days < 273 {
85        9
86    } else if adjusted_prior_days < 304 {
87        10
88    } else if adjusted_prior_days < 334 {
89        11
90    } else {
91        12
92    };
93    let day = (date - fixed_from_julian(adjusted_year, month, 1) + 1) as u8; // as days_in_month is < u8::MAX
94    debug_assert!(day <= 31, "Day assertion failed; date: {date:?}, adjusted_year: {adjusted_year}, prior_days: {prior_days}, month: {month}, day: {day}");
95
96    Ok((adjusted_year, month, day))
97}
98
99/// Get a fixed date from the ymd of a Julian date.
100///
101/// Years are counted as in _Calendrical Calculations_ by Reingold & Dershowitz,
102/// meaning there is no year 0. For instance, near the epoch date, years are counted: -3, -2, -1, 1, 2, 3 instead of -2, -1, 0, 1, 2, 3.
103///
104/// Primarily useful for use with code constructing epochs specified in the bookg
105pub const fn fixed_from_julian_book_version(book_year: i32, month: u8, day: u8) -> RataDie {
106    debug_assert!(book_year != 0);
107    // TODO: Should we check the bounds here?
108    fixed_from_julian(
109        if book_year < 0 {
110            book_year + 1
111        } else {
112            book_year
113        },
114        month,
115        day,
116    )
117}