calendrical_calculations/
julian.rs1use crate::helpers::{i64_to_i32, k_day_after, I32CastError};
11use crate::rata_die::RataDie;
12
13const JULIAN_EPOCH: RataDie = crate::gregorian::fixed_from_gregorian(0, 12, 30);
15
16#[inline(always)]
18pub const fn is_leap_year(year: i32) -> bool {
19 year % 4 == 0
20}
21
22pub const fn fixed_from_julian(year: i32, month: u8, day: u8) -> RataDie {
24 let mut fixed =
25 JULIAN_EPOCH.to_i64_date() - 1 + 365 * (year as i64 - 1) + (year as i64 - 1).div_euclid(4);
26 debug_assert!(month > 0 && month < 13, "Month should be in range 1..=12.");
27 fixed += match month {
28 1 => 0,
29 2 => 31,
30 3 => 59,
31 4 => 90,
32 5 => 120,
33 6 => 151,
34 7 => 181,
35 8 => 212,
36 9 => 243,
37 10 => 273,
38 11 => 304,
39 12 => 334,
40 _ => -1,
41 };
42 if month > 2 && is_leap_year(year) {
44 fixed += 1;
45 }
46 RataDie::new(fixed + (day as i64))
47}
48
49pub fn julian_from_fixed(date: RataDie) -> Result<(i32, u8, u8), I32CastError> {
51 let approx = (4 * date.to_i64_date() + 1464).div_euclid(1461);
52 let year = i64_to_i32(approx)?;
53 let prior_days = date
54 - fixed_from_julian(year, 1, 1)
55 - if is_leap_year(year) && date > fixed_from_julian(year, 2, 28) {
56 1
57 } else {
58 0
59 };
60 let adjusted_year = if prior_days >= 365 {
61 year.saturating_add(1)
62 } else {
63 year
64 };
65 let adjusted_prior_days = prior_days.rem_euclid(365);
66 debug_assert!((0..365).contains(&adjusted_prior_days));
67 let month = if adjusted_prior_days < 31 {
68 1
69 } else if adjusted_prior_days < 59 {
70 2
71 } else if adjusted_prior_days < 90 {
72 3
73 } else if adjusted_prior_days < 120 {
74 4
75 } else if adjusted_prior_days < 151 {
76 5
77 } else if adjusted_prior_days < 181 {
78 6
79 } else if adjusted_prior_days < 212 {
80 7
81 } else if adjusted_prior_days < 243 {
82 8
83 } else if adjusted_prior_days < 273 {
84 9
85 } else if adjusted_prior_days < 304 {
86 10
87 } else if adjusted_prior_days < 334 {
88 11
89 } else {
90 12
91 };
92 let day = (date - fixed_from_julian(adjusted_year, month, 1) + 1) as u8; debug_assert!(day <= 31, "Day assertion failed; date: {date:?}, adjusted_year: {adjusted_year}, prior_days: {prior_days}, month: {month}, day: {day}");
94
95 Ok((adjusted_year, month, day))
96}
97
98pub const fn fixed_from_julian_book_version(book_year: i32, month: u8, day: u8) -> RataDie {
105 debug_assert!(book_year != 0);
106 fixed_from_julian(
108 if book_year < 0 {
109 book_year + 1
110 } else {
111 book_year
112 },
113 month,
114 day,
115 )
116}
117
118pub fn easter(year: i32) -> RataDie {
120 let shifted_epact = (14 + 11 * year.rem_euclid(19)) % 30;
121 let paschal_moon = fixed_from_julian(year, 4, 19) - shifted_epact as i64;
122 k_day_after(0, paschal_moon)
123}
124
125#[test]
126fn test_easter() {
127 for (y, m, d) in [
129 (2021, 5, 2),
130 (2022, 4, 24),
131 (2023, 4, 16),
132 (2024, 5, 5),
133 (2025, 4, 20),
134 (2026, 4, 12),
135 (2027, 5, 2),
136 (2028, 4, 16),
137 (2029, 4, 8),
138 ] {
139 assert_eq!(easter(y), crate::gregorian::fixed_from_gregorian(y, m, d));
140 }
141}