1use crate::helpers::{final_func, i64_to_i32, next_u8};
11use crate::rata_die::{Moment, RataDie};
12#[allow(unused_imports)]
13use core_maths::*;
14
15pub(crate) const FIXED_HEBREW_EPOCH: RataDie =
17 crate::julian::fixed_from_julian_book_version(-3761, 10, 7);
18
19#[derive(Copy, Clone, Debug, Default, Hash, Eq, PartialEq, PartialOrd, Ord)]
22#[allow(clippy::exhaustive_structs)]
23pub struct BookHebrew {
24 pub year: i32,
26 pub month: u8,
28 pub day: u8,
30}
31
32pub const NISAN: u8 = 1;
35pub const IYYAR: u8 = 2;
37pub const SIVAN: u8 = 3;
39pub const TAMMUZ: u8 = 4;
41pub const AV: u8 = 5;
43pub const ELUL: u8 = 6;
45pub const TISHRI: u8 = 7;
47pub const MARHESHVAN: u8 = 8;
49pub const KISLEV: u8 = 9;
51pub const TEVET: u8 = 10;
53pub const SHEVAT: u8 = 11;
55pub const ADAR: u8 = 12;
57pub const ADARII: u8 = 13;
59
60impl BookHebrew {
63 pub fn to_civil_date(self) -> (i32, u8, u8) {
66 let biblical_month = self.month;
67 let biblical_year = self.year;
68 let mut civil_month;
69 civil_month = (biblical_month + 6) % 12;
70
71 if civil_month == 0 {
72 civil_month = 12;
73 }
74
75 if Self::is_hebrew_leap_year(biblical_year) && biblical_month < TISHRI {
76 civil_month += 1;
77 }
78 (biblical_year, civil_month, self.day)
79 }
80
81 pub fn from_civil_date(civil_year: i32, civil_month: u8, civil_day: u8) -> Self {
84 let mut biblical_month;
85
86 if civil_month <= 6 {
87 biblical_month = civil_month + 6; } else {
89 biblical_month = civil_month - 6; if Self::is_hebrew_leap_year(civil_year) {
91 biblical_month -= 1
92 }
93 if biblical_month == 0 {
94 biblical_month = 13;
96 }
97 }
98
99 BookHebrew {
100 year: civil_year,
101 month: biblical_month,
102 day: civil_day,
103 }
104 }
105 #[allow(dead_code)]
108 pub(crate) fn molad(book_year: i32, book_month: u8) -> Moment {
109 let y = if book_month < TISHRI {
110 book_year + 1
111 } else {
112 book_year
113 }; let months_elapsed = (book_month as f64 - TISHRI as f64) + ((235.0 * y as f64 - 234.0) / 19.0).floor(); Moment::new(
119 FIXED_HEBREW_EPOCH.to_f64_date() - (876.0 / 25920.0)
120 + months_elapsed * (29.0 + (1.0 / 2.0) + (793.0 / 25920.0)),
121 )
122 }
123
124 #[allow(dead_code)]
127 fn last_month_of_book_hebrew_year(book_year: i32) -> u8 {
128 if Self::is_hebrew_leap_year(book_year) {
129 ADARII
130 } else {
131 ADAR
132 }
133 }
134
135 fn book_hebrew_calendar_elapsed_days(book_year: i32) -> i32 {
138 let months_elapsed = ((235.0 * book_year as f64 - 234.0) / 19.0).floor() as i64;
139 let parts_elapsed = 12084 + 13753 * months_elapsed;
140 let days = 29 * months_elapsed + (parts_elapsed as f64 / 25920.0).floor() as i64;
141
142 if (3 * (days + 1)).rem_euclid(7) < 3 {
143 days as i32 + 1
144 } else {
145 days as i32
146 }
147 }
148
149 fn book_hebrew_year_length_correction(book_year: i32) -> u8 {
152 let ny0 = Self::book_hebrew_calendar_elapsed_days(book_year - 1);
153 let ny1 = Self::book_hebrew_calendar_elapsed_days(book_year);
154 let ny2 = Self::book_hebrew_calendar_elapsed_days(book_year + 1);
155
156 if (ny2 - ny1) == 356 {
157 2
158 } else if (ny1 - ny0) == 382 {
159 1
160 } else {
161 0
162 }
163 }
164
165 pub fn book_hebrew_new_year(book_year: i32) -> RataDie {
168 RataDie::new(
169 FIXED_HEBREW_EPOCH.to_i64_date()
170 + Self::book_hebrew_calendar_elapsed_days(book_year) as i64
171 + Self::book_hebrew_year_length_correction(book_year) as i64,
172 )
173 }
174
175 pub fn days_in_book_hebrew_year(book_year: i32) -> u16 {
177 (Self::book_hebrew_new_year(1 + book_year) - Self::book_hebrew_new_year(book_year)) as u16
178 }
179
180 pub fn is_hebrew_leap_year(book_year: i32) -> bool {
182 (7 * book_year + 1).rem_euclid(19) < 7
183 }
184
185 #[allow(dead_code)]
188 fn is_long_marheshvan(book_year: i32) -> bool {
189 let long_marheshavan_year_lengths = [355, 385];
190 long_marheshavan_year_lengths.contains(&Self::days_in_book_hebrew_year(book_year))
191 }
192
193 #[allow(dead_code)]
196 fn is_short_kislev(book_year: i32) -> bool {
197 let short_kislev_year_lengths = [353, 383];
198 short_kislev_year_lengths.contains(&Self::days_in_book_hebrew_year(book_year))
199 }
200
201 pub fn last_day_of_book_hebrew_month(book_year: i32, book_month: u8) -> u8 {
204 match book_month {
205 IYYAR | TAMMUZ | ELUL | TEVET | ADARII => 29,
206 ADAR => {
207 if !Self::is_hebrew_leap_year(book_year) {
208 29
209 } else {
210 30
211 }
212 }
213 MARHESHVAN => {
214 if !Self::is_long_marheshvan(book_year) {
215 29
216 } else {
217 30
218 }
219 }
220 KISLEV => {
221 if Self::is_short_kislev(book_year) {
222 29
223 } else {
224 30
225 }
226 }
227 _ => 30,
228 }
229 }
230
231 pub fn fixed_from_book_hebrew(date: BookHebrew) -> RataDie {
233 let book_year = date.year;
234 let book_month = date.month;
235 let book_day = date.day;
236
237 let mut total_days = Self::book_hebrew_new_year(book_year) + book_day.into() - 1; if book_month < TISHRI {
240 for m in
242 (TISHRI..=Self::last_month_of_book_hebrew_year(book_year)).chain(NISAN..book_month)
243 {
244 total_days += Self::last_day_of_book_hebrew_month(book_year, m).into();
245 }
246 } else {
247 for m in TISHRI..book_month {
249 total_days += Self::last_day_of_book_hebrew_month(book_year, m).into();
250 }
251 }
252
253 total_days
254 }
255
256 pub fn book_hebrew_from_fixed(date: RataDie) -> BookHebrew {
258 let approx = i64_to_i32(
259 1 + ((date - FIXED_HEBREW_EPOCH) as f64).div_euclid(35975351.0 / 98496.0) as i64, )
261 .unwrap_or_else(|e| e.saturate());
262
263 let year_condition = |year: i32| Self::book_hebrew_new_year(year) <= date;
265 let year = final_func(approx - 1, year_condition);
266
267 let start = if date
269 < Self::fixed_from_book_hebrew(BookHebrew {
270 year,
271 month: NISAN,
272 day: 1,
273 }) {
274 TISHRI
275 } else {
276 NISAN
277 };
278
279 let month_condition = |m: u8| {
280 date <= Self::fixed_from_book_hebrew(BookHebrew {
281 year,
282 month: m,
283 day: Self::last_day_of_book_hebrew_month(year, m),
284 })
285 };
286 let month = next_u8(start, month_condition);
288
289 let day = (date
291 - Self::fixed_from_book_hebrew(BookHebrew {
292 year,
293 month,
294 day: 1,
295 }))
296 + 1;
297
298 BookHebrew {
299 year,
300 month,
301 day: day as u8,
302 }
303 }
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309
310 #[derive(Debug)]
311 struct DateCase {
312 year: i32,
313 month: u8,
314 day: u8,
315 }
316
317 static TEST_FIXED_DATE: [i64; 33] = [
318 -214193, -61387, 25469, 49217, 171307, 210155, 253427, 369740, 400085, 434355, 452605,
319 470160, 473837, 507850, 524156, 544676, 567118, 569477, 601716, 613424, 626596, 645554,
320 664224, 671401, 694799, 704424, 708842, 709409, 709580, 727274, 728714, 744313, 764652,
321 ];
322
323 static HEBREW_DATES: [DateCase; 33] = [
324 DateCase {
325 year: 3174,
326 month: 5,
327 day: 10,
328 },
329 DateCase {
330 year: 3593,
331 month: 9,
332 day: 25,
333 },
334 DateCase {
335 year: 3831,
336 month: 7,
337 day: 3,
338 },
339 DateCase {
340 year: 3896,
341 month: 7,
342 day: 9,
343 },
344 DateCase {
345 year: 4230,
346 month: 10,
347 day: 18,
348 },
349 DateCase {
350 year: 4336,
351 month: 3,
352 day: 4,
353 },
354 DateCase {
355 year: 4455,
356 month: 8,
357 day: 13,
358 },
359 DateCase {
360 year: 4773,
361 month: 2,
362 day: 6,
363 },
364 DateCase {
365 year: 4856,
366 month: 2,
367 day: 23,
368 },
369 DateCase {
370 year: 4950,
371 month: 1,
372 day: 7,
373 },
374 DateCase {
375 year: 5000,
376 month: 13,
377 day: 8,
378 },
379 DateCase {
380 year: 5048,
381 month: 1,
382 day: 21,
383 },
384 DateCase {
385 year: 5058,
386 month: 2,
387 day: 7,
388 },
389 DateCase {
390 year: 5151,
391 month: 4,
392 day: 1,
393 },
394 DateCase {
395 year: 5196,
396 month: 11,
397 day: 7,
398 },
399 DateCase {
400 year: 5252,
401 month: 1,
402 day: 3,
403 },
404 DateCase {
405 year: 5314,
406 month: 7,
407 day: 1,
408 },
409 DateCase {
410 year: 5320,
411 month: 12,
412 day: 27,
413 },
414 DateCase {
415 year: 5408,
416 month: 3,
417 day: 20,
418 },
419 DateCase {
420 year: 5440,
421 month: 4,
422 day: 3,
423 },
424 DateCase {
425 year: 5476,
426 month: 5,
427 day: 5,
428 },
429 DateCase {
430 year: 5528,
431 month: 4,
432 day: 4,
433 },
434 DateCase {
435 year: 5579,
436 month: 5,
437 day: 11,
438 },
439 DateCase {
440 year: 5599,
441 month: 1,
442 day: 12,
443 },
444 DateCase {
445 year: 5663,
446 month: 1,
447 day: 22,
448 },
449 DateCase {
450 year: 5689,
451 month: 5,
452 day: 19,
453 },
454 DateCase {
455 year: 5702,
456 month: 7,
457 day: 8,
458 },
459 DateCase {
460 year: 5703,
461 month: 1,
462 day: 14,
463 },
464 DateCase {
465 year: 5704,
466 month: 7,
467 day: 8,
468 },
469 DateCase {
470 year: 5752,
471 month: 13,
472 day: 12,
473 },
474 DateCase {
475 year: 5756,
476 month: 12,
477 day: 5,
478 },
479 DateCase {
480 year: 5799,
481 month: 8,
482 day: 12,
483 },
484 DateCase {
485 year: 5854,
486 month: 5,
487 day: 5,
488 },
489 ];
490
491 static EXPECTED_MOLAD_DATES: [f64; 33] = [
492 -1850718767f64 / 8640f64,
493 -1591805959f64 / 25920f64,
494 660097927f64 / 25920f64,
495 1275506059f64 / 25920f64,
496 4439806081f64 / 25920f64,
497 605235101f64 / 2880f64,
498 3284237627f64 / 12960f64,
499 9583515841f64 / 25920f64,
500 2592403883f64 / 6480f64,
501 2251656649f64 / 5184f64,
502 11731320839f64 / 25920f64,
503 12185988041f64 / 25920f64,
504 6140833583f64 / 12960f64,
505 6581722991f64 / 12960f64,
506 6792982499f64 / 12960f64,
507 4705980311f64 / 8640f64,
508 14699670013f64 / 25920f64,
509 738006961f64 / 1296f64,
510 1949499007f64 / 3240f64,
511 5299956319f64 / 8640f64,
512 3248250415f64 / 5184f64,
513 16732660061f64 / 25920f64,
514 17216413717f64 / 25920f64,
515 1087650871f64 / 1620f64,
516 2251079609f64 / 3240f64,
517 608605601f64 / 864f64,
518 306216383f64 / 432f64,
519 18387526207f64 / 25920f64,
520 3678423761f64 / 5184f64,
521 1570884431f64 / 2160f64,
522 18888119389f64 / 25920f64,
523 19292268013f64 / 25920f64,
524 660655045f64 / 864f64,
525 ];
526
527 static EXPECTED_LAST_HEBREW_MONTH: [u8; 33] = [
528 12, 12, 12, 12, 12, 12, 12, 12, 13, 12, 13, 12, 12, 12, 12, 13, 12, 13, 12, 13, 12, 12, 12,
529 12, 12, 13, 12, 13, 12, 13, 12, 12, 12,
530 ];
531
532 static EXPECTED_HEBREW_ELASPED_CALENDAR_DAYS: [i32; 33] = [
533 1158928, 1311957, 1398894, 1422636, 1544627, 1583342, 1626812, 1742956, 1773254, 1807597,
534 1825848, 1843388, 1847051, 1881010, 1897460, 1917895, 1940545, 1942729, 1974889, 1986554,
535 1999723, 2018712, 2037346, 2044640, 2068027, 2077507, 2082262, 2082617, 2083000, 2100511,
536 2101988, 2117699, 2137779,
537 ];
538
539 static EXPECTED_FIXED_HEBREW_NEW_YEAR: [i64; 33] = [
540 -214497, -61470, 25467, 49209, 171200, 209915, 253385, 369529, 399827, 434172, 452421,
541 469963, 473624, 507583, 524033, 544468, 567118, 569302, 601462, 613127, 626296, 645285,
542 663919, 671213, 694600, 704080, 708835, 709190, 709573, 727084, 728561, 744272, 764352,
543 ];
544
545 static EXPECTED_DAYS_IN_HEBREW_YEAR: [u16; 33] = [
546 354, 354, 355, 355, 355, 355, 355, 353, 383, 354, 383, 354, 354, 355, 353, 383, 353, 385,
547 353, 383, 355, 354, 354, 354, 355, 385, 355, 383, 354, 385, 355, 354, 355,
548 ];
549
550 static EXPECTED_MARHESHVAN_VALUES: [bool; 33] = [
551 false, false, true, true, true, true, true, false, false, false, false, false, false, true,
552 false, false, false, true, false, false, true, false, false, false, true, true, true,
553 false, false, true, true, false, true,
554 ];
555
556 static EXPECTED_KISLEV_VALUES: [bool; 33] = [
557 false, false, false, false, false, false, false, true, true, false, true, false, false,
558 false, true, true, true, false, true, true, false, false, false, false, false, false,
559 false, true, false, false, false, false, false,
560 ];
561
562 static EXPECTED_DAY_IN_MONTH: [u8; 33] = [
563 30, 30, 30, 30, 29, 30, 30, 29, 29, 30, 29, 30, 29, 29, 30, 30, 30, 30, 30, 29, 30, 29, 30,
564 30, 30, 30, 30, 30, 30, 29, 29, 29, 30,
565 ];
566
567 #[allow(dead_code)]
568 static CIVIL_EXPECTED_DAY_IN_MONTH: [u8; 33] = [
569 30, 30, 30, 30, 29, 30, 29, 29, 29, 30, 30, 30, 29, 29, 30, 29, 30, 29, 29, 30, 30, 29, 30,
570 30, 30, 30, 30, 29, 30, 30, 29, 29, 30,
571 ];
572
573 #[test]
574 fn test_hebrew_epoch() {
575 let fixed_hebrew_date = -1373427.0;
577 assert_eq!(FIXED_HEBREW_EPOCH.to_f64_date(), fixed_hebrew_date);
578 }
579
580 #[test]
581 fn test_hebrew_molad() {
582 let precision = 1_00000f64;
583 for (case, expected) in HEBREW_DATES.iter().zip(EXPECTED_MOLAD_DATES.iter()) {
584 let molad =
585 (BookHebrew::molad(case.year, case.month).inner() * precision).round() / precision;
586 let final_expected = (expected * precision).round() / precision;
587 assert_eq!(molad, final_expected, "{case:?}");
588 }
589 }
590
591 #[test]
592 fn test_last_book_hebrew_month() {
593 for (case, expected) in HEBREW_DATES.iter().zip(EXPECTED_LAST_HEBREW_MONTH.iter()) {
594 let last_month = BookHebrew::last_month_of_book_hebrew_year(case.year);
595 assert_eq!(last_month, *expected);
596 }
597 }
598
599 #[test]
600 fn test_book_hebrew_calendar_elapsed_days() {
601 for (case, expected) in HEBREW_DATES
602 .iter()
603 .zip(EXPECTED_HEBREW_ELASPED_CALENDAR_DAYS.iter())
604 {
605 let elapsed_days = BookHebrew::book_hebrew_calendar_elapsed_days(case.year);
606 assert_eq!(elapsed_days, *expected);
607 }
608 }
609
610 #[test]
611 fn test_book_hebrew_year_length_correction() {
612 let year_length_correction: [u8; 33] = [
613 2, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
614 0, 0, 0, 0,
615 ];
616 for (case, expected) in HEBREW_DATES.iter().zip(year_length_correction.iter()) {
617 let correction = BookHebrew::book_hebrew_year_length_correction(case.year);
618 assert_eq!(correction, *expected);
619 }
620 }
621
622 #[test]
623 fn test_book_hebrew_new_year() {
624 for (case, expected) in HEBREW_DATES
625 .iter()
626 .zip(EXPECTED_FIXED_HEBREW_NEW_YEAR.iter())
627 {
628 let f_date = BookHebrew::book_hebrew_new_year(case.year);
629 assert_eq!(f_date.to_i64_date(), *expected);
630 }
631 }
632
633 #[test]
634 fn test_days_in_book_hebrew_year() {
635 for (case, expected) in HEBREW_DATES.iter().zip(EXPECTED_DAYS_IN_HEBREW_YEAR.iter()) {
636 let days_in_year = BookHebrew::days_in_book_hebrew_year(case.year);
637 assert_eq!(days_in_year, *expected);
638 }
639 }
640
641 #[test]
642 fn test_long_marheshvan() {
643 for (case, expected) in HEBREW_DATES.iter().zip(EXPECTED_MARHESHVAN_VALUES.iter()) {
644 let marsheshvan = BookHebrew::is_long_marheshvan(case.year);
645 assert_eq!(marsheshvan, *expected);
646 }
647 }
648
649 #[test]
650 fn test_short_kislev() {
651 for (case, expected) in HEBREW_DATES.iter().zip(EXPECTED_KISLEV_VALUES.iter()) {
652 let kislev = BookHebrew::is_short_kislev(case.year);
653 assert_eq!(kislev, *expected);
654 }
655 }
656
657 #[test]
658 fn test_last_day_in_book_hebrew_month() {
659 for (case, expected) in HEBREW_DATES.iter().zip(EXPECTED_DAY_IN_MONTH.iter()) {
660 let days_in_month = BookHebrew::last_day_of_book_hebrew_month(case.year, case.month);
661 assert_eq!(days_in_month, *expected);
662 }
663 }
664
665 #[test]
666 fn test_fixed_from_book_hebrew() {
667 for (case, f_date) in HEBREW_DATES.iter().zip(TEST_FIXED_DATE.iter()) {
668 assert_eq!(
669 BookHebrew::fixed_from_book_hebrew(BookHebrew {
670 year: case.year,
671 month: case.month,
672 day: case.day
673 }),
674 RataDie::new(*f_date),
675 "{case:?}"
676 );
677 }
678 }
679
680 #[test]
681 fn test_book_hebrew_from_fixed() {
682 for (case, f_date) in HEBREW_DATES.iter().zip(TEST_FIXED_DATE.iter()) {
683 assert_eq!(
684 BookHebrew::book_hebrew_from_fixed(RataDie::new(*f_date)),
685 BookHebrew {
686 year: case.year,
687 month: case.month,
688 day: case.day
689 },
690 "{case:?}"
691 );
692 }
693 }
694
695 #[test]
696 fn test_civil_to_book_conversion() {
697 for (f_date, case) in TEST_FIXED_DATE.iter().zip(HEBREW_DATES.iter()) {
698 let book_hebrew = BookHebrew::book_hebrew_from_fixed(RataDie::new(*f_date));
699 let (y, m, d) = book_hebrew.to_civil_date();
700 let book_hebrew = BookHebrew::from_civil_date(y, m, d);
701
702 assert_eq!(
703 (case.year, case.month),
704 (book_hebrew.year, book_hebrew.month)
705 )
706 }
707 }
708}