Oregami
Repositories/oxedyne/fe2o3

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
12use oxedyne_fe2o3_core::prelude::*;
13
14#[derive(Clone, Debug, PartialEq, Eq)]
15pub 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
26impl 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
99pub struct JapaneseEraRegistry {
100 eras: Vec<JapaneseEra>,
101}
102
103impl 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
210impl Default for JapaneseEraRegistry {
211 fn default() -> Self {
212 Self::new()
213 }
214}
215
216#[cfg(test)]
217mod 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}