oxedyne/fe2o3/fe2o3_datime/src/calendar/era.rs
10.4 KiB, 43 runs
created by r1870400018:8384, 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 | //! Japanese Imperial Era (Nengō) system for accurate Japanese calendar calculations. |
| 2 | //! |
| 3 | //! This module provides comprehensive support for Japanese imperial eras, |
| 4 | //! including historical eras and proper era transition logic. |
| 5 | //! |
| 6 | //! The Japanese calendar uses the same month/day structure as the Gregorian calendar |
| 7 | //! but years are counted from the beginning of each emperor's reign. |
| 8 | //! |
| 9 | //! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\ |
| 10 | //! Anthropic Claude |
| 11 | |
| 12 | use oxedyne_fe2o3_core::prelude::*; |
| 13 | |
| 14 | #[derive(Clone, Debug, PartialEq, Eq)] |
| 15 | pub struct JapaneseEra { |
| 16 | pub name_japanese: &'static str, |
| 17 | pub name_romaji: &'static str, |
| 18 | pub start_year: i32, // Gregorian |
| 19 | pub start_month: u8, // 1-12 |
| 20 | pub start_day: u8, |
| 21 | pub end_year: Option<i32>, // None for the current era |
| 22 | pub end_month: Option<u8>, |
| 23 | pub end_day: Option<u8>, |
| 24 | } |
| 25 | |
| 26 | impl JapaneseEra { |
| 27 | pub fn contains_date(&self, year: i32, month: u8, day: u8) -> bool { |
| 28 | // Check if date is after era start |
| 29 | if year < self.start_year { |
| 30 | return false; |
| 31 | } |
| 32 | if year == self.start_year { |
| 33 | if month < self.start_month { |
| 34 | return false; |
| 35 | } |
| 36 | if month == self.start_month && day < self.start_day { |
| 37 | return false; |
| 38 | } |
| 39 | } |
| 40 | |
| 41 | // Check if date is before era end (if era has ended) |
| 42 | if let (Some(end_year), Some(end_month), Some(end_day)) = |
| 43 | (self.end_year, self.end_month, self.end_day) { |
| 44 | if year > end_year { |
| 45 | return false; |
| 46 | } |
| 47 | if year == end_year { |
| 48 | if month > end_month { |
| 49 | return false; |
| 50 | } |
| 51 | if month == end_month && day > end_day { |
| 52 | return false; |
| 53 | } |
| 54 | } |
| 55 | } |
| 56 | |
| 57 | true |
| 58 | } |
| 59 | |
| 60 | pub fn gregorian_to_era_year(&self, gregorian_year: i32) -> Outcome<i32> { |
| 61 | // Check if the year falls within this era |
| 62 | if gregorian_year < self.start_year { |
| 63 | return Err(err!("Year {} is before era {} starts in {}", |
| 64 | gregorian_year, self.name_romaji, self.start_year; |
| 65 | Invalid, Input)); |
| 66 | } |
| 67 | |
| 68 | if let Some(end_year) = self.end_year { |
| 69 | if gregorian_year > end_year { |
| 70 | return Err(err!("Year {} is after era {} ends in {}", |
| 71 | gregorian_year, self.name_romaji, end_year; |
| 72 | Invalid, Input)); |
| 73 | } |
| 74 | } |
| 75 | |
| 76 | Ok(gregorian_year - self.start_year + 1) |
| 77 | } |
| 78 | |
| 79 | pub fn era_year_to_gregorian(&self, era_year: i32) -> Outcome<i32> { |
| 80 | if era_year < 1 { |
| 81 | return Err(err!("Era year must be positive, got {}", era_year; Invalid, Input)); |
| 82 | } |
| 83 | |
| 84 | let gregorian_year = self.start_year + era_year - 1; |
| 85 | |
| 86 | // Validate that this year is within the era bounds |
| 87 | if let Some(end_year) = self.end_year { |
| 88 | if gregorian_year > end_year { |
| 89 | return Err(err!("Era year {} exceeds end of era {} ({})", |
| 90 | era_year, self.name_romaji, self.name_japanese; |
| 91 | Invalid, Input)); |
| 92 | } |
| 93 | } |
| 94 | |
| 95 | Ok(gregorian_year) |
| 96 | } |
| 97 | } |
| 98 | |
| 99 | pub struct JapaneseEraRegistry { |
| 100 | eras: Vec<JapaneseEra>, |
| 101 | } |
| 102 | |
| 103 | impl JapaneseEraRegistry { |
| 104 | pub fn new() -> Self { |
| 105 | Self { |
| 106 | eras: vec![ |
| 107 | // Modern eras (post-Meiji Restoration) |
| 108 | JapaneseEra { |
| 109 | name_japanese: "令和", |
| 110 | name_romaji: "Reiwa", |
| 111 | start_year: 2019, |
| 112 | start_month: 5, |
| 113 | start_day: 1, |
| 114 | end_year: None, |
| 115 | end_month: None, |
| 116 | end_day: None, |
| 117 | }, |
| 118 | JapaneseEra { |
| 119 | name_japanese: "平成", |
| 120 | name_romaji: "Heisei", |
| 121 | start_year: 1989, |
| 122 | start_month: 1, |
| 123 | start_day: 8, |
| 124 | end_year: Some(2019), |
| 125 | end_month: Some(4), |
| 126 | end_day: Some(30), |
| 127 | }, |
| 128 | JapaneseEra { |
| 129 | name_japanese: "昭和", |
| 130 | name_romaji: "Showa", |
| 131 | start_year: 1926, |
| 132 | start_month: 12, |
| 133 | start_day: 25, |
| 134 | end_year: Some(1989), |
| 135 | end_month: Some(1), |
| 136 | end_day: Some(7), |
| 137 | }, |
| 138 | JapaneseEra { |
| 139 | name_japanese: "大正", |
| 140 | name_romaji: "Taisho", |
| 141 | start_year: 1912, |
| 142 | start_month: 7, |
| 143 | start_day: 30, |
| 144 | end_year: Some(1926), |
| 145 | end_month: Some(12), |
| 146 | end_day: Some(24), |
| 147 | }, |
| 148 | JapaneseEra { |
| 149 | name_japanese: "明治", |
| 150 | name_romaji: "Meiji", |
| 151 | start_year: 1868, |
| 152 | start_month: 1, |
| 153 | start_day: 25, |
| 154 | end_year: Some(1912), |
| 155 | end_month: Some(7), |
| 156 | end_day: Some(29), |
| 157 | }, |
| 158 | ], |
| 159 | } |
| 160 | } |
| 161 | |
| 162 | pub fn find_era_for_date(&self, year: i32, month: u8, day: u8) -> Outcome<&JapaneseEra> { |
| 163 | for era in &self.eras { |
| 164 | if era.contains_date(year, month, day) { |
| 165 | return Ok(era); |
| 166 | } |
| 167 | } |
| 168 | |
| 169 | Err(err!("No Japanese era found for date {}-{:02}-{:02}", year, month, day; Invalid, Input)) |
| 170 | } |
| 171 | |
| 172 | pub fn find_era_by_name(&self, name: &str) -> Option<&JapaneseEra> { |
| 173 | let name_lower = name.to_lowercase(); |
| 174 | self.eras.iter().find(|era| era.name_romaji.to_lowercase() == name_lower) |
| 175 | } |
| 176 | |
| 177 | pub fn current_era(&self) -> &JapaneseEra { |
| 178 | // Current era is always the first in our list |
| 179 | &self.eras[0] |
| 180 | } |
| 181 | |
| 182 | /// Newest first. |
| 183 | pub fn all_eras(&self) -> &[JapaneseEra] { |
| 184 | &self.eras |
| 185 | } |
| 186 | |
| 187 | pub fn gregorian_to_japanese(&self, gregorian_year: i32, month: u8, day: u8) -> Outcome<(String, i32, u8, u8)> { |
| 188 | let era = res!(self.find_era_for_date(gregorian_year, month, day)); |
| 189 | let era_year = res!(era.gregorian_to_era_year(gregorian_year)); |
| 190 | |
| 191 | Ok((era.name_romaji.to_string(), era_year, month, day)) |
| 192 | } |
| 193 | |
| 194 | pub fn japanese_to_gregorian(&self, era_name: &str, era_year: i32, month: u8, day: u8) -> Outcome<(i32, u8, u8)> { |
| 195 | let era = ok!(self.find_era_by_name(era_name) |
| 196 | .ok_or_else(|| err!("Unknown Japanese era: {}", era_name; Invalid, Input))); |
| 197 | |
| 198 | let gregorian_year = res!(era.era_year_to_gregorian(era_year)); |
| 199 | |
| 200 | // Validate the specific date within the era |
| 201 | if !era.contains_date(gregorian_year, month, day) { |
| 202 | return Err(err!("Date {}-{:02}-{:02} is not valid in era {}", |
| 203 | era_year, month, day, era_name; Invalid, Input)); |
| 204 | } |
| 205 | |
| 206 | Ok((gregorian_year, month, day)) |
| 207 | } |
| 208 | } |
| 209 | |
| 210 | impl Default for JapaneseEraRegistry { |
| 211 | fn default() -> Self { |
| 212 | Self::new() |
| 213 | } |
| 214 | } |
| 215 | |
| 216 | #[cfg(test)] |
| 217 | mod tests { |
| 218 | use super::*; |
| 219 | |
| 220 | #[test] |
| 221 | fn test_era_date_containment() { |
| 222 | let registry = JapaneseEraRegistry::new(); |
| 223 | let reiwa = registry.current_era(); |
| 224 | |
| 225 | // Reiwa started May 1, 2019 |
| 226 | assert!(reiwa.contains_date(2019, 5, 1)); |
| 227 | assert!(reiwa.contains_date(2024, 1, 1)); |
| 228 | assert!(!reiwa.contains_date(2019, 4, 30)); |
| 229 | assert!(!reiwa.contains_date(2018, 12, 31)); |
| 230 | } |
| 231 | |
| 232 | #[test] |
| 233 | fn test_era_year_conversion() { |
| 234 | let registry = JapaneseEraRegistry::new(); |
| 235 | let reiwa = registry.current_era(); |
| 236 | |
| 237 | // Reiwa 1 = 2019, Reiwa 6 = 2024 |
| 238 | assert_eq!(reiwa.gregorian_to_era_year(2019).unwrap(), 1); |
| 239 | assert_eq!(reiwa.gregorian_to_era_year(2024).unwrap(), 6); |
| 240 | |
| 241 | assert_eq!(reiwa.era_year_to_gregorian(1).unwrap(), 2019); |
| 242 | assert_eq!(reiwa.era_year_to_gregorian(6).unwrap(), 2024); |
| 243 | } |
| 244 | |
| 245 | #[test] |
| 246 | fn test_era_transitions() { |
| 247 | let registry = JapaneseEraRegistry::new(); |
| 248 | |
| 249 | // Test Heisei to Reiwa transition (April 30, 2019 -> May 1, 2019) |
| 250 | let heisei_last = registry.find_era_for_date(2019, 4, 30).unwrap(); |
| 251 | let reiwa_first = registry.find_era_for_date(2019, 5, 1).unwrap(); |
| 252 | |
| 253 | assert_eq!(heisei_last.name_romaji, "Heisei"); |
| 254 | assert_eq!(reiwa_first.name_romaji, "Reiwa"); |
| 255 | |
| 256 | // Heisei 31 ended on April 30, 2019 |
| 257 | assert_eq!(heisei_last.gregorian_to_era_year(2019).unwrap(), 31); |
| 258 | // Reiwa 1 started on May 1, 2019 |
| 259 | assert_eq!(reiwa_first.gregorian_to_era_year(2019).unwrap(), 1); |
| 260 | } |
| 261 | |
| 262 | #[test] |
| 263 | fn test_japanese_calendar_conversion() { |
| 264 | let registry = JapaneseEraRegistry::new(); |
| 265 | |
| 266 | // Test current date in Reiwa era |
| 267 | let (era_name, era_year, month, day) = registry.gregorian_to_japanese(2024, 6, 15).unwrap(); |
| 268 | assert_eq!(era_name, "Reiwa"); |
| 269 | assert_eq!(era_year, 6); |
| 270 | assert_eq!(month, 6); |
| 271 | assert_eq!(day, 15); |
| 272 | |
| 273 | // Convert back |
| 274 | let (greg_year, greg_month, greg_day) = registry.japanese_to_gregorian("Reiwa", 6, 6, 15).unwrap(); |
| 275 | assert_eq!(greg_year, 2024); |
| 276 | assert_eq!(greg_month, 6); |
| 277 | assert_eq!(greg_day, 15); |
| 278 | } |
| 279 | |
| 280 | #[test] |
| 281 | fn test_historical_eras() { |
| 282 | let registry = JapaneseEraRegistry::new(); |
| 283 | |
| 284 | // Test Showa era (1926-1989) |
| 285 | let showa = registry.find_era_by_name("Showa").unwrap(); |
| 286 | assert_eq!(showa.gregorian_to_era_year(1945).unwrap(), 20); // End of WWII |
| 287 | assert_eq!(showa.gregorian_to_era_year(1989).unwrap(), 64); // Last year of Showa |
| 288 | |
| 289 | // Test Meiji era (1868-1912) |
| 290 | let meiji = registry.find_era_by_name("Meiji").unwrap(); |
| 291 | assert_eq!(meiji.gregorian_to_era_year(1868).unwrap(), 1); // First year |
| 292 | assert_eq!(meiji.gregorian_to_era_year(1912).unwrap(), 45); // Last year |
| 293 | } |
| 294 | |
| 295 | #[test] |
| 296 | fn test_era_not_found() { |
| 297 | let registry = JapaneseEraRegistry::new(); |
| 298 | |
| 299 | // Test date before Meiji era |
| 300 | assert!(registry.find_era_for_date(1850, 1, 1).is_err()); |
| 301 | |
| 302 | // Test unknown era name |
| 303 | assert!(registry.find_era_by_name("Unknown").is_none()); |
| 304 | } |
| 305 | } |