jdate/
lib.rs

1use time::{OffsetDateTime, Date};
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/// An easier way to create a time::Date object
9pub fn gdate(year: i32, month: u8, day: u8) -> Option<Date> {
10    return Date::from_calendar_date(year, month.try_into().unwrap(), day).ok();
11}
12
13/// Jewish Date representation
14#[derive(Debug, Clone, Copy, PartialEq)]
15pub struct JDate {
16    year: i32,
17    month: u8, // 1 = Nisan, 13 = Adar2
18    day: u8, // 1 - 30
19}
20
21impl JDate {
22    /// Create a new JDate with year, month, day. If the date is invalid: None
23    /// is returned.
24    pub fn new(year: i32, month: u8, day: u8) -> Option<JDate> {
25        if !date_is_valid(year, month, day) {
26            return None;
27        }
28        Some(JDate{year, month, day})
29    }
30
31    /// Create a new JDate using a Julian Day number
32    pub fn from_jd(jd: i32) -> JDate {
33        let ed = jd - 347997; // days since epoch
34        let mut year = ed * 100 / 36525;
35        while year_start(year) < ed {year += 1;}
36        while year_start(year) > ed {year -= 1;}
37        let mut days = year_start(year);
38        let days_in_month = year_months(year);
39        let mut month = 7;
40        loop {
41            let length = days_in_month[month] as i32;
42            if days + length > ed {break}
43            month += 1;
44            if month == 14 {month = 1}
45            if month == 7 {unreachable!()}
46            days += length;
47        }
48        return JDate{
49            year: year,
50            month: month as u8,
51            day: (ed-days+1) as u8
52        };
53    }
54
55    /// Get the Julian Day number
56    pub fn to_jd(self: Self) -> i32 {
57        let mut ed = year_start(self.year) - 1;
58        let days_in_month = year_months(self.year);
59        let mut month: u8 = 7;
60        loop {
61            let length = days_in_month[month as usize];
62            if month == self.month {
63                ed += self.day as i32;
64                break;
65            }
66            ed += length as i32;
67            month += 1;
68            if month == 14 {month = 1}
69            if month == 7 {unreachable!()}
70        }
71        return ed + 347997;
72    }
73
74
75    /// Get the year component
76    pub fn year(self: Self) -> i32 {return self.year}
77    /// Get the month component
78    pub fn month(self: Self) -> u8 {return self.month}
79    /// Get the day component
80    pub fn day(self: Self) -> u8 {return self.day}
81
82    /// Get the month as a string
83    pub fn month_name(self: Self) -> &'static str {
84        const NAMES: [&str; 13] = [
85            "Nisan", "Iyar", "Sivan", "Tamuz", "Av", "Elul",
86            "Tishrei", "Cheshvan", "Kislev", "Tevet", "Shvat", "Adar", "Adar2"];
87        if self.month == 12 && is_leap_year(self.year) {
88            return "Adar1"
89        }
90        return NAMES[self.month as usize - 1];
91
92    }
93}
94
95impl fmt::Display for JDate {
96    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
97        write!(f, "{:0>4}-{}-{:0>2}", self.year, self.month_name(), self.day)
98    }
99}
100
101impl From<Date> for JDate {
102    /// Convert Gregorian Date to JDate
103    fn from(d: Date) -> Self {
104        let jd = d.to_julian_day();
105        return JDate::from_jd(jd);
106    }
107}
108
109impl From<JDate> for Date {
110    /// Convert JDate to Gregorian Date
111    fn from(d: JDate) -> Self {
112        let jd = d.to_jd();
113        return Date::from_julian_day(jd).unwrap();
114    }
115}
116
117/// Get today's local date
118pub fn today() -> Date {
119    let now = OffsetDateTime::now_local().unwrap_or(OffsetDateTime::now_utc());
120    return now.date();
121}
122
123/// Determine if the Jewish year is a leap year
124pub fn is_leap_year(year: i32) -> bool {
125    match year % 19 {
126        0|3|6|8|11|14|17 => true,
127        _ => false
128    }
129}
130
131/// Determine if the Jewish date is valid
132pub fn date_is_valid(year: i32, month: u8, day: u8) -> bool {
133    if month < 1 || month > 13 || day < 1 || day > 30 {
134        return false;
135    }
136    if month == 13 {
137        return day <= 29 && is_leap_year(year);
138    }
139    if day == 30 {
140        return match month {
141            1|3|5|7|11 => true,
142            2|4|6|10 => false,
143            12 => is_leap_year(year),
144            8 => {
145                let len = year_length(year);
146                len % 10 == 5 // complete year (355 or 385)
147            },
148            9 => {
149                let len = year_length(year);
150                len % 10 >= 4 // complete or regular year (354 or 384)
151            },
152            _ => unreachable!()
153        }
154    }
155    true
156}
157
158/// Calculates the molad of the given year
159///
160/// returns the number of chalakim since the epoch
161pub fn molad(year: i32) -> i64 {
162    let parts_month = (29*24+12)*1080+793;
163    let parts_year  = 12 * parts_month;
164    let parts_lyear = 13 * parts_month;
165    let parts_cycle = 12 * parts_year + 7 * parts_lyear;
166    let total_cycles  = (year-1).div_euclid(19);
167    let year_in_cycle = (year-1).rem_euclid(19);
168    let mut molad: i64 = (24+5)*1080+204; // molad tohu
169    molad += total_cycles as i64 * parts_cycle as i64;
170    for year in 0..year_in_cycle {
171        if is_leap_year(year+1) {
172            molad += parts_lyear as i64;
173        } else {
174            molad += parts_year as i64;
175        }
176    }
177    return molad;
178}
179
180/// Calculates the molad of the given year
181///
182/// returns the days since epoch, hours (after 6pm), and parts (out of 1080
183/// chalakim)
184pub fn molad_components(year: i32) -> (i32, u8, u16) {
185    let molad = molad(year);
186    return ((molad.div_euclid(1080*24)) as i32,
187            (molad.div_euclid(1080).rem_euclid(24)) as u8,
188            (molad.rem_euclid(1080)) as u16);
189}
190
191/// Print molad in a friendly format
192pub fn molad_print(year: i32) {
193    let (day, hour, parts) = molad_components(year);
194    let day = day % 7;
195    let hour = (hour + 18) % 24;
196    let minute = parts / 18;
197    let parts = parts % 18;
198
199    let days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
200    let day_str = days[day as usize];
201    println!("Molad {year}: {day_str} {hour:0>2}:{minute:0>2} and {parts:>2} \
202              chalakim");
203}
204
205/// Calculates what day (since epoch) the year starts
206pub fn year_start(year: i32) -> i32 {
207    let molad = molad(year);
208    let day   = molad.div_euclid(1080*24) as i32;
209    let parts = molad.rem_euclid(1080*24);
210    let mut rosh = day;
211    // first rule: if molad is after noon (18 hours after 6pm), Rosh Hashana is
212    // postponed 1 day
213    if parts >= 18*1080 {
214        rosh += 1;
215    }
216    // second rule: lo ADU
217    if rosh % 7 == 0 || rosh % 7 == 3 || rosh % 7 == 5 {
218        rosh += 1;
219    }
220    // third rule: Ga-Ta-RaD
221    if !is_leap_year(year) && day % 7 == 2 && parts >= 9*1080+204 {
222        rosh = day+2;
223    }
224    // fourth rule: Be-TU-TeKaPoT
225    if is_leap_year(year-1) && day % 7 == 1 && parts >= 15*1080+589 {
226        rosh = day+1;
227    }
228    return rosh;
229}
230
231/// Calculates the number of days in the Jewish year
232pub fn year_length(year: i32) -> i32 {
233    let rosh1 = year_start(year);
234    let rosh2 = year_start(year+1);
235    return rosh2-rosh1;
236}
237
238/// Returns a list of months with the number of days in them
239pub fn year_months(year: i32) -> [u8; 14] {
240    let mut days_in_month = [0, 30, 29, 30, 29, 30, 29,     // Nisan - Elul
241                                30, 29, 30, 29, 30, 29, 0]; // Tishrei - Adar2
242    if is_leap_year(year) {
243        days_in_month[12] = 30; days_in_month[13] = 29;
244    }
245    let length = year_length(year);
246    if length % 10 == 3 {
247        // Deficient year
248        days_in_month[9] = 29;
249    }
250    if length % 10 == 5 {
251        // Complete year
252        days_in_month[8] = 30;
253    }
254    return days_in_month;
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260
261    #[test]
262    fn test_molad_year() {
263        let tod = today();
264        println!("{:?}", tod);
265        assert_eq!(molad_components(1), (1, 5, 204));
266        assert_eq!(molad_components(5785), (2112590, 9, 391));
267    }
268
269    #[test]
270    fn test_leap_year() {
271        assert!(is_leap_year(5700));
272        assert!(!is_leap_year(5701));
273        assert!(!is_leap_year(5702));
274        assert!(is_leap_year(5703));
275        assert!(is_leap_year(5782));
276        assert!(!is_leap_year(5783));
277        assert!(is_leap_year(5784));
278        assert!(!is_leap_year(5785));
279        assert!(!is_leap_year(5786));
280        assert!(is_leap_year(5787));
281    }
282
283    #[test]
284    fn test_from_greg() {
285        assert_eq!(JDate::from(gdate(1, 1, 1).unwrap()),
286                   JDate::new(3761, 10, 18).unwrap());
287        assert_eq!(JDate::from(gdate(-3760, 9,  7).unwrap()),
288                   JDate::new(1, 7, 1).unwrap());
289        assert_eq!(JDate::from(gdate(2024, 12, 31).unwrap()),
290                   JDate::new(5785, 9, 30).unwrap());
291        assert_eq!(JDate::from(gdate(2025,  1,  1).unwrap()),
292                   JDate::new(5785, 10, 1).unwrap());
293        assert_eq!(JDate::from(gdate(2025,  2,  1).unwrap()),
294                   JDate::new(5785, 11, 3).unwrap());
295        assert_eq!(JDate::from(gdate(2025,  3,  1).unwrap()),
296                   JDate::new(5785, 12, 1).unwrap());
297        assert_eq!(JDate::from(gdate(2024,  2, 10).unwrap()),
298                   JDate::new(5784, 12, 1).unwrap());
299        assert_eq!(JDate::from(gdate(2024,  3, 11).unwrap()),
300                   JDate::new(5784, 13, 1).unwrap());
301        assert_eq!(JDate::from(gdate(2024,  4,  9).unwrap()),
302                   JDate::new(5784, 1, 1).unwrap());
303        assert_eq!(JDate::from(gdate(2024, 10,  2).unwrap()),
304                   JDate::new(5784, 6, 29).unwrap());
305        assert_eq!(JDate::from(gdate(2024, 10,  3).unwrap()),
306                   JDate::new(5785, 7, 1).unwrap());
307    }
308
309    #[test]
310    fn test_from_jdate() {
311        assert_eq!(Date::from(JDate::new(3761, 10, 18).unwrap()),
312                   gdate(1, 1, 1).unwrap());
313        assert_eq!(Date::from(JDate::new(1, 7, 1).unwrap()),
314                   gdate(-3760, 9, 7).unwrap());
315        assert_eq!(Date::from(JDate::new(5785, 9, 30).unwrap()),
316                   gdate(2024, 12, 31).unwrap());
317        assert_eq!(Date::from(JDate::new(5785, 10, 1).unwrap()),
318                   gdate(2025, 1, 1).unwrap());
319        assert_eq!(Date::from(JDate::new(5785, 11, 3).unwrap()),
320                   gdate(2025, 2, 1).unwrap());
321        assert_eq!(Date::from(JDate::new(5785, 12, 1).unwrap()),
322                   gdate(2025, 3, 1).unwrap());
323        assert_eq!(Date::from(JDate::new(5784, 12, 1).unwrap()),
324                   gdate(2024, 2, 10).unwrap());
325        assert_eq!(Date::from(JDate::new(5784, 13, 1).unwrap()),
326                   gdate(2024, 3, 11).unwrap());
327        assert_eq!(Date::from(JDate::new(5784, 1, 1).unwrap()),
328                   gdate(2024, 4, 9).unwrap());
329        assert_eq!(Date::from(JDate::new(5784, 6, 29).unwrap()),
330                   gdate(2024, 10, 2).unwrap());
331        assert_eq!(Date::from(JDate::new(5785, 7, 1).unwrap()),
332                   gdate(2024, 10, 3).unwrap());
333    }
334}