Oregami
Repositories/oxedyne/fe2o3

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

30.9 KiB, 133 runs

created by r1870400018:6289, 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, system::CalendarSystem, era::JapaneseEraRegistry, hebrew::HebrewCalendar},
6 constant::MonthOfYear,
7 time::CalClockZone,
8};
9
10use oxedyne_fe2o3_core::prelude::*;
11use oxedyne_fe2o3_jdat::prelude::*;
12use oxedyne_fe2o3_namex::{
13 id::{InNamex, LocalId, NamexId},
14};
15
16use std::fmt;
17
18/// Every calendar converts to and from every other through the Julian Day
19/// Number, so no pair needs its own conversion.
20///
21/// ```ignore
22/// use oxedyne_fe2o3_datime::calendar::Calendarres!();
23/// use oxedyne_fe2o3_datime::time::CalClockZoneres!();
24///
25/// // Create calendars
26/// let gregorian = Calendar::new()res!(); // Default to Gregorian
27/// let julian = Calendar::Julianres!();
28/// let islamic = Calendar::Islamicres!();
29/// let japanese = Calendar::Japaneseres!();
30///
31/// // Create dates using the calendar
32/// let zone = CalClockZone::utc()res!();
33/// let date = res!(gregorian.date(2024, 1, 15, zone))res!();
34/// let julian_date = res!(julian.date(2024, 1, 15, zone))res!();
35///
36/// // Convert between calendars
37/// let islamic_date = res!(gregorian.convert_date(&date, &islamic))res!();
38/// ```
39#[derive(Clone, Debug, PartialEq, Eq, Hash)]
40pub enum Calendar {
41 Gregorian, // ISO 8601, leap year every 4 except centuries not divisible by 400
42 Julian, // leap year every 4, no exceptions
43 Islamic, // lunar, 12 months, about 354 days, epoch 622 CE
44 Hebrew, // lunisolar, 12 or 13 months, epoch 3761 BCE
45 Japanese, // Gregorian months, years reset with each emperor
46 Thai, // Gregorian + 543 years
47 Minguo, // Gregorian, year 1 = 1912 CE
48 Holocene, // Gregorian + 10,000 years
49}
50
51impl Default for Calendar {
52 fn default() -> Self {
53 Self::Gregorian
54 }
55}
56
57impl fmt::Display for Calendar {
58 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59 write!(f, "{}", self.name())
60 }
61}
62
63impl Calendar {
64 pub fn new() -> Self {
65 Self::default()
66 }
67
68 pub fn name(&self) -> &'static str {
69 match self {
70 Self::Gregorian => "Gregorian",
71 Self::Julian => "Julian",
72 Self::Islamic => "Islamic",
73 Self::Hebrew => "Hebrew",
74 Self::Japanese => "Japanese",
75 Self::Thai => "Thai Buddhist",
76 Self::Minguo => "Minguo",
77 Self::Holocene => "Holocene",
78 }
79 }
80
81 pub fn id(&self) -> &'static str {
82 match self {
83 Self::Gregorian => "gregorian",
84 Self::Julian => "julian",
85 Self::Islamic => "islamic",
86 Self::Hebrew => "hebrew",
87 Self::Japanese => "japanese",
88 Self::Thai => "thai",
89 Self::Minguo => "minguo",
90 Self::Holocene => "holocene",
91 }
92 }
93
94 /// The year, month and day are read in this calendar's own reckoning, not
95 /// the Gregorian one.
96 ///
97 /// ```ignore
98 /// let gregorian = Calendar::Gregorianres!();
99 /// let zone = CalClockZone::utc()res!();
100 /// let date = res!(gregorian.date(2024, 1, 15, zone))res!();
101 /// ```
102 pub fn date(&self, year: i32, month: u8, day: u8, zone: CalClockZone) -> Outcome<CalendarDate> {
103 // Convert to internal representation if needed
104 let (internal_year, internal_month, internal_day) = res!(self.to_internal_date(year, month, day));
105
106 // Convert to underlying calendar system for storage
107 let calendar_system = self.to_calendar_system();
108 CalendarDate::new_with_system(internal_year, internal_month, internal_day, zone, calendar_system)
109 }
110
111 pub fn date_from_ymd(&self, year: i32, month: MonthOfYear, day: u8, zone: CalClockZone) -> Outcome<CalendarDate> {
112 self.date(year, month.of(), day, zone)
113 }
114
115 pub fn date_from_gregorian(&self, gregorian_year: i32, gregorian_month: u8, gregorian_day: u8, zone: CalClockZone) -> Outcome<CalendarDate> {
116 // Convert Gregorian date to this calendar's year/month/day representation
117 let (cal_year, cal_month, cal_day) = self.from_internal_date(gregorian_year, gregorian_month, gregorian_day);
118
119 // Create the date in this calendar system
120 // Note: This will internally convert back to Gregorian for storage, which should give us the original date
121 self.date(cal_year, cal_month, cal_day, zone)
122 }
123
124 pub fn convert_date(&self, date: &CalendarDate, target_calendar: &Calendar) -> Outcome<CalendarDate> {
125 if self == target_calendar {
126 return Ok(date.clone());
127 }
128
129 // All dates are stored internally in Gregorian format
130 // So we can directly convert from Gregorian to the target calendar
131 let gregorian_year = date.year();
132 let gregorian_month = date.month();
133 let gregorian_day = date.day();
134
135 // Create a new date in the target calendar system using the Gregorian date
136 target_calendar.date_from_gregorian(gregorian_year, gregorian_month, gregorian_day, date.zone().clone())
137 }
138
139 pub fn is_leap_year(&self, year: i32) -> bool {
140 match self {
141 Self::Gregorian | Self::Japanese | Self::Thai | Self::Minguo | Self::Holocene => {
142 // These all use Gregorian leap year rules but with different epochs
143 let gregorian_year = self.to_gregorian_year(year);
144 // Gregorian leap year rule: every 4 years except centuries not divisible by 400
145 (gregorian_year % 4 == 0) && ((gregorian_year % 100 != 0) || (gregorian_year % 400 == 0))
146 },
147 Self::Julian => {
148 // Julian leap year: every 4 years
149 year % 4 == 0
150 },
151 Self::Islamic => {
152 // Islamic calendar has leap years in a 30-year cycle
153 // Years 2, 5, 7, 10, 13, 16, 18, 21, 24, 26, 29 are leap years
154 let cycle_year = ((year - 1) % 30) + 1;
155 matches!(cycle_year, 2 | 5 | 7 | 10 | 13 | 16 | 18 | 21 | 24 | 26 | 29)
156 },
157 Self::Hebrew => {
158 // Hebrew leap years occur 7 times in a 19-year cycle
159 HebrewCalendar::is_hebrew_leap_year(year)
160 },
161 }
162 }
163
164 pub fn days_in_month(&self, year: i32, month: u8) -> Outcome<u8> {
165 match self {
166 Self::Islamic => {
167 // Islamic calendar: alternating 30/29 days, with Dhul-Hijjah having 30 in leap years
168 match month {
169 1 | 3 | 5 | 7 | 9 | 11 => Ok(30), // Odd months have 30 days
170 2 | 4 | 6 | 8 | 10 => Ok(29), // Even months have 29 days
171 12 => {
172 // Dhul-Hijjah: 29 days normally, 30 in leap years
173 if self.is_leap_year(year) { Ok(30) } else { Ok(29) }
174 },
175 _ => Err(err!("Invalid month {} for Islamic calendar", month; Invalid, Input)),
176 }
177 },
178 Self::Julian => {
179 // Julian calendar uses same month structure as Gregorian but different leap year rules
180 match month {
181 1 => Ok(31), // January
182 2 => if self.is_leap_year(year) { Ok(29) } else { Ok(28) }, // February
183 3 => Ok(31), // March
184 4 => Ok(30), // April
185 5 => Ok(31), // May
186 6 => Ok(30), // June
187 7 => Ok(31), // July
188 8 => Ok(31), // August
189 9 => Ok(30), // September
190 10 => Ok(31), // October
191 11 => Ok(30), // November
192 12 => Ok(31), // December
193 _ => Err(err!("Invalid month {} for Julian calendar", month; Invalid, Input)),
194 }
195 },
196 Self::Hebrew => {
197 // Hebrew calendar has variable month lengths
198 HebrewCalendar::days_in_hebrew_month(year, month)
199 },
200 _ => {
201 // All other calendars use Gregorian month structure
202 let gregorian_year = self.to_gregorian_year(year);
203 let month_enum = res!(MonthOfYear::from_number(month));
204 Ok(month_enum.days_in_month(gregorian_year))
205 },
206 }
207 }
208
209 pub fn days_in_year(&self, year: i32) -> u16 {
210 match self {
211 Self::Islamic => {
212 if self.is_leap_year(year) { 355 } else { 354 }
213 },
214 Self::Hebrew => {
215 HebrewCalendar::days_in_hebrew_year(year) as u16
216 },
217 _ => {
218 if self.is_leap_year(year) { 366 } else { 365 }
219 },
220 }
221 }
222
223 pub fn validate_date(&self, year: i32, month: u8, day: u8) -> Outcome<()> {
224 if day == 0 {
225 return Err(err!("Day cannot be 0"; Invalid, Input));
226 }
227
228 let max_days = res!(self.days_in_month(year, month));
229 if day > max_days {
230 return Err(err!(
231 "Day {} is invalid for month {} year {} in {} calendar (max {})",
232 day, month, year, self.name(), max_days;
233 Invalid, Input
234 ));
235 }
236
237 // Additional calendar-specific validation
238 match self {
239 Self::Japanese => {
240 // Validate that the year makes sense for Japanese calendar
241 if year < 1 {
242 return Err(err!("Japanese calendar year must be positive"; Invalid, Input));
243 }
244 },
245 Self::Islamic => {
246 // Validate Islamic calendar range
247 if year < 1 {
248 return Err(err!("Islamic calendar year must be positive"; Invalid, Input));
249 }
250 if month > 12 {
251 return Err(err!("Islamic calendar month must be 1-12"; Invalid, Input));
252 }
253 },
254 Self::Thai => {
255 // Thai Buddhist calendar starts from 544 BCE (year 1)
256 if year < 1 {
257 return Err(err!("Thai Buddhist calendar year must be positive"; Invalid, Input));
258 }
259 },
260 Self::Minguo => {
261 // Republic of China calendar starts from 1912 CE (year 1)
262 if year < 1 {
263 return Err(err!("Minguo calendar year must be positive"; Invalid, Input));
264 }
265 },
266 _ => {
267 // Gregorian, Julian, Holocene can handle negative years (BCE)
268 },
269 }
270
271 Ok(())
272 }
273
274 fn to_internal_date(&self, year: i32, month: u8, day: u8) -> Outcome<(i32, u8, u8)> {
275 res!(self.validate_date(year, month, day));
276
277 match self {
278 Self::Gregorian => Ok((year, month, day)),
279 Self::Julian => Ok((year, month, day)), // Julian dates stored as-is
280 Self::Islamic => {
281 // Convert Islamic date to Gregorian using accurate algorithms
282 crate::calendar::islamic::IslamicCalendar::islamic_to_gregorian(year, month, day)
283 },
284 Self::Hebrew => {
285 // Convert Hebrew date to Gregorian
286 HebrewCalendar::to_gregorian(year, month, day)
287 },
288 Self::Japanese => {
289 // Use proper Japanese era system
290 let era_registry = JapaneseEraRegistry::new();
291 let (gregorian_year, gregorian_month, gregorian_day) =
292 res!(era_registry.japanese_to_gregorian("Reiwa", year, month, day)
293 .or_else(|_| {
294 // Try other eras if Reiwa doesn't work
295 for era in era_registry.all_eras() {
296 if let Ok(result) = era_registry.japanese_to_gregorian(era.name_romaji, year, month, day) {
297 return Ok(result);
298 }
299 }
300 Err(err!("Invalid Japanese calendar date: {} year {}, month {}, day {}",
301 "unknown era", year, month, day; Invalid, Input))
302 }));
303 Ok((gregorian_year, gregorian_month, gregorian_day))
304 },
305 Self::Thai => {
306 // Thai Buddhist = Gregorian - 543
307 let gregorian_year = year - 543;
308 Ok((gregorian_year, month, day))
309 },
310 Self::Minguo => {
311 // Minguo = Gregorian - 1911
312 let gregorian_year = year + 1911;
313 Ok((gregorian_year, month, day))
314 },
315 Self::Holocene => {
316 // Holocene = Gregorian - 10000
317 let gregorian_year = year - 10000;
318 Ok((gregorian_year, month, day))
319 },
320 }
321 }
322
323 fn from_internal_date(&self, gregorian_year: i32, month: u8, day: u8) -> (i32, u8, u8) {
324 match self {
325 Self::Gregorian => (gregorian_year, month, day),
326 Self::Julian => (gregorian_year, month, day), // Julian dates stored as-is
327 Self::Islamic => {
328 // Convert Gregorian to Islamic using accurate algorithms
329 match crate::calendar::islamic::IslamicCalendar::gregorian_to_islamic(gregorian_year, month, day) {
330 Ok((islamic_year, islamic_month, islamic_day)) => (islamic_year, islamic_month, islamic_day),
331 Err(_) => {
332 // Fallback for invalid dates
333 (gregorian_year, month, day)
334 }
335 }
336 },
337 Self::Hebrew => {
338 // Convert Gregorian to Hebrew
339 match HebrewCalendar::from_gregorian(gregorian_year, month, day) {
340 Ok((hebrew_year, hebrew_month, hebrew_day)) => (hebrew_year, hebrew_month, hebrew_day),
341 Err(_) => {
342 // Fallback for invalid dates
343 (gregorian_year, month, day)
344 }
345 }
346 },
347 Self::Japanese => {
348 // Use proper Japanese era system
349 let era_registry = JapaneseEraRegistry::new();
350 match era_registry.gregorian_to_japanese(gregorian_year, month, day) {
351 Ok((_era_name, era_year, month, day)) => (era_year, month, day),
352 Err(_) => {
353 // Fallback for dates outside known eras
354 let current_era = era_registry.current_era();
355 let era_year = gregorian_year - current_era.start_year + 1;
356 (era_year, month, day)
357 }
358 }
359 },
360 Self::Thai => {
361 // Thai Buddhist = Gregorian + 543
362 let thai_year = gregorian_year + 543;
363 (thai_year, month, day)
364 },
365 Self::Minguo => {
366 // Minguo = Gregorian - 1911
367 let minguo_year = gregorian_year - 1911;
368 (minguo_year, month, day)
369 },
370 Self::Holocene => {
371 // Holocene = Gregorian + 10000
372 let holocene_year = gregorian_year + 10000;
373 (holocene_year, month, day)
374 },
375 }
376 }
377
378 pub fn to_gregorian_year(&self, year: i32) -> i32 {
379 match self {
380 Self::Gregorian | Self::Julian => year,
381 Self::Islamic => {
382 // Rough approximation: Islamic year 1 = 622 CE
383 // More precise conversion would account for lunar vs solar years
384 622 + ((year - 1) * 354) / 365
385 },
386 Self::Hebrew => {
387 // Hebrew calendar epoch is 3761 BCE
388 // Rough approximation for year conversion
389 year - 3760
390 },
391 Self::Japanese => {
392 // Use proper Japanese era system for current era approximation
393 let era_registry = JapaneseEraRegistry::new();
394 let current_era = era_registry.current_era();
395 match current_era.era_year_to_gregorian(year) {
396 Ok(gregorian_year) => gregorian_year,
397 Err(_) => {
398 // If current era doesn't work, try other eras
399 for era in era_registry.all_eras() {
400 if let Ok(gregorian_year) = era.era_year_to_gregorian(year) {
401 return gregorian_year;
402 }
403 }
404 // Fallback to current era start + year
405 current_era.start_year + year - 1
406 }
407 }
408 },
409 Self::Thai => year - 543,
410 Self::Minguo => year + 1911,
411 Self::Holocene => year - 10000,
412 }
413 }
414
415 #[allow(dead_code)]
416 fn from_gregorian_year(&self, gregorian_year: i32) -> (i32, u8, u8) {
417 let calendar_year = match self {
418 Self::Gregorian | Self::Julian => gregorian_year,
419 Self::Islamic => {
420 // Rough approximation
421 ((gregorian_year - 622) * 365) / 354 + 1
422 },
423 Self::Hebrew => {
424 // Hebrew calendar conversion: add 3760 to get Hebrew year
425 gregorian_year + 3760
426 },
427 Self::Japanese => {
428 // Use proper Japanese era system
429 let era_registry = JapaneseEraRegistry::new();
430 match era_registry.gregorian_to_japanese(gregorian_year, 1, 1) {
431 Ok((_era_name, era_year, _, _)) => era_year,
432 Err(_) => {
433 // Fallback for dates outside known eras
434 let current_era = era_registry.current_era();
435 gregorian_year - current_era.start_year + 1
436 }
437 }
438 },
439 Self::Thai => gregorian_year + 543,
440 Self::Minguo => gregorian_year - 1911,
441 Self::Holocene => gregorian_year + 10000,
442 };
443 (calendar_year, 1, 1) // Return as if January 1st
444 }
445
446
447 fn to_calendar_system(&self) -> CalendarSystem {
448 match self {
449 Self::Gregorian | Self::Islamic | Self::Hebrew | Self::Japanese | Self::Thai | Self::Minguo | Self::Holocene => {
450 CalendarSystem::Gregorian
451 },
452 Self::Julian => CalendarSystem::Julian,
453 }
454 }
455
456 // ========================================================================
457 // Calendar-specific conversion algorithms
458 // ========================================================================
459
460
461 #[allow(dead_code)]
462 fn islamic_to_jdn(&self, year: i32, month: u8, day: u8) -> Outcome<i64> {
463 // Islamic calendar algorithm
464 // Based on the standard Islamic calendar calculation
465 let y = year as i64;
466 let m = month as i64;
467 let d = day as i64;
468
469 let epoch = 1948440; // Islamic epoch in Julian days (July 16, 622 CE)
470
471 // Calculate total days from Islamic epoch
472 let total_days = (y - 1) * 354 + ((y - 1) * 11) / 30 +
473 (m - 1) * 29 + (m / 2) + d - 1;
474
475 Ok(epoch + total_days)
476 }
477
478 #[allow(dead_code)]
479 fn jdn_to_gregorian(&self, jdn: i64) -> Outcome<(i32, u8, u8)> {
480 let a = jdn + 32044;
481 let b = (4 * a + 3) / 146097;
482 let c = a - (146097 * b) / 4;
483 let d = (4 * c + 3) / 1461;
484 let e = c - (1461 * d) / 4;
485 let m = (5 * e + 2) / 153;
486
487 let day = (e - (153 * m + 2) / 5 + 1) as u8;
488 let month = (m + 3 - 12 * (m / 10)) as u8;
489 let year = (100 * b + d - 4800 + m / 10) as i32;
490
491 Ok((year, month, day))
492 }
493
494
495 pub fn from_str(s: &str) -> Outcome<Self> {
496 match s.to_lowercase().as_str() {
497 "gregorian" | "greg" | "g" => Ok(Self::Gregorian),
498 "julian" | "jul" | "j" => Ok(Self::Julian),
499 "islamic" | "hijri" | "muslim" | "i" => Ok(Self::Islamic),
500 "hebrew" | "jewish" | "judaism" => Ok(Self::Hebrew),
501 "japanese" | "jp" | "imperial" => Ok(Self::Japanese),
502 "thai" | "buddhist" | "th" => Ok(Self::Thai),
503 "minguo" | "roc" | "taiwan" | "m" => Ok(Self::Minguo),
504 "holocene" | "human" | "h" => Ok(Self::Holocene),
505 _ => Err(err!("Unknown calendar system: {}", s; Invalid, Input)),
506 }
507 }
508
509 pub fn all() -> impl Iterator<Item = Calendar> {
510 [
511 Self::Gregorian,
512 Self::Julian,
513 Self::Islamic,
514 Self::Hebrew,
515 Self::Japanese,
516 Self::Thai,
517 Self::Minguo,
518 Self::Holocene,
519 ].into_iter()
520 }
521
522 pub fn epoch_year(&self) -> i32 {
523 match self {
524 Self::Gregorian | Self::Julian => 1,
525 Self::Islamic => 622,
526 Self::Hebrew => -3760, // Hebrew calendar epoch (3761 BCE)
527 Self::Japanese => 2019, // Current era: Reiwa
528 Self::Thai => -543, // 544 BCE
529 Self::Minguo => 1912, // Republic of China establishment
530 Self::Holocene => -9999, // 10,000 BCE
531 }
532 }
533
534 pub fn description(&self) -> &'static str {
535 match self {
536 Self::Gregorian => "Modern international standard calendar, reformed in 1582",
537 Self::Julian => "Ancient Roman calendar, used before Gregorian reform",
538 Self::Islamic => "Lunar calendar starting from the Hijra (622 CE)",
539 Self::Hebrew => "Lunisolar calendar used for Jewish religious observances",
540 Self::Japanese => "Based on imperial eras, currently Reiwa era",
541 Self::Thai => "Buddhist calendar, Gregorian structure + 543 years",
542 Self::Minguo => "Republic of China calendar, starting from 1912 CE",
543 Self::Holocene => "Scientific calendar starting from Holocene epoch",
544 }
545 }
546}
547
548// ============================================================================
549// Namex Integration for Universal Identifier Support
550// ============================================================================
551
552impl InNamex for Calendar {
553 /// Permanent and globally unique across fe2o3 systems.
554 fn name_id(&self) -> Outcome<NamexId> {
555 let id_str = match self {
556 Self::Gregorian => "cal_gregorian_0123456789abcdef0123456789abcd=",
557 Self::Julian => "cal_julian_abcdef0123456789abcdef0123456789=",
558 Self::Islamic => "cal_islamic_fedcba9876543210fedcba9876543210=",
559 Self::Hebrew => "cal_hebrew_789abc123def456789abc123def456789=",
560 Self::Japanese => "cal_japanese_123abc456def789123abc456def789a=",
561 Self::Thai => "cal_thai_987654321fedcba987654321fedcba98=",
562 Self::Minguo => "cal_minguo_abcdef123456789abcdef123456789a=",
563 Self::Holocene => "cal_holocene_456789abcdef123456789abcdef123=",
564 };
565 NamexId::try_from(id_str)
566 }
567
568 /// An 8-bit id, for compact binary forms.
569 fn local_id(&self) -> LocalId {
570 match self {
571 Self::Gregorian => LocalId(1),
572 Self::Julian => LocalId(2),
573 Self::Islamic => LocalId(3),
574 Self::Hebrew => LocalId(4),
575 Self::Japanese => LocalId(5),
576 Self::Thai => LocalId(6),
577 Self::Minguo => LocalId(7),
578 Self::Holocene => LocalId(8),
579 }
580 }
581
582 fn assoc_names_base64(gname: &'static str) -> Outcome<Option<Vec<(&'static str, &'static str)>>> {
583 let ids = match gname {
584 "calendars" => vec![
585 ("Gregorian", "cal_gregorian_0123456789abcdef0123456789abcd="),
586 ("Julian", "cal_julian_abcdef0123456789abcdef0123456789="),
587 ("Islamic", "cal_islamic_fedcba9876543210fedcba9876543210="),
588 ("Japanese", "cal_japanese_123abc456def789123abc456def789a="),
589 ("Thai", "cal_thai_987654321fedcba987654321fedcba98="),
590 ("Minguo", "cal_minguo_abcdef123456789abcdef123456789a="),
591 ("Holocene", "cal_holocene_456789abcdef123456789abcdef123="),
592 ],
593 _ => return Err(err!("Group name '{}' not recognised for Calendar", gname; Invalid, Input)),
594 };
595 Ok(Some(ids))
596 }
597}
598
599// ============================================================================
600// JDAT Integration for Type-Safe Serialization
601// ============================================================================
602
603impl ToDat for Calendar {
604 fn to_dat(&self) -> Outcome<Dat> {
605 Ok(Dat::Str(self.id().to_string()))
606 }
607}
608
609impl FromDat for Calendar {
610 /// Accepts either the string or the structured form.
611 fn from_dat(dat: Dat) -> Outcome<Self> {
612 match dat {
613 // String format - use existing parser
614 Dat::Str(s) => Self::from_str(&s),
615
616 // Binary format with LocalId
617 Dat::U8(local_id) => {
618 match LocalId(local_id) {
619 LocalId(1) => Ok(Self::Gregorian),
620 LocalId(2) => Ok(Self::Julian),
621 LocalId(3) => Ok(Self::Islamic),
622 LocalId(4) => Ok(Self::Hebrew),
623 LocalId(5) => Ok(Self::Japanese),
624 LocalId(6) => Ok(Self::Thai),
625 LocalId(7) => Ok(Self::Minguo),
626 LocalId(8) => Ok(Self::Holocene),
627 _ => Err(err!("Invalid calendar LocalId: {}", local_id; Invalid, Input)),
628 }
629 },
630
631 _ => Err(err!("Expected string or u8 for Calendar"; Invalid, Input)),
632 }
633 }
634}
635
636impl Calendar {
637 /// One byte per calendar, being the local id.
638 pub fn to_dat_binary(&self) -> Outcome<Dat> {
639 Ok(Dat::U8(self.local_id().0))
640 }
641
642 pub fn to_dat_structured(&self) -> Outcome<Dat> {
643 Ok(mapdat! {
644 "id" => self.id(),
645 "name" => self.name(),
646 "description" => self.description(),
647 "namex_id" => res!(self.name_id()).to_string(),
648 "local_id" => self.local_id().0,
649 "epoch_year" => self.epoch_year(),
650 })
651 }
652}
653
654#[cfg(test)]
655mod tests {
656 use super::*;
657 use crate::time::CalClockZone;
658
659 #[test]
660 fn test_calendar_creation() {
661 let gregorian = Calendar::new();
662 assert_eq!(gregorian, Calendar::Gregorian);
663
664 let islamic = Calendar::Islamic;
665 assert_eq!(islamic.name(), "Islamic");
666 assert_eq!(islamic.id(), "islamic");
667 }
668
669 #[test]
670 fn test_calendar_date_creation() {
671 let zone = CalClockZone::utc();
672
673 // Test Gregorian
674 let gregorian = Calendar::Gregorian;
675 let date = gregorian.date(2024, 1, 15, zone.clone()).unwrap();
676 assert_eq!(date.year(), 2024);
677 assert_eq!(date.month(), 1);
678 assert_eq!(date.day(), 15);
679
680 // Test Holocene
681 let holocene = Calendar::Holocene;
682 let holocene_date = holocene.date(12024, 1, 15, zone.clone()).unwrap();
683 // Internal representation should be Gregorian 2024
684 assert_eq!(holocene_date.year(), 2024);
685 }
686
687 #[test]
688 fn test_calendar_leap_years() {
689 // Test Gregorian leap years
690 let gregorian = Calendar::Gregorian;
691 assert!(gregorian.is_leap_year(2024));
692 assert!(!gregorian.is_leap_year(1900));
693 assert!(gregorian.is_leap_year(2000));
694
695 // Test Julian leap years
696 let julian = Calendar::Julian;
697 assert!(julian.is_leap_year(1900)); // Leap in Julian, not Gregorian
698 assert!(julian.is_leap_year(2024));
699
700 // Test Islamic leap years
701 let islamic = Calendar::Islamic;
702 assert!(islamic.is_leap_year(2)); // 2nd year of 30-year cycle
703 assert!(!islamic.is_leap_year(1));
704 }
705
706 #[test]
707 fn test_thai_calendar() {
708 let thai = Calendar::Thai;
709 let zone = CalClockZone::utc();
710
711 // Thai year 2567 = Gregorian year 2024
712 let thai_date = thai.date(2567, 1, 15, zone).unwrap();
713 assert_eq!(thai_date.year(), 2024); // Internal Gregorian representation
714 }
715
716 #[test]
717 fn test_minguo_calendar() {
718 let minguo = Calendar::Minguo;
719 let zone = CalClockZone::utc();
720
721 // Minguo year 113 = Gregorian year 2024
722 let minguo_date = minguo.date(113, 1, 15, zone).unwrap();
723 assert_eq!(minguo_date.year(), 2024); // Internal Gregorian representation
724 }
725
726 #[test]
727 fn test_holocene_calendar() {
728 let holocene = Calendar::Holocene;
729 let zone = CalClockZone::utc();
730
731 // Holocene year 12024 = Gregorian year 2024
732 let holocene_date = holocene.date(12024, 1, 15, zone).unwrap();
733 assert_eq!(holocene_date.year(), 2024); // Internal Gregorian representation
734 }
735
736 #[test]
737 fn test_islamic_calendar() -> Outcome<()> {
738 let islamic = Calendar::Islamic;
739 let zone = CalClockZone::utc();
740
741 // Test basic Islamic date creation
742 let _islamic_date = ok!(islamic.date(1445, 1, 15, zone));
743 // The internal representation will be converted to Gregorian equivalent
744
745 // Test Islamic month lengths
746 assert_eq!(islamic.days_in_month(1445, 1).unwrap(), 30); // Muharram
747 assert_eq!(islamic.days_in_month(1445, 2).unwrap(), 29); // Safar
748
749 // Year 1445 is actually a leap year (cycle year 5), so Dhul-Hijjah has 30 days
750 assert_eq!(islamic.days_in_month(1445, 12).unwrap(), 30); // Leap year Dhul-Hijjah
751
752 // Test a non-leap year (1444 is cycle year 4, not leap)
753 assert_eq!(islamic.days_in_month(1444, 12).unwrap(), 29); // Non-leap year Dhul-Hijjah
754 Ok(())
755 }
756
757 #[test]
758 fn test_calendar_parsing() {
759 assert_eq!(Calendar::from_str("gregorian").unwrap(), Calendar::Gregorian);
760 assert_eq!(Calendar::from_str("ISLAMIC").unwrap(), Calendar::Islamic);
761 assert_eq!(Calendar::from_str("thai").unwrap(), Calendar::Thai);
762 assert_eq!(Calendar::from_str("holocene").unwrap(), Calendar::Holocene);
763
764 assert!(Calendar::from_str("invalid").is_err());
765 }
766
767 #[test]
768 fn test_calendar_validation() {
769 let islamic = Calendar::Islamic;
770
771 // Valid Islamic dates
772 assert!(islamic.validate_date(1445, 1, 30).is_ok());
773 assert!(islamic.validate_date(1445, 2, 29).is_ok());
774
775 // Invalid Islamic dates
776 assert!(islamic.validate_date(1445, 1, 31).is_err()); // Month 1 has only 30 days
777 assert!(islamic.validate_date(1445, 13, 1).is_err()); // Month 13 doesn't exist
778 assert!(islamic.validate_date(0, 1, 1).is_err()); // Year 0 invalid
779 }
780
781 #[test]
782 fn test_calendar_info() {
783 let calendars = Calendar::all().collect::<Vec<_>>();
784 assert_eq!(calendars.len(), 7);
785
786 for calendar in calendars {
787 assert!(!calendar.name().is_empty());
788 assert!(!calendar.id().is_empty());
789 assert!(!calendar.description().is_empty());
790 }
791 }
792}