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 | |
| 4 | use crate::{ |
| 5 | calendar::{CalendarDate, system::CalendarSystem, era::JapaneseEraRegistry, hebrew::HebrewCalendar}, |
| 6 | constant::MonthOfYear, |
| 7 | time::CalClockZone, |
| 8 | }; |
| 9 | |
| 10 | use oxedyne_fe2o3_core::prelude::*; |
| 11 | use oxedyne_fe2o3_jdat::prelude::*; |
| 12 | use oxedyne_fe2o3_namex::{ |
| 13 | id::{InNamex, LocalId, NamexId}, |
| 14 | }; |
| 15 | |
| 16 | use 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)] |
| 40 | pub 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 | |
| 51 | impl Default for Calendar { |
| 52 | fn default() -> Self { |
| 53 | Self::Gregorian |
| 54 | } |
| 55 | } |
| 56 | |
| 57 | impl fmt::Display for Calendar { |
| 58 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| 59 | write!(f, "{}", self.name()) |
| 60 | } |
| 61 | } |
| 62 | |
| 63 | impl 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 | |
| 552 | impl 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 | |
| 603 | impl ToDat for Calendar { |
| 604 | fn to_dat(&self) -> Outcome<Dat> { |
| 605 | Ok(Dat::Str(self.id().to_string())) |
| 606 | } |
| 607 | } |
| 608 | |
| 609 | impl 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 | |
| 636 | impl 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)] |
| 655 | mod 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 | } |