shakuntala_devi_trainer/
lib.rs

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
55//https://stackoverflow.com/questions/725098/leap-year-calculation
56//https://en.wikipedia.org/wiki/Leap_year#Algorithm
57fn is_leap_year(y: i32) -> bool {
58    (y % 4 == 0) && (y % 100 != 0) || (y % 400 == 0)
59}
60
61//https://stackoverflow.com/questions/6385190/correctness-of-sakamotos-algorithm-to-find-the-day-of-week
62pub 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
73//https://www.youtube.com/watch?v=4LHzUkfQ8oE&t=534s
74//https://brainly.in/question/19415705
75//https://fiat-knox.livejournal.com/1067226.html
76pub 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
154//http://mathforum.org/library/drmath/view/62324.html
155//https://medium.com/explorations-in-python/calculating-the-day-of-the-week-with-zellers-congruence-in-python-8009001dd84e
156pub 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(1901, 1, 7);
201    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 random_date = NaiveDate::from_ymd_opt(1980, 2, 1).unwrap();
273    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}