Coverage Report

Created: 2025-11-11 06:52

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/calendrical_calculations-0.1.2/src/persian.rs
Line
Count
Source
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
10
use crate::helpers::{i64_to_i32, I32CastError, IntegerRoundings};
11
use crate::rata_die::RataDie;
12
13
/// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L4720>
14
// Book states that the Persian epoch is the date: 3/19/622 and since the Persian Calendar has no year 0, the best choice was to use the Julian function.
15
const FIXED_PERSIAN_EPOCH: RataDie = crate::julian::fixed_from_julian(622, 3, 19);
16
17
// All these years are not leap, while they are considered leap by the 33-year
18
// rule. The year following each of them is leap, but it's considered non-leap
19
// by the 33-year rule. This table has been tested to match the modified
20
// astronomical algorithm based on the 52.5 degrees east meridian from 1178 AP
21
// (an arbitrary date before the Persian calendar was adopted in 1304 AP) to
22
// 3000 AP (an arbitrary date far into the future).
23
const NON_LEAP_CORRECTION: [i32; 78] = [
24
    1502, 1601, 1634, 1667, 1700, 1733, 1766, 1799, 1832, 1865, 1898, 1931, 1964, 1997, 2030, 2059,
25
    2063, 2096, 2129, 2158, 2162, 2191, 2195, 2224, 2228, 2257, 2261, 2290, 2294, 2323, 2327, 2356,
26
    2360, 2389, 2393, 2422, 2426, 2455, 2459, 2488, 2492, 2521, 2525, 2554, 2558, 2587, 2591, 2620,
27
    2624, 2653, 2657, 2686, 2690, 2719, 2723, 2748, 2752, 2756, 2781, 2785, 2789, 2818, 2822, 2847,
28
    2851, 2855, 2880, 2884, 2888, 2913, 2917, 2921, 2946, 2950, 2954, 2979, 2983, 2987,
29
];
30
31
const MIN_NON_LEAP_CORRECTION: i32 = NON_LEAP_CORRECTION[0];
32
33
/// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L4803>
34
/// Not used, but kept for comparative purposes
35
0
pub fn fixed_from_arithmetic_persian(year: i32, month: u8, day: u8) -> RataDie {
36
0
    let p_year = i64::from(year);
37
0
    let month = i64::from(month);
38
0
    let day = i64::from(day);
39
0
    let y = if p_year > 0 {
40
0
        p_year - 474
41
    } else {
42
0
        p_year - 473
43
    };
44
0
    let year = y.rem_euclid(2820) + 474;
45
46
0
    RataDie::new(
47
0
        FIXED_PERSIAN_EPOCH.to_i64_date() - 1
48
0
            + 1029983 * y.div_euclid(2820)
49
0
            + 365 * (year - 1)
50
0
            + (31 * year - 5).div_euclid(128)
51
0
            + if month <= 7 {
52
0
                31 * (month - 1)
53
            } else {
54
0
                30 * (month - 1) + 6
55
            }
56
0
            + day,
57
    )
58
0
}
59
60
/// fixed_from_arithmetic_persian, modified to use the more correct 33-year rule
61
0
pub fn fixed_from_fast_persian(year: i32, month: u8, day: u8) -> RataDie {
62
0
    let p_year = i64::from(year);
63
0
    let month = i64::from(month);
64
0
    let day = i64::from(day);
65
0
    let mut new_year = FIXED_PERSIAN_EPOCH.to_i64_date() - 1
66
0
        + 365 * (p_year - 1)
67
0
        + (8 * p_year + 21).div_euclid(33);
68
0
    if year > MIN_NON_LEAP_CORRECTION && NON_LEAP_CORRECTION.binary_search(&(year - 1)).is_ok() {
69
0
        new_year -= 1;
70
0
    }
71
0
    RataDie::new(
72
0
        new_year - 1
73
0
            + if month <= 7 {
74
0
                31 * (month - 1)
75
            } else {
76
0
                30 * (month - 1) + 6
77
            }
78
0
            + day,
79
    )
80
0
}
81
82
/// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L4857>
83
/// Not used, but kept for comparative purposes
84
0
pub fn arithmetic_persian_from_fixed(date: RataDie) -> Result<(i32, u8, u8), I32CastError> {
85
0
    let year = arithmetic_persian_year_from_fixed(date);
86
0
    let year = i64_to_i32(year)?;
87
    #[allow(clippy::unwrap_used)] // valid month,day
88
0
    let day_of_year = 1_i64 + (date - fixed_from_arithmetic_persian(year, 1, 1));
89
    #[allow(unstable_name_collisions)] // div_ceil is unstable and polyfilled
90
0
    let month = if day_of_year <= 186 {
91
0
        day_of_year.div_ceil(31) as u8
92
    } else {
93
0
        (day_of_year - 6).div_ceil(30) as u8
94
    };
95
0
    let day = (date - fixed_from_arithmetic_persian(year, month, 1) + 1) as u8;
96
0
    Ok((year, month, day))
97
0
}
98
99
/// arithmetic_persian_from_fixed, modified to use the 33-year rule method
100
0
pub fn fast_persian_from_fixed(date: RataDie) -> Result<(i32, u8, u8), I32CastError> {
101
0
    let year = fast_persian_year_from_fixed(date);
102
0
    let mut year = i64_to_i32(year)?;
103
0
    let mut day_of_year = 1_i64 + (date - fixed_from_fast_persian(year, 1, 1));
104
0
    if day_of_year == 366
105
0
        && year >= MIN_NON_LEAP_CORRECTION
106
0
        && NON_LEAP_CORRECTION.binary_search(&year).is_ok()
107
0
    {
108
0
        year += 1;
109
0
        day_of_year = 1;
110
0
    }
111
    #[allow(unstable_name_collisions)] // div_ceil is unstable and polyfilled
112
0
    let month = if day_of_year <= 186 {
113
0
        day_of_year.div_ceil(31) as u8
114
    } else {
115
0
        (day_of_year - 6).div_ceil(30) as u8
116
    };
117
0
    let day = (date - fixed_from_fast_persian(year, month, 1) + 1) as u8;
118
0
    Ok((year, month, day))
119
0
}
120
121
/// Lisp code reference: <https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L4829>
122
/// Not used, but kept for comparative purposes
123
0
fn arithmetic_persian_year_from_fixed(date: RataDie) -> i64 {
124
0
    let d0 = date - fixed_from_arithmetic_persian(475, 1, 1);
125
0
    let n2820 = d0.div_euclid(1029983);
126
0
    let d1 = d0.rem_euclid(1029983);
127
0
    let y2820 = if d1 == 1029982 {
128
0
        2820
129
    } else {
130
0
        (128 * d1 + 46878).div_euclid(46751)
131
    };
132
0
    let year = 474 + n2820 * 2820 + y2820;
133
0
    if year > 0 {
134
0
        year
135
    } else {
136
0
        year - 1
137
    }
138
0
}
139
140
/// arithmetic_persian_year_from_fixed modified for the 33-year rule
141
0
fn fast_persian_year_from_fixed(date: RataDie) -> i64 {
142
0
    let days_since_epoch = date - FIXED_PERSIAN_EPOCH + 1;
143
0
    1 + (33 * days_since_epoch + 3).div_euclid(12053)
144
0
}
145
146
/// Lisp code reference: https://github.com/EdReingold/calendar-code2/blob/main/calendar.l#L4789
147
/// Not used, but kept for comparative purposes
148
#[allow(dead_code)]
149
0
fn is_arithmetic_leap_year(p_year: i32, _data: ()) -> bool {
150
0
    let mut p_year = p_year as i64;
151
0
    if 0 < p_year {
152
0
        p_year -= 474;
153
0
    } else {
154
0
        p_year -= 473;
155
0
    };
156
0
    let year = p_year.rem_euclid(2820) + 474;
157
158
0
    ((year + 38) * 31).rem_euclid(128) < 31
159
0
}
160
161
/// Calculated using the 33-year rule
162
0
pub fn is_leap_year(p_year: i32, _data: ()) -> bool {
163
0
    if p_year >= MIN_NON_LEAP_CORRECTION && NON_LEAP_CORRECTION.binary_search(&p_year).is_ok() {
164
0
        false
165
0
    } else if p_year > MIN_NON_LEAP_CORRECTION
166
0
        && NON_LEAP_CORRECTION.binary_search(&(p_year - 1)).is_ok()
167
    {
168
0
        true
169
    } else {
170
0
        let p_year = p_year as i64;
171
0
        (25 * p_year + 11).rem_euclid(33) < 8
172
    }
173
0
}
174
175
#[cfg(test)]
176
mod tests {
177
    use super::*;
178
    #[test]
179
    fn test_persian_epoch() {
180
        let epoch = FIXED_PERSIAN_EPOCH.to_i64_date();
181
        // Iso year of Persian Epoch
182
        let epoch_year_from_fixed = crate::iso::iso_year_from_fixed(RataDie::new(epoch));
183
        // 622 is the correct ISO year for the Persian Epoch
184
        assert_eq!(epoch_year_from_fixed, 622);
185
    }
186
187
    // Persian New Year occurring in March of Gregorian year (g_year) to fixed date
188
    fn nowruz(g_year: i32) -> RataDie {
189
        let (y, _m, _d) = crate::iso::iso_from_fixed(FIXED_PERSIAN_EPOCH).unwrap();
190
        let persian_year = g_year - y + 1;
191
        let year = if persian_year <= 0 {
192
            persian_year - 1
193
        } else {
194
            persian_year
195
        };
196
        fixed_from_fast_persian(year, 1, 1)
197
    }
198
199
    #[test]
200
    fn test_nowruz() {
201
        // These values are used as test data in appendix C of the "Calendrical Calculations" book
202
        let nowruz_test_year_start = 2000;
203
        let nowruz_test_year_end = 2103;
204
205
        for year in nowruz_test_year_start..=nowruz_test_year_end {
206
            let two_thousand_eight_to_fixed = nowruz(year).to_i64_date();
207
            let iso_date = crate::iso::fixed_from_iso(year, 3, 21);
208
            let (persian_year, _m, _d) = fast_persian_from_fixed(iso_date).unwrap();
209
            assert_eq!(
210
                fast_persian_from_fixed(RataDie::new(two_thousand_eight_to_fixed))
211
                    .unwrap()
212
                    .0,
213
                persian_year
214
            );
215
        }
216
    }
217
}