Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_datime/src/calendar/hebrew.rs

16.2 KiB, 39 runs

created by r1870400018:8386, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
2//! Anthropic Claude
3
4use crate::{
5 calendar::CalendarDate,
6};
7use oxedyne_fe2o3_core::prelude::*;
8
9/// A lunisolar calendar: 12 months in a common year and 13 in a leap year, of
10/// 29 or 30 days each, so a year may be deficient, regular or abundant.
11#[derive(Debug, Clone, PartialEq)]
12pub struct HebrewCalendar;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub enum HebrewMonth {
16 Tishrei = 1,
17 Cheshvan = 2,
18 Kislev = 3,
19 Tevet = 4,
20 Shevat = 5,
21 Adar = 6, // In leap years, this becomes Adar II
22 AdarI = 7, // Only exists in leap years
23 Nisan = 8,
24 Iyar = 9,
25 Sivan = 10,
26 Tammuz = 11,
27 Av = 12,
28 Elul = 13,
29}
30
31impl HebrewMonth {
32 pub fn number(&self, is_leap_year: bool) -> u8 {
33 match (self, is_leap_year) {
34 (Self::Tishrei, _) => 1,
35 (Self::Cheshvan, _) => 2,
36 (Self::Kislev, _) => 3,
37 (Self::Tevet, _) => 4,
38 (Self::Shevat, _) => 5,
39 (Self::AdarI, true) => 6,
40 (Self::Adar, false) => 6,
41 (Self::Adar, true) => 7, // Adar II in leap years
42 (Self::AdarI, false) => return 0, // Invalid in non-leap years
43 (Self::Nisan, false) => 7,
44 (Self::Nisan, true) => 8,
45 (Self::Iyar, false) => 8,
46 (Self::Iyar, true) => 9,
47 (Self::Sivan, false) => 9,
48 (Self::Sivan, true) => 10,
49 (Self::Tammuz, false) => 10,
50 (Self::Tammuz, true) => 11,
51 (Self::Av, false) => 11,
52 (Self::Av, true) => 12,
53 (Self::Elul, false) => 12,
54 (Self::Elul, true) => 13,
55 }
56 }
57
58 pub fn from_number(month: u8, is_leap_year: bool) -> Outcome<Self> {
59 match (month, is_leap_year) {
60 (1, _) => Ok(Self::Tishrei),
61 (2, _) => Ok(Self::Cheshvan),
62 (3, _) => Ok(Self::Kislev),
63 (4, _) => Ok(Self::Tevet),
64 (5, _) => Ok(Self::Shevat),
65 (6, false) => Ok(Self::Adar),
66 (6, true) => Ok(Self::AdarI),
67 (7, false) => Ok(Self::Nisan),
68 (7, true) => Ok(Self::Adar), // Adar II
69 (8, false) => Ok(Self::Iyar),
70 (8, true) => Ok(Self::Nisan),
71 (9, false) => Ok(Self::Sivan),
72 (9, true) => Ok(Self::Iyar),
73 (10, false) => Ok(Self::Tammuz),
74 (10, true) => Ok(Self::Sivan),
75 (11, false) => Ok(Self::Av),
76 (11, true) => Ok(Self::Tammuz),
77 (12, false) => Ok(Self::Elul),
78 (12, true) => Ok(Self::Av),
79 (13, true) => Ok(Self::Elul),
80 _ => Err(err!("Invalid Hebrew month number: {}", month; Invalid, Input)),
81 }
82 }
83
84 pub fn name(&self) -> &'static str {
85 match self {
86 Self::Tishrei => "Tishrei",
87 Self::Cheshvan => "Cheshvan",
88 Self::Kislev => "Kislev",
89 Self::Tevet => "Tevet",
90 Self::Shevat => "Shevat",
91 Self::Adar => "Adar",
92 Self::AdarI => "Adar I",
93 Self::Nisan => "Nisan",
94 Self::Iyar => "Iyar",
95 Self::Sivan => "Sivan",
96 Self::Tammuz => "Tammuz",
97 Self::Av => "Av",
98 Self::Elul => "Elul",
99 }
100 }
101}
102
103impl HebrewCalendar {
104 pub fn new() -> Self {
105 Self
106 }
107
108 /// Seven leap years fall in each 19-year cycle.
109 pub fn is_hebrew_leap_year(year: i32) -> bool {
110 ((year * 7 + 1) % 19) < 7
111 }
112
113 pub fn months_in_hebrew_year(year: i32) -> u8 {
114 if Self::is_hebrew_leap_year(year) {
115 13
116 } else {
117 12
118 }
119 }
120
121 pub fn days_in_hebrew_month(year: i32, month: u8) -> Outcome<u8> {
122 let is_leap = Self::is_hebrew_leap_year(year);
123
124 match (month, is_leap) {
125 (1, _) => Ok(30), // Tishrei always has 30 days
126 (2, _) => { // Cheshvan can have 29 or 30 days
127 let year_length = Self::days_in_hebrew_year(year);
128 if year_length % 10 == 3 || year_length % 10 == 5 {
129 Ok(29)
130 } else {
131 Ok(30)
132 }
133 },
134 (3, _) => { // Kislev can have 29 or 30 days
135 let year_length = Self::days_in_hebrew_year(year);
136 if year_length % 10 == 3 {
137 Ok(29)
138 } else {
139 Ok(30)
140 }
141 },
142 (4, _) => Ok(29), // Tevet
143 (5, _) => Ok(30), // Shevat
144 (6, false) => Ok(29), // Adar in regular year
145 (6, true) => Ok(30), // Adar I in leap year
146 (7, false) => Ok(30), // Nisan in regular year
147 (7, true) => Ok(29), // Adar II in leap year
148 (8, false) => Ok(29), // Iyar in regular year
149 (8, true) => Ok(30), // Nisan in leap year
150 (9, false) => Ok(30), // Sivan in regular year
151 (9, true) => Ok(29), // Iyar in leap year
152 (10, false) => Ok(29), // Tammuz in regular year
153 (10, true) => Ok(30), // Sivan in leap year
154 (11, false) => Ok(30), // Av in regular year
155 (11, true) => Ok(29), // Tammuz in leap year
156 (12, false) => Ok(29), // Elul in regular year
157 (12, true) => Ok(30), // Av in leap year
158 (13, true) => Ok(29), // Elul in leap year
159 _ => Err(err!("Invalid Hebrew month: {}", month; Invalid, Input)),
160 }
161 }
162
163 pub fn days_in_hebrew_year(year: i32) -> i32 {
164 Self::hebrew_elapsed_days(year + 1) - Self::hebrew_elapsed_days(year)
165 }
166
167 /// Gauss's algorithm, counting from 1 Tishrei of year 1.
168 pub fn hebrew_elapsed_days(year: i32) -> i32 {
169 let months_elapsed = (235 * ((year - 1) / 19)) + // Complete cycles of 19 years
170 (12 * ((year - 1) % 19)) + // Regular months in incomplete cycle
171 ((7 * ((year - 1) % 19) + 1) / 19); // Leap months
172
173 let parts_elapsed = 204 + 793 * (months_elapsed % 1080);
174 let hours_elapsed = 5 + 12 * months_elapsed + 793 * (months_elapsed / 1080) +
175 parts_elapsed / 1080;
176
177 let day = 1 + 29 * months_elapsed + hours_elapsed / 24;
178 let parts = 1080 * (hours_elapsed % 24) + parts_elapsed % 1080;
179
180 let mut alternative_day = day;
181 if parts >= 19440 || // If new moon is at or after noon
182 (day % 7 == 2 && parts >= 9924 && !Self::is_hebrew_leap_year(year)) || // Tuesday restriction
183 (day % 7 == 1 && parts >= 16789 && Self::is_hebrew_leap_year(year - 1)) { // Monday restriction
184 alternative_day += 1;
185 }
186
187 // Adjust for forbidden days (Rosh Hashanah cannot fall on Sunday, Wednesday, or Friday)
188 if alternative_day % 7 == 0 || alternative_day % 7 == 3 || alternative_day % 7 == 5 {
189 alternative_day += 1;
190 }
191
192 alternative_day
193 }
194
195 pub fn from_gregorian(year: i32, month: u8, day: u8) -> Outcome<(i32, u8, u8)> {
196 // Convert Gregorian to Julian Day Number
197 let jdn = Self::gregorian_to_jdn(year, month, day);
198
199 // Convert JDN to Hebrew date
200 Self::jdn_to_hebrew(jdn)
201 }
202
203 pub fn to_gregorian(hebrew_year: i32, hebrew_month: u8, hebrew_day: u8) -> Outcome<(i32, u8, u8)> {
204 // Validate Hebrew date
205 res!(Self::validate_hebrew_date(hebrew_year, hebrew_month, hebrew_day));
206
207 // Convert Hebrew to Julian Day Number
208 let jdn = res!(Self::hebrew_to_jdn(hebrew_year, hebrew_month, hebrew_day));
209
210 // Convert JDN to Gregorian date
211 Ok(Self::jdn_to_gregorian(jdn))
212 }
213
214 pub fn gregorian_to_jdn(year: i32, month: u8, day: u8) -> i64 {
215 let (y, m) = if month <= 2 {
216 (year - 1, month as i32 + 12)
217 } else {
218 (year, month as i32)
219 };
220
221 let a = y / 100;
222 let b = 2 - a + a / 4;
223
224 let jdn = (365.25 * (y + 4716) as f64) as i64 +
225 (30.6001 * (m + 1) as f64) as i64 +
226 day as i64 + b as i64 - 1524;
227
228 jdn
229 }
230
231 fn jdn_to_gregorian(jdn: i64) -> (i32, u8, u8) {
232 let a = jdn + 32044;
233 let b = (4 * a + 3) / 146097;
234 let c = a - (146097 * b) / 4;
235 let d = (4 * c + 3) / 1461;
236 let e = c - (1461 * d) / 4;
237 let m = (5 * e + 2) / 153;
238
239 let day = (e - (153 * m + 2) / 5 + 1) as u8;
240 let month = (m + 3 - 12 * (m / 10)) as u8;
241 let year = (100 * b + d - 4800 + m / 10) as i32;
242
243 (year, month, day)
244 }
245
246 fn gregorian_month_days(year: i32, month: u8) -> u8 {
247 match month {
248 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
249 4 | 6 | 9 | 11 => 30,
250 2 => {
251 if (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) {
252 29
253 } else {
254 28
255 }
256 },
257 _ => 0,
258 }
259 }
260
261 fn hebrew_to_jdn(year: i32, month: u8, day: u8) -> Outcome<i64> {
262 // Calculate days elapsed from 1 Tishrei year 1
263 let mut days = Self::hebrew_elapsed_days(year) + day as i32 - 1;
264
265 // Add days for months in the current year (before the specified month)
266 for m in 1..month {
267 days += res!(Self::days_in_hebrew_month(year, m)) as i32;
268 }
269
270 // The Hebrew epoch (1 Tishrei 1) corresponds to Julian Day Number 347997
271 // This is Sunday, October 6, 3761 BCE in the proleptic Gregorian calendar
272 let hebrew_epoch_jdn = 347997i64;
273
274 Ok(days as i64 + hebrew_epoch_jdn)
275 }
276
277 fn jdn_to_hebrew(jdn: i64) -> Outcome<(i32, u8, u8)> {
278 // Convert JDN to Hebrew days since epoch
279 let hebrew_epoch_jdn = 347997i64;
280 let hebrew_days = (jdn - hebrew_epoch_jdn) as i32;
281
282 // Start from a reasonable year estimate based on the day count
283 // Approximate: 354 days per year on average
284 let mut year = 1 + hebrew_days / 354;
285 if year < 1 {
286 year = 1;
287 }
288
289 // Adjust year to find the correct one
290 while year > 1 && Self::hebrew_elapsed_days(year) > hebrew_days {
291 year -= 1;
292 }
293
294 while Self::hebrew_elapsed_days(year + 1) <= hebrew_days {
295 year += 1;
296 }
297
298 // Find month and day within the year
299 let year_start = Self::hebrew_elapsed_days(year);
300 let day_of_year = hebrew_days - year_start + 1;
301
302 if day_of_year <= 0 {
303 return Err(err!("Invalid day calculation for JDN {}", jdn; Invalid));
304 }
305
306 let mut month = 1;
307 let mut days_counted = 0;
308
309 let months_in_year = Self::months_in_hebrew_year(year);
310 while month <= months_in_year {
311 let days_in_month = res!(Self::days_in_hebrew_month(year, month)) as i32;
312 if days_counted + days_in_month >= day_of_year {
313 let day = day_of_year - days_counted;
314 if day <= 0 || day > days_in_month {
315 return Err(err!("Invalid day {} for month {} in Hebrew year {}", day, month, year; Invalid));
316 }
317 return Ok((year, month, day as u8));
318 }
319 days_counted += days_in_month;
320 month += 1;
321 }
322
323 Err(err!("Failed to convert JDN {} to Hebrew date (year {}, day_of_year {})", jdn, year, day_of_year; Invalid))
324 }
325
326 pub fn validate_hebrew_date(year: i32, month: u8, day: u8) -> Outcome<()> {
327 if year < 1 || year > 9999 {
328 return Err(err!("Hebrew year {} out of range", year; Invalid, Input));
329 }
330
331 let months_in_year = Self::months_in_hebrew_year(year);
332 if month < 1 || month > months_in_year {
333 return Err(err!("Hebrew month {} invalid for year {}", month, year; Invalid, Input));
334 }
335
336 let days_in_month = res!(Self::days_in_hebrew_month(year, month));
337 if day < 1 || day > days_in_month {
338 return Err(err!("Hebrew day {} invalid for month {}/{}", day, month, year; Invalid, Input));
339 }
340
341 Ok(())
342 }
343}
344
345impl HebrewCalendar {
346 pub fn calendar_name(&self) -> &'static str {
347 "Hebrew"
348 }
349
350 pub fn is_leap_year_for_date(&self, date: &CalendarDate) -> bool {
351 // Convert to Hebrew year first
352 match Self::from_gregorian(date.year(), date.month_of_year().of(), date.day()) {
353 Ok((hebrew_year, _, _)) => Self::is_hebrew_leap_year(hebrew_year),
354 Err(_) => false,
355 }
356 }
357
358 pub fn days_in_month_for_date(&self, date: &CalendarDate) -> Outcome<u8> {
359 // Convert to Hebrew date first
360 let (hebrew_year, hebrew_month, _) = res!(Self::from_gregorian(
361 date.year(),
362 date.month_of_year().of(),
363 date.day()
364 ));
365
366 Self::days_in_hebrew_month(hebrew_year, hebrew_month)
367 }
368
369 pub fn months_in_year_for_date(&self, date: &CalendarDate) -> u8 {
370 // Convert to Hebrew year first
371 match Self::from_gregorian(date.year(), date.month_of_year().of(), date.day()) {
372 Ok((hebrew_year, _, _)) => Self::months_in_hebrew_year(hebrew_year),
373 Err(_) => 12, // Default
374 }
375 }
376
377 pub fn days_in_year_for_date(&self, date: &CalendarDate) -> u16 {
378 // Convert to Hebrew year first
379 match Self::from_gregorian(date.year(), date.month_of_year().of(), date.day()) {
380 Ok((hebrew_year, _, _)) => Self::days_in_hebrew_year(hebrew_year) as u16,
381 Err(_) => 365, // Default
382 }
383 }
384
385 pub fn validate_gregorian_date(&self, year: i32, month: u8, day: u8) -> Outcome<()> {
386 // This validates Gregorian dates that will be converted to Hebrew
387 if year < 1 || year > 9999 {
388 return Err(err!("Year {} out of range", year; Invalid, Input));
389 }
390
391 // Basic Gregorian validation
392 if month < 1 || month > 12 {
393 return Err(err!("Month {} out of range", month; Invalid, Input));
394 }
395
396 let days_in_month = Self::gregorian_month_days(year, month);
397 if day < 1 || day > days_in_month {
398 return Err(err!("Day {} out of range for month {}", day, month; Invalid, Input));
399 }
400
401 Ok(())
402 }
403
404 pub fn format_calendar_date(&self, date: &CalendarDate) -> String {
405 // Convert to Hebrew date and format
406 match Self::from_gregorian(date.year(), date.month_of_year().of(), date.day()) {
407 Ok((hebrew_year, hebrew_month, hebrew_day)) => {
408 let month_enum = HebrewMonth::from_number(hebrew_month, Self::is_hebrew_leap_year(hebrew_year))
409 .unwrap_or(HebrewMonth::Tishrei);
410 format!("{} {}, {}", month_enum.name(), hebrew_day, hebrew_year)
411 },
412 Err(_) => format!("Invalid Hebrew date"),
413 }
414 }
415}
416
417#[cfg(test)]
418mod tests {
419 use super::*;
420
421 #[test]
422 fn test_hebrew_leap_years() {
423 // Known Hebrew leap years
424 assert!(HebrewCalendar::is_hebrew_leap_year(5784)); // 2023-2024
425 assert!(HebrewCalendar::is_hebrew_leap_year(5787)); // 2026-2027
426 assert!(HebrewCalendar::is_hebrew_leap_year(5790)); // 2029-2030
427
428 // Known non-leap years
429 assert!(!HebrewCalendar::is_hebrew_leap_year(5783)); // 2022-2023
430 assert!(!HebrewCalendar::is_hebrew_leap_year(5785)); // 2024-2025
431 assert!(!HebrewCalendar::is_hebrew_leap_year(5786)); // 2025-2026
432 }
433
434 #[test]
435 fn test_hebrew_month_days() {
436 // Regular year
437 assert_eq!(HebrewCalendar::days_in_hebrew_month(5783, 1).unwrap(), 30); // Tishrei
438 assert_eq!(HebrewCalendar::days_in_hebrew_month(5783, 6).unwrap(), 29); // Adar
439
440 // Leap year
441 assert_eq!(HebrewCalendar::days_in_hebrew_month(5784, 6).unwrap(), 30); // Adar I
442 assert_eq!(HebrewCalendar::days_in_hebrew_month(5784, 7).unwrap(), 29); // Adar II
443 }
444
445 #[test]
446 fn test_gregorian_to_hebrew_conversion() {
447 // Test known date conversions
448 // September 16, 2023 = 1 Tishrei 5784 (Rosh Hashanah)
449 let (year, month, day) = HebrewCalendar::from_gregorian(2023, 9, 16).unwrap();
450 assert_eq!(year, 5784);
451 assert_eq!(month, 1); // Tishrei
452 assert_eq!(day, 1);
453
454 // December 8, 2023 = 25 Kislev 5784 (Chanukah)
455 let (year, month, day) = HebrewCalendar::from_gregorian(2023, 12, 8).unwrap();
456 assert_eq!(year, 5784);
457 assert_eq!(month, 3); // Kislev
458 assert_eq!(day, 25);
459 }
460
461 #[test]
462 fn test_hebrew_to_gregorian_conversion() {
463 // Test inverse conversions
464 // 1 Tishrei 5784 = September 16, 2023
465 let (year, month, day) = HebrewCalendar::to_gregorian(5784, 1, 1).unwrap();
466 assert_eq!(year, 2023);
467 assert_eq!(month, 9);
468 assert_eq!(day, 16);
469
470 // 15 Nisan 5784 = April 23, 2024 (Passover)
471 let (year, month, day) = HebrewCalendar::to_gregorian(5784, 8, 15).unwrap();
472 assert_eq!(year, 2024);
473 assert_eq!(month, 4);
474 assert_eq!(day, 23);
475 }
476
477 #[test]
478 fn test_round_trip_conversions() {
479 // Test that conversions are reversible
480 let test_dates = vec![
481 (2023, 1, 1),
482 (2023, 9, 16),
483 (2024, 4, 23),
484 (2025, 12, 31),
485 ];
486
487 for (g_year, g_month, g_day) in test_dates {
488 let (h_year, h_month, h_day) = HebrewCalendar::from_gregorian(g_year, g_month, g_day).unwrap();
489 let (g_year2, g_month2, g_day2) = HebrewCalendar::to_gregorian(h_year, h_month, h_day).unwrap();
490
491 assert_eq!(g_year, g_year2);
492 assert_eq!(g_month, g_month2);
493 assert_eq!(g_day, g_day2);
494 }
495 }
496
497 #[test]
498 fn test_hebrew_calendar_validation() {
499 // Valid dates
500 assert!(HebrewCalendar::validate_hebrew_date(5784, 1, 1).is_ok());
501 assert!(HebrewCalendar::validate_hebrew_date(5784, 13, 29).is_ok()); // Leap year has 13 months
502
503 // Invalid dates
504 assert!(HebrewCalendar::validate_hebrew_date(5783, 13, 1).is_err()); // Non-leap year
505 assert!(HebrewCalendar::validate_hebrew_date(5784, 1, 31).is_err()); // Tishrei has 30 days
506 assert!(HebrewCalendar::validate_hebrew_date(0, 1, 1).is_err()); // Invalid year
507 }
508
509 #[test]
510 fn test_hebrew_month_names() {
511 assert_eq!(HebrewMonth::Tishrei.name(), "Tishrei");
512 assert_eq!(HebrewMonth::AdarI.name(), "Adar I");
513 assert_eq!(HebrewMonth::Elul.name(), "Elul");
514 }
515
516 #[test]
517 fn test_calendar_interface() {
518 let calendar = HebrewCalendar::new();
519 let zone = crate::time::CalClockZone::utc();
520
521 // Create a Gregorian date that we'll treat as Hebrew
522 let date = CalendarDate::new(2023, 9, 16, zone).unwrap();
523
524 // Test Calendar trait methods
525 assert_eq!(calendar.calendar_name(), "Hebrew");
526 assert!(calendar.is_leap_year_for_date(&date)); // 5784 is a leap year
527 assert_eq!(calendar.months_in_year_for_date(&date), 13);
528
529 // Test formatting
530 let formatted = calendar.format_calendar_date(&date);
531 assert!(formatted.contains("Tishrei"));
532 assert!(formatted.contains("5784"));
533 }
534}