1use chrono::prelude::*;
2use chrono::Duration;
3use num_traits::cast::FromPrimitive;
4use std::fmt;
5use std::sync::LazyLock;
6use std::{
7 collections::{HashMap, VecDeque},
8 convert::TryInto,
9};
10
11pub const MIN_YEAR: u32 = 1583;
12pub const MAX_YEAR: u32 = 2204;
13pub const DEFAULT_FIRST_YEAR: u32 = 1932;
14pub const DEFAULT_LAST_YEAR: u32 = 2032;
15
16pub static YEARS: LazyLock<HashMap<i32, i32>> = LazyLock::new(|| {
17 const T3: [i32; 7] = [0, 5, 3, 1, 6, 4, 2];
18 let mut years = HashMap::new();
19 let mut cycled = T3.iter().cycle();
20 for year in (1584..1600).step_by(4) {
21 years.insert(year, *cycled.next().unwrap());
22 }
23 let mut cycled = T3.iter().cycle();
24 for year in (1600..1700).step_by(4) {
25 years.insert(year, 6 + *cycled.next().unwrap());
26 }
27 let mut cycled = T3.iter().cycle();
28 for year in (1700..1800).step_by(4) {
29 years.insert(year, 4 + *cycled.next().unwrap());
30 }
31 let mut cycled = T3.iter().cycle();
32 for year in (1800..1900).step_by(4) {
33 years.insert(year, 2 + *cycled.next().unwrap());
34 }
35 let mut cycled = T3.iter().cycle();
36 for year in (1900..2000).step_by(4) {
37 years.insert(year, *cycled.next().unwrap());
38 }
39 let mut cycled = T3.iter().cycle();
40 for year in (2000..2100).step_by(4) {
41 years.insert(year, 6 + *cycled.next().unwrap());
42 }
43 let mut cycled = T3.iter().cycle();
44 for year in (2100..2200).step_by(4) {
45 years.insert(year, 4 + *cycled.next().unwrap());
46 }
47 for year in years.values_mut() {
48 *year %= 7;
49 }
50 years
51});
52
53pub const T2: [i32; 12] = [0, 3, 3, 6, 1, 4, 6, 2, 5, 0, 3, 5];
54
55fn is_leap_year(y: i32) -> bool {
58 (y % 4 == 0) && (y % 100 != 0) || (y % 400 == 0)
59}
60
61pub fn tomohiko_sakamoto(dt: NaiveDate) -> Weekday {
63 const DAYS: [i32; 12] = [0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4];
64 let y = if dt.month() < 3 {
65 dt.year() - 1
66 } else {
67 dt.year()
68 };
69 let day = (y + y / 4 - y / 100 + y / 400 + DAYS[dt.month0() as usize] + dt.day() as i32) % 7;
70 Weekday::from_i32(day).unwrap().pred()
71}
72
73pub fn shakuntala_devi_nearest_leap_year(year: i32, v: &mut Option<&mut Tips>) -> i32 {
77 let t1 = YEARS.get(&year);
78 match t1 {
79 Some(result) => {
80 if is_leap_year(year) {
81 if v.is_some() {
82 v.as_mut().unwrap().0.push_back(format!(
83 "leap year and direct year table entry {:#?}",
84 result
85 ))
86 };
87 result.to_owned()
88 } else {
89 if v.is_some() {
90 v.as_mut().unwrap().0.push_back(format!(
91 "not a leap year but direct year table entry {:#?}",
92 result
93 ))
94 };
95 let mut nearest_leap_year = year - 1;
96 while !is_leap_year(nearest_leap_year) {
97 nearest_leap_year -= 1;
98 }
99 if v.is_some() {
100 v.as_mut()
101 .unwrap()
102 .0
103 .push_back(format!("nearest leap year {:#?}", nearest_leap_year));
104 v.as_mut().unwrap().0.push_back(format!(
105 "nearest leap year table entry {:#?}",
106 YEARS.get(&nearest_leap_year).unwrap()
107 ))
108 };
109 nearest_leap_year
110 }
111 }
112 None => {
113 let mut nearest_leap_year = year - 1;
114 while !is_leap_year(nearest_leap_year) {
115 nearest_leap_year -= 1;
116 }
117 if v.is_some() {
118 v.as_mut().unwrap().0.push_back(format!(
119 "no direct year table entry, nearest leap year {:#?}",
120 nearest_leap_year
121 ));
122 v.as_mut().unwrap().0.push_back(format!(
123 "no direct year table entry, nearest leap year table entry {:#?}",
124 YEARS.get(&nearest_leap_year).unwrap()
125 ))
126 };
127 nearest_leap_year
128 }
129 }
130}
131pub fn shakuntala_devi(dt: NaiveDate) -> (Weekday, Tips) {
132 let mut v: Tips = Tips(VecDeque::new());
133 let day = dt.day() % 7;
134 let month_table_entry = T2[dt.month0() as usize];
135 let result1 = (day as i32 + month_table_entry) % 7;
136 v.0.push_back(format!(
137 "(day {} + month table entry {}) mod 7 = {}",
138 day, month_table_entry, result1
139 ));
140
141 let result2 = shakuntala_devi_nearest_leap_year(dt.year(), &mut Some(&mut v));
142 let result3 = if YEARS.get(&dt.year()).is_some() && is_leap_year(dt.year()) {
143 if dt.month() > 2 {
144 result1 + result2
145 } else {
146 result1 + result2 - 1
147 }
148 } else {
149 result1 + YEARS.get(&result2).unwrap() + dt.year() - result2
150 };
151 (Weekday::from_i32(result3.rem_euclid(7)).unwrap().pred(), v)
152}
153
154pub fn zeller(dt: NaiveDate) -> Weekday {
157 let mut year = dt.year();
158 let mut month = dt.month();
159 if dt.month() < 3 {
160 month += 12;
161 year -= 1;
162 }
163 Weekday::from_i32(
164 (dt.day() as i32 - 2 + (13 * (month + 1) / 5) as i32 + year + year / 4 - year / 100
165 + year / 400)
166 % 7,
167 )
168 .unwrap()
169}
170
171#[allow(clippy::useless_conversion)]
172pub fn st_mag_53(dt: NaiveDate) -> Weekday {
173 let (j, m, a) = (dt.day(), dt.month(), dt.year() as u32);
174 let man = (0.6 + 1.0 / f64::from(m) + 0.001) as u32;
175 let mp = m + 12 * man;
176 let ap = a - man;
177 let jd = j
178 + ((367.0 * (f64::from(mp) - 1.0) + 5.0) / 12.0 + 0.001) as u32
179 + (365.25 * (f64::from(ap) + 4712.0) + 0.001) as u32;
180 let jd = jd - ((f64::from(ap) / 100.0) as u32 + (f64::from(ap) / 400.0) as u32);
181 let js = f64::from(jd - 1720977) / 7.0;
182 let js = (7.0 * f64::from(js - f64::from(js as u32)) + 0.001) as i32;
183 Weekday::from_i32(js).unwrap()
184}
185
186pub fn svm_86_distance(dt: NaiveDate) -> u32 {
187 let (j, m, mut a) = (dt.day(), dt.month(), dt.year() as u32);
188 let mut n = a * 365 + 31 * (m - 1) + j;
189 if m <= 2 {
190 a -= 1;
191 }
192 n = n + (a / 4) - (a / 100) + (a / 400);
193 if m > 2 {
194 n -= (f64::from(m - 1) * 0.4 + 2.7) as u32;
195 }
196 n
197}
198
199pub fn svm_86(dt: NaiveDate) -> Weekday {
200 let base_date = NaiveDate::from_ymd_opt(1583, 1, 3).unwrap();
202 let distance = svm_86_distance(dt) - svm_86_distance(base_date);
203 Weekday::from_u32(distance % 7).unwrap()
204}
205
206pub const DOOMSDAY_COMMON_YEAR: [i32; 12] = [3, 28, 7, 4, 9, 6, 11, 8, 5, 10, 7, 12];
207pub const DOOMSDAY_LEAP_YEAR: [i32; 12] = [4, 29, 7, 4, 9, 6, 11, 8, 5, 10, 7, 12];
208
209pub fn anchor_day(year: i32) -> i32 {
210 if (1800..=1899).contains(&year) {
211 5
212 } else if (1900..=1999).contains(&year) {
213 3
214 } else if (2000..=2099).contains(&year) {
215 2
216 } else if (2100..=2199).contains(&year) {
217 0
218 } else {
219 panic!("year not supported")
220 }
221}
222
223pub fn conway_doomsday(dt: NaiveDate) -> Weekday {
224 let two_digit = dt.year() % 100;
225 let step1 = two_digit / 12;
226 let step2 = two_digit - (12 * step1);
227 let step3 = step2 / 4;
228 let step4 = anchor_day(dt.year());
229 let step5 = step1 + step2 + step3 + step4;
230 let step6 = step5 % 7;
231 let step7 = if is_leap_year(dt.year()) {
232 DOOMSDAY_LEAP_YEAR[dt.month0() as usize]
233 } else {
234 DOOMSDAY_COMMON_YEAR[dt.month0() as usize]
235 };
236 let step8 = if dt.day() as i32 > step7 {
237 (dt.day() as i32 - step7) + step6
238 } else {
239 (step6 - (step7 - dt.day() as i32)) % 7
240 };
241 let result = step8 % 7;
242 let result = if result < 0 { result + 7 } else { result } as u32;
243 Weekday::from_u32(result).unwrap().pred()
244}
245
246pub fn random_date(from_year: u32, to_year: u32) -> NaiveDate {
247 let start = NaiveDate::from_ymd_opt(from_year.try_into().unwrap(), 1, 1)
248 .unwrap()
249 .num_days_from_ce();
250 let end = NaiveDate::from_ymd_opt(to_year.try_into().unwrap(), 1, 1)
251 .unwrap()
252 .num_days_from_ce();
253 let days = rand::random_range(1..end - start);
254 let dt = NaiveDate::from_ymd_opt(from_year.try_into().unwrap(), 1, 7).unwrap();
255 dt + Duration::days(days as i64)
256}
257
258#[derive(Debug, Clone)]
259pub struct Tips(pub VecDeque<String>);
260
261impl fmt::Display for Tips {
262 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
263 for v in &self.0 {
264 writeln!(f, "{}", v)?;
265 }
266 Ok(())
267 }
268}
269
270pub fn random_date_with_tips(from_year: u32, to_year: u32) -> (NaiveDate, Weekday, Tips) {
271 let random_date = random_date(from_year, to_year);
272 let (shakuntala_devi_answer, tips) = shakuntala_devi(random_date);
274 (random_date, shakuntala_devi_answer, tips)
275}
276
277#[test]
278fn tomohiko_sakamoto_check() {
279 let calendar = NaiveDate::from_ymd_opt(1583, 1, 1).unwrap().iter_days();
280 for dt in calendar {
281 assert_eq!(tomohiko_sakamoto(dt), dt.weekday(), "testing {}", dt);
282 if dt.year() == 10000 {
283 break;
284 };
285 }
286}
287
288#[test]
289fn shakuntala_devi_unit_check() {
290 let dt = NaiveDate::from_ymd_opt(1928, 1, 7).unwrap();
291 println!("response {} {} ", dt.year(), dt.weekday());
292 assert_eq!(shakuntala_devi(dt).0, dt.weekday(), "testing {}", dt);
293}
294
295#[test]
296fn shakuntala_devi_check() {
297 let calendar = NaiveDate::from_ymd_opt(1584, 1, 1).unwrap().iter_days();
298 for dt in calendar {
299 assert_eq!(shakuntala_devi(dt).0, dt.weekday(), "testing {}", dt);
300 if dt.year() == 2204 {
301 break;
302 };
303 }
304}
305
306#[test]
307fn zeller_unit_check() {
308 let dt = NaiveDate::from_ymd_opt(1928, 1, 7).unwrap();
309 println!("response {} {} ", dt.year(), dt.weekday());
310 assert_eq!(zeller(dt), dt.weekday(), "testing {}", dt);
311}
312
313#[test]
314fn zeller_check() {
315 let calendar = NaiveDate::from_ymd_opt(1584, 1, 1).unwrap().iter_days();
316 for dt in calendar {
317 assert_eq!(zeller(dt), dt.weekday(), "testing {}", dt);
318 if dt.year() == 10000 {
319 break;
320 };
321 }
322}
323
324#[test]
325fn st_mag_53_unit_check() {
326 let dt = NaiveDate::from_ymd_opt(1980, 1, 7).unwrap();
327 println!("response {} {} ", dt.year(), dt.weekday());
328 assert_eq!(st_mag_53(dt), dt.weekday(), "testing {}", dt);
329}
330
331#[test]
332fn st_mag_53_check() {
333 let calendar = NaiveDate::from_ymd_opt(1700, 1, 1).unwrap().iter_days();
334 for dt in calendar {
335 assert_eq!(st_mag_53(dt), dt.weekday(), "testing {}", dt);
336 if dt.year() == 2000 {
337 break;
338 };
339 }
340}
341
342#[test]
343fn svm_86_check() {
344 let calendar = NaiveDate::from_ymd_opt(1583, 1, 3).unwrap().iter_days();
345 for dt in calendar {
346 assert_eq!(svm_86(dt), dt.weekday(), "testing {}", dt);
347 if dt.year() == 10000 {
348 break;
349 };
350 }
351}
352
353#[test]
354fn conway_check() {
355 let calendar = NaiveDate::from_ymd_opt(1800, 1, 1).unwrap().iter_days();
356 for dt in calendar {
357 assert_eq!(conway_doomsday(dt), dt.weekday(), "testing {}", dt);
358 if dt.year() == 2199 {
359 break;
360 };
361 }
362}
363
364#[test]
365fn leap_year_unit_check() {
366 assert!(is_leap_year(1584));
367}
368
369#[test]
370fn leap_year_check() {
371 for year in 1853..10000 {
372 assert!(is_leap_year(year) == NaiveDate::from_ymd_opt(year, 2, 29).is_some());
373 }
374}
375
376#[test]
377fn leap_year_reverse_check() {
378 for year in 1853..2204 {
379 if is_leap_year(year) {
380 assert!(YEARS.get(&year) != None)
381 };
382 }
383}