Skip to main content

jdate/
lib.rs

1use chrono::{NaiveDate, Local, Datelike};
2use std::fmt;
3
4// The Epoch we use is the molad tohu (Day 1 = 1 Tishrei 1 = 7 September -3760)
5// It is a theoretical time point 1 year before creation.
6
7
8/// Rata Die day number
9pub struct RD {
10    pub rd: i32
11}
12
13/// Gregorian Date
14#[derive(Debug, Clone, Copy, PartialEq)]
15pub struct GDate {
16    pub year: i32,
17    pub month: u8, // 1 = Nisan, 13 = Adar2
18    pub day: u8, // 1 - 30
19}
20
21/// Jewish Date
22#[derive(Debug, Clone, Copy, PartialEq)]
23pub struct JDate {
24    pub year: i32,
25    pub month: u8, // 1 = Nisan, 13 = Adar2
26    pub day: u8, // 1 - 30
27}
28
29impl GDate {
30    pub fn new(year: i32, month: u8, day: u8) -> Option<GDate> {
31        NaiveDate::from_ymd_opt(year, month as u32, day as u32)
32            .map(|_| GDate{year: year, month: month, day: day})
33    }
34
35    pub fn today() -> GDate {
36        let date = Local::now().date_naive();
37        GDate{
38            year:  date.year(),
39            month: date.month() as u8,
40            day:   date.day() as u8
41        }
42    }
43}
44
45
46impl JDate {
47    /// Create a new JDate with year, month, day. If the date is invalid: None is returned.
48    pub fn new(year: i32, month: u8, day: u8) -> Option<JDate> {
49        match date_is_valid(year, month, day) {
50            true  => Some(JDate{year: year, month: month, day: day}),
51            false => None
52        }
53    }
54
55    pub fn today() -> JDate {
56        JDate::from(GDate::today())
57    }
58
59    /// Get the month as a string
60    pub fn month_name(self: Self) -> &'static str {
61        const NAMES: [&str; 13] = [
62            "Nisan", "Iyar", "Sivan", "Tamuz", "Av", "Elul",
63            "Tishrei", "Cheshvan", "Kislev", "Tevet", "Shvat", "Adar", "Adar2"];
64        if self.month == 12 && is_leap_year(self.year) {
65            return "Adar1"
66        }
67        return NAMES[self.month as usize - 1];
68
69    }
70
71    pub fn dafyomi(self: Self) -> (&'static str, u8) {
72        let tractates = [
73            ("Berachos",      63),
74            ("Shabbos",      156),
75            ("Eruvin",       104),
76            ("Pesachim",     120),
77            ("Shekalim",      21),
78            ("Yoma",          87),
79            ("Sukah",         55),
80            ("Beitzah",       39),
81            ("Rosh Hashanah", 34),
82            ("Taanis",        30),
83            ("Megilah",       31),
84            ("Moed Katan",    28),
85            ("Chagigah",      26),
86            ("Yevamos",      121),
87            ("Kesuvos",      111),
88            ("Nedarim",       90),
89            ("Nazir",         65),
90            ("Sotah",         48),
91            ("Gitin",         89),
92            ("Kidushin",      81),
93            ("Bava Kama",    118),
94            ("Bava Metzia",  118),
95            ("Bava Basra",   175),
96            ("Sanhedrin",    112),
97            ("Makos",         23),
98            ("Shevuos",       48),
99            ("Avodah Zarah",  75),
100            ("Horayos",       13),
101            ("Zevachim",     119),
102            ("Menachos",     109),
103            ("Chulin",       141),
104            ("Bechoros",      60),
105            ("Erchin",        33),
106            ("Temurah",       33),
107            ("Kerisus",       27),
108            ("Meilah",        36),
109            ("Nidah",         72),
110        ];
111        let rd = RD::from(self).rd;
112        let mut day = (rd - 37).rem_euclid(2711);
113        for (trac, pages) in tractates.iter() {
114            if day <= pages - 1 {
115                return (trac, (day+2) as u8);
116            }
117            day -= pages;
118        }
119        return ("", 0);
120    }
121
122    pub fn omer(self: Self) -> u8 {
123        let day = self.day;
124        let month = self.month;
125        if month == 1 && day >= 16 {
126            return day - 15;
127        } else if month == 2 {
128            return day + 15;
129        } else if month == 3 && day < 6 {
130            return day + 44;
131        } else {
132            return 0;
133        }
134    }
135}
136
137impl fmt::Display for GDate {
138    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
139        write!(f, "{:0>4}-{:0>2}-{:0>2}", self.year, self.month, self.day)
140    }
141}
142
143impl fmt::Display for JDate {
144    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
145        write!(f, "{:0>4}-{}-{:0>2}", self.year, self.month_name(), self.day)
146    }
147}
148
149impl From<RD> for JDate {
150    /// JDate from RD
151    fn from(rd: RD) -> JDate {
152        let ed = rd.rd + 1373428; // days since epoch
153        let mut year = ed * 100 / 36525;
154        while year_start(year) < ed {year += 1;}
155        while year_start(year) > ed {year -= 1;}
156        let mut days = year_start(year);
157        let days_in_month = year_months(year);
158        let mut month = 7;
159        loop {
160            let length = days_in_month[month] as i32;
161            if days + length > ed {break}
162            month += 1;
163            if month == 14 {month = 1}
164            if month == 7 {unreachable!()}
165            days += length;
166        }
167        return JDate{
168            year: year,
169            month: month as u8,
170            day: (ed-days+1) as u8
171        };
172    }
173}
174
175impl From<JDate> for RD {
176    /// RD from JDate
177    fn from(j: JDate) -> RD {
178        let mut ed = year_start(j.year) - 1;
179        let days_in_month = year_months(j.year);
180        let mut month: u8 = 7;
181        loop {
182            let length = days_in_month[month as usize];
183            if month == j.month {
184                ed += j.day as i32;
185                break;
186            }
187            ed += length as i32;
188            month += 1;
189            if month == 14 {month = 1}
190            if month == 7 {unreachable!()}
191        }
192        RD{rd: ed - 1373428}
193    }
194}
195
196impl From<RD> for GDate {
197    /// GDate from RD
198    fn from(rd: RD) -> GDate {
199        let date = NaiveDate::from_epoch_days(rd.rd - 719163).unwrap();
200        GDate{
201            year: date.year(),
202            month: date.month() as u8,
203            day: date.day() as u8
204        }
205    }
206}
207
208impl From<GDate> for RD {
209    /// RD from GDate
210    fn from(g: GDate) -> RD {
211        RD{
212            rd: NaiveDate::from_ymd_opt(g.year, g.month as u32, g.day as u32)
213                .unwrap()
214                .to_epoch_days()
215                + 719163
216        }
217    }
218}
219
220impl From<GDate> for JDate {
221    /// Convert Gregorian Date to JDate
222    fn from(d: GDate) -> JDate {
223        JDate::from(RD::from(d))
224    }
225}
226
227impl From<JDate> for GDate {
228    /// Convert JDate to Gregorian Date
229    fn from(j: JDate) -> Self {
230        GDate::from(RD::from(j))
231
232    }
233}
234
235/// Determine if the Jewish year is a leap year
236pub fn is_leap_year(year: i32) -> bool {
237    match year % 19 {
238        0|3|6|8|11|14|17 => true,
239        _ => false
240    }
241}
242
243/// Determine if the Jewish date is valid
244pub fn date_is_valid(year: i32, month: u8, day: u8) -> bool {
245    if month < 1 || month > 13 || day < 1 || day > 30 {
246        return false;
247    }
248    if month == 13 {
249        return day <= 29 && is_leap_year(year);
250    }
251    if day == 30 {
252        return match month {
253            1|3|5|7|11 => true,
254            2|4|6|10 => false,
255            12 => is_leap_year(year),
256            8 => {
257                year_length(year) % 10 == 5 // complete year (355 or 385)
258            },
259            9 => {
260                year_length(year) % 10 >= 4 // complete or regular year (354, 355, 384 or 385)
261            },
262            _ => unreachable!()
263        }
264    }
265    true
266}
267
268/// Calculates the molad of the given year
269///
270/// returns the number of chalakim since the epoch
271pub fn molad(year: i32) -> i64 {
272    let parts_month = (29*24+12)*1080+793;
273    let parts_year  = 12 * parts_month;
274    let parts_lyear = 13 * parts_month;
275    let parts_cycle = 12 * parts_year + 7 * parts_lyear;
276    let total_cycles  = (year-1).div_euclid(19);
277    let year_in_cycle = (year-1).rem_euclid(19);
278    let mut molad: i64 = (24+5)*1080+204; // molad tohu
279    molad += total_cycles as i64 * parts_cycle as i64;
280    for year in 0..year_in_cycle {
281        if is_leap_year(year+1) {
282            molad += parts_lyear as i64;
283        } else {
284            molad += parts_year as i64;
285        }
286    }
287    return molad;
288}
289
290/// Calculates the molad of the given year
291///
292/// returns the days since epoch, hours (after 6pm), and parts (out of 1080
293/// chalakim)
294pub fn molad_components(year: i32) -> (i32, u8, u16) {
295    let molad = molad(year);
296    return ((molad.div_euclid(1080*24)) as i32,
297            (molad.div_euclid(1080).rem_euclid(24)) as u8,
298            (molad.rem_euclid(1080)) as u16);
299}
300
301/// Print molad in a friendly format
302pub fn molad_print(year: i32) {
303    let (day, hour, parts) = molad_components(year);
304    let day = day % 7;
305    let hour = (hour + 18) % 24;
306    let minute = parts / 18;
307    let parts = parts % 18;
308
309    let days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
310    let day_str = days[day as usize];
311    println!("Molad {year}: {day_str} {hour:0>2}:{minute:0>2} and {parts:>2} \
312              chalakim");
313}
314
315/// Calculates what day (since epoch) the year starts
316pub fn year_start(year: i32) -> i32 {
317    let molad = molad(year);
318    let day   = molad.div_euclid(1080*24) as i32;
319    let parts = molad.rem_euclid(1080*24) as i32;
320    let mut rosh = day;
321    // first rule: if molad is after noon (18 hours after 6pm), Rosh Hashana is
322    // postponed 1 day
323    if parts >= 18*1080 {
324        rosh += 1;
325    }
326    // second rule: lo ADU
327    if rosh % 7 == 0 || rosh % 7 == 3 || rosh % 7 == 5 {
328        rosh += 1;
329    }
330    // third rule: Ga-Ta-RaD
331    if !is_leap_year(year) && day % 7 == 2 && parts >= 9*1080+204 {
332        rosh = day+2;
333    }
334    // fourth rule: Be-TU-TeKaPoT
335    if is_leap_year(year-1) && day % 7 == 1 && parts >= 15*1080+589 {
336        rosh = day+1;
337    }
338    return rosh;
339}
340
341/// Calculates the number of days in the Jewish year
342pub fn year_length(year: i32) -> i32 {
343    let rosh1 = year_start(year);
344    let rosh2 = year_start(year+1);
345    return rosh2-rosh1;
346}
347
348/// Returns a list of months with the number of days in them
349pub fn year_months(year: i32) -> [u8; 14] {
350    let mut days_in_month = [0, 30, 29, 30, 29, 30, 29,     // Nisan - Elul
351                                30, 29, 30, 29, 30, 29, 0]; // Tishrei - Adar2
352    if is_leap_year(year) {
353        days_in_month[12] = 30; days_in_month[13] = 29;
354    }
355    let length = year_length(year);
356    if length % 10 == 3 {
357        // Deficient year
358        days_in_month[9] = 29;
359    }
360    if length % 10 == 5 {
361        // Complete year
362        days_in_month[8] = 30;
363    }
364    return days_in_month;
365}
366
367#[cfg(test)]
368mod tests {
369    use super::*;
370
371    // helpers
372
373    fn gdate(year: i32, month: u8, day: u8) -> GDate {
374        GDate::new(year, month, day).unwrap()
375    }
376
377    fn jdate(year: i32, month: u8, day: u8) -> JDate {
378        JDate::new(year, month, day).unwrap()
379    }
380
381    fn check_roundtrip(g: GDate, j: JDate) {
382        assert_eq!(JDate::from(g), j);
383        assert_eq!(GDate::from(j), g);
384    }
385
386
387    #[test]
388    fn test_molad_year() {
389        assert_eq!(molad_components(1), (1, 5, 204));
390        assert_eq!(molad_components(5785), (2112590, 9, 391));
391    }
392
393    #[test]
394    fn test_leap_year() {
395        assert!(is_leap_year(5700));
396        assert!(!is_leap_year(5701));
397        assert!(!is_leap_year(5702));
398        assert!(is_leap_year(5703));
399        assert!(is_leap_year(5782));
400        assert!(!is_leap_year(5783));
401        assert!(is_leap_year(5784));
402        assert!(!is_leap_year(5785));
403        assert!(!is_leap_year(5786));
404        assert!(is_leap_year(5787));
405    }
406
407    #[test]
408    fn test_jdate_new() {
409        // valid dates
410        assert!(JDate::new(5785,  1,  1).is_some());
411        assert!(JDate::new(5784, 12, 30).is_some());
412        assert!(JDate::new(5784, 13,  1).is_some());
413        assert!(JDate::new(5786,  9, 30).is_some());
414        assert!(JDate::new(5787,  9, 30).is_some());
415        // invalid dates
416        assert!(JDate::new(5785, 13,  1).is_none());
417        assert!(JDate::new(5785,  0,  1).is_none());
418        assert!(JDate::new(5785, 14,  1).is_none());
419        assert!(JDate::new(5785,  1,  0).is_none());
420        assert!(JDate::new(5785,  1, 31).is_none());
421        assert!(JDate::new(5785,  2, 30).is_none());
422        assert!(JDate::new(5785, 12, 30).is_none());
423        assert!(JDate::new(5785, 13, 30).is_none());
424        assert!(JDate::new(5781,  8, 30).is_none());
425        assert!(JDate::new(5781,  9, 30).is_none());
426        assert!(JDate::new(5786,  8, 30).is_none());
427    }
428
429    #[test]
430    fn test_roundtrip() {
431        check_roundtrip(gdate(    1, 1,  1), jdate(3761, 10, 18));
432        check_roundtrip(gdate(-3760, 9,  7), jdate(   1,  7,  1));
433        check_roundtrip(gdate(2024, 12, 31), jdate(5785,  9, 30));
434        check_roundtrip(gdate(2025,  1,  1), jdate(5785, 10,  1));
435        check_roundtrip(gdate(2025,  2,  1), jdate(5785, 11,  3));
436        check_roundtrip(gdate(2025,  3,  1), jdate(5785, 12,  1));
437        check_roundtrip(gdate(2024,  2, 10), jdate(5784, 12,  1));
438        check_roundtrip(gdate(2024,  3, 11), jdate(5784, 13,  1));
439        check_roundtrip(gdate(2024,  4,  9), jdate(5784,  1,  1));
440        check_roundtrip(gdate(2024, 10,  2), jdate(5784,  6, 29));
441        check_roundtrip(gdate(2024, 10,  3), jdate(5785,  7,  1));
442    }
443
444    #[test]
445    fn test_dafyomi() {
446        assert_eq!(jdate(5772,  5, 15).dafyomi(), ("Berachos", 2));
447        assert_eq!(jdate(5780, 10,  8).dafyomi(), ("Berachos", 2));
448        assert_eq!(jdate(5780, 12, 11).dafyomi(), ("Berachos", 64));
449        assert_eq!(jdate(5780, 12, 12).dafyomi(), ("Shabbos", 2));
450        assert_eq!(jdate(5784,  7,  6).dafyomi(), ("Kidushin", 39));
451        assert_eq!(jdate(5787,  3,  2).dafyomi(), ("Nidah", 73));
452    }
453
454    #[test]
455    fn test_omer() {
456        assert_eq!(jdate(5785,  1, 15).omer(), 0);
457        assert_eq!(jdate(5785,  1, 16).omer(), 1);
458        assert_eq!(jdate(5785,  1, 30).omer(), 15);
459        assert_eq!(jdate(5785,  2,  1).omer(), 16);
460        assert_eq!(jdate(5785,  2, 29).omer(), 44);
461        assert_eq!(jdate(5785,  3,  1).omer(), 45);
462        assert_eq!(jdate(5785,  3,  5).omer(), 49);
463        assert_eq!(jdate(5785,  3,  6).omer(), 0);
464        assert_eq!(jdate(5785,  4,  1).omer(), 0);
465        assert_eq!(jdate(5785,  1,  1).omer(), 0);
466    }
467}