/rust/registry/src/index.crates.io-6f17d22bba15001f/icu_calendar-1.5.2/src/roc.rs
Line | Count | Source (jump to first uncovered line) |
1 | | // This file is part of ICU4X. For terms of use, please see the file |
2 | | // called LICENSE at the top level of the ICU4X source tree |
3 | | // (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). |
4 | | |
5 | | //! This module contains types and implementations for the Republic of China calendar. |
6 | | //! |
7 | | //! ```rust |
8 | | //! use icu::calendar::{roc::Roc, Date, DateTime}; |
9 | | //! |
10 | | //! // `Date` type |
11 | | //! let date_iso = Date::try_new_iso_date(1970, 1, 2) |
12 | | //! .expect("Failed to initialize ISO Date instance."); |
13 | | //! let date_roc = Date::new_from_iso(date_iso, Roc); |
14 | | //! |
15 | | //! // `DateTime` type |
16 | | //! let datetime_iso = DateTime::try_new_iso_datetime(1970, 1, 2, 13, 1, 0) |
17 | | //! .expect("Failed to initialize ISO DateTime instance."); |
18 | | //! let datetime_roc = DateTime::new_from_iso(datetime_iso, Roc); |
19 | | //! |
20 | | //! // `Date` checks |
21 | | //! assert_eq!(date_roc.year().number, 59); |
22 | | //! assert_eq!(date_roc.month().ordinal, 1); |
23 | | //! assert_eq!(date_roc.day_of_month().0, 2); |
24 | | //! |
25 | | //! // `DateTime` checks |
26 | | //! assert_eq!(datetime_roc.date.year().number, 59); |
27 | | //! assert_eq!(datetime_roc.date.month().ordinal, 1); |
28 | | //! assert_eq!(datetime_roc.date.day_of_month().0, 2); |
29 | | //! assert_eq!(datetime_roc.time.hour.number(), 13); |
30 | | //! assert_eq!(datetime_roc.time.minute.number(), 1); |
31 | | //! assert_eq!(datetime_roc.time.second.number(), 0); |
32 | | //! ``` |
33 | | |
34 | | use crate::{ |
35 | | calendar_arithmetic::ArithmeticDate, iso::IsoDateInner, types, AnyCalendarKind, Calendar, |
36 | | CalendarError, Date, DateTime, Iso, Time, |
37 | | }; |
38 | | use calendrical_calculations::helpers::i64_to_saturated_i32; |
39 | | use tinystr::tinystr; |
40 | | |
41 | | /// Year of the beginning of the Taiwanese (ROC/Minguo) calendar. |
42 | | /// 1912 ISO = ROC 1 |
43 | | const ROC_ERA_OFFSET: i32 = 1911; |
44 | | |
45 | | /// The Republic of China (ROC) Calendar |
46 | | /// |
47 | | /// The [Republic of China calendar] is a solar calendar used in Taiwan and Penghu, as well as by overseas diaspora from |
48 | | /// those locations. Months and days are identical to the [`Gregorian`] calendar, while years are counted |
49 | | /// with 1912, the year of the establishment of the Republic of China, as year 1 of the ROC/Minguo/民国/民國 era. |
50 | | /// |
51 | | /// [Republic of China calendar]: https://en.wikipedia.org/wiki/Republic_of_China_calendar |
52 | | /// |
53 | | /// The Republic of China calendar should not be confused with the Chinese traditional lunar calendar |
54 | | /// (see [`Chinese`]). |
55 | | /// |
56 | | /// # Era codes |
57 | | /// |
58 | | /// This calendar supports two era codes: `"roc"`, corresponding to years in the 民國 (minguo) era (CE year 1912 and |
59 | | /// after), and `"roc-inverse"`, corresponding to years before the 民國 (minguo) era (CE year 1911 and before). |
60 | | /// |
61 | | /// |
62 | | /// # Month codes |
63 | | /// |
64 | | /// This calendar supports 12 solar month codes (`"M01" - "M12"`) |
65 | | /// |
66 | | /// [`Chinese`]: crate::chinese::Chinese |
67 | | /// [`Gregorian`]: crate::Gregorian |
68 | | #[derive(Copy, Clone, Debug, Default)] |
69 | | #[allow(clippy::exhaustive_structs)] // this type is stable |
70 | | pub struct Roc; |
71 | | |
72 | | /// The inner date type used for representing [`Date`]s of [`Roc`]. See [`Date`] and [`Roc`] for more info. |
73 | | #[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)] |
74 | | pub struct RocDateInner(IsoDateInner); |
75 | | |
76 | | impl Calendar for Roc { |
77 | | type DateInner = RocDateInner; |
78 | | |
79 | 0 | fn date_from_codes( |
80 | 0 | &self, |
81 | 0 | era: crate::types::Era, |
82 | 0 | year: i32, |
83 | 0 | month_code: crate::types::MonthCode, |
84 | 0 | day: u8, |
85 | 0 | ) -> Result<Self::DateInner, crate::Error> { |
86 | 0 | let year = if era.0 == tinystr!(16, "roc") { |
87 | 0 | if year <= 0 { |
88 | 0 | return Err(CalendarError::OutOfRange); |
89 | 0 | } |
90 | 0 | year + ROC_ERA_OFFSET |
91 | 0 | } else if era.0 == tinystr!(16, "roc-inverse") { |
92 | 0 | if year <= 0 { |
93 | 0 | return Err(CalendarError::OutOfRange); |
94 | 0 | } |
95 | 0 | 1 - year + ROC_ERA_OFFSET |
96 | | } else { |
97 | 0 | return Err(CalendarError::UnknownEra(era.0, self.debug_name())); |
98 | | }; |
99 | | |
100 | 0 | ArithmeticDate::new_from_codes(self, year, month_code, day) |
101 | 0 | .map(IsoDateInner) |
102 | 0 | .map(RocDateInner) |
103 | 0 | } |
104 | | |
105 | 0 | fn date_from_iso(&self, iso: crate::Date<crate::Iso>) -> Self::DateInner { |
106 | 0 | RocDateInner(*iso.inner()) |
107 | 0 | } |
108 | | |
109 | 0 | fn date_to_iso(&self, date: &Self::DateInner) -> crate::Date<crate::Iso> { |
110 | 0 | Date::from_raw(date.0, Iso) |
111 | 0 | } |
112 | | |
113 | 0 | fn months_in_year(&self, date: &Self::DateInner) -> u8 { |
114 | 0 | Iso.months_in_year(&date.0) |
115 | 0 | } |
116 | | |
117 | 0 | fn days_in_year(&self, date: &Self::DateInner) -> u16 { |
118 | 0 | Iso.days_in_year(&date.0) |
119 | 0 | } |
120 | | |
121 | 0 | fn days_in_month(&self, date: &Self::DateInner) -> u8 { |
122 | 0 | Iso.days_in_month(&date.0) |
123 | 0 | } |
124 | | |
125 | 0 | fn offset_date(&self, date: &mut Self::DateInner, offset: crate::DateDuration<Self>) { |
126 | 0 | Iso.offset_date(&mut date.0, offset.cast_unit()) |
127 | 0 | } |
128 | | |
129 | 0 | fn until( |
130 | 0 | &self, |
131 | 0 | date1: &Self::DateInner, |
132 | 0 | date2: &Self::DateInner, |
133 | 0 | _calendar2: &Self, |
134 | 0 | largest_unit: crate::DateDurationUnit, |
135 | 0 | smallest_unit: crate::DateDurationUnit, |
136 | 0 | ) -> crate::DateDuration<Self> { |
137 | 0 | Iso.until(&date1.0, &date2.0, &Iso, largest_unit, smallest_unit) |
138 | 0 | .cast_unit() |
139 | 0 | } |
140 | | |
141 | 0 | fn debug_name(&self) -> &'static str { |
142 | 0 | "ROC" |
143 | 0 | } |
144 | | |
145 | 0 | fn year(&self, date: &Self::DateInner) -> crate::types::FormattableYear { |
146 | 0 | year_as_roc(date.0 .0.year as i64) |
147 | 0 | } |
148 | | |
149 | 0 | fn is_in_leap_year(&self, date: &Self::DateInner) -> bool { |
150 | 0 | Iso.is_in_leap_year(&date.0) |
151 | 0 | } |
152 | | |
153 | 0 | fn month(&self, date: &Self::DateInner) -> crate::types::FormattableMonth { |
154 | 0 | Iso.month(&date.0) |
155 | 0 | } |
156 | | |
157 | 0 | fn day_of_month(&self, date: &Self::DateInner) -> crate::types::DayOfMonth { |
158 | 0 | Iso.day_of_month(&date.0) |
159 | 0 | } |
160 | | |
161 | 0 | fn day_of_year_info(&self, date: &Self::DateInner) -> crate::types::DayOfYearInfo { |
162 | 0 | let prev_year = date.0 .0.year.saturating_sub(1); |
163 | 0 | let next_year = date.0 .0.year.saturating_add(1); |
164 | 0 | types::DayOfYearInfo { |
165 | 0 | day_of_year: Iso::day_of_year(date.0), |
166 | 0 | days_in_year: Iso::days_in_year_direct(date.0 .0.year), |
167 | 0 | prev_year: year_as_roc(prev_year as i64), |
168 | 0 | days_in_prev_year: Iso::days_in_year_direct(prev_year), |
169 | 0 | next_year: year_as_roc(next_year as i64), |
170 | 0 | } |
171 | 0 | } |
172 | | |
173 | | /// The [`AnyCalendarKind`] corresponding to this calendar |
174 | 0 | fn any_calendar_kind(&self) -> Option<AnyCalendarKind> { |
175 | 0 | Some(AnyCalendarKind::Roc) |
176 | 0 | } |
177 | | } |
178 | | |
179 | | impl Date<Roc> { |
180 | | /// Construct a new Republic of China calendar Date. |
181 | | /// |
182 | | /// Years are specified in the "roc" era. This function accepts an extended year in that era, so dates |
183 | | /// before Minguo are negative and year 0 is 1 Before Minguo. To specify dates using explicit era |
184 | | /// codes, use [`Roc::date_from_codes()`]. |
185 | | /// |
186 | | /// ```rust |
187 | | /// use icu::calendar::Date; |
188 | | /// use icu::calendar::gregorian::Gregorian; |
189 | | /// use tinystr::tinystr; |
190 | | /// |
191 | | /// // Create a new ROC Date |
192 | | /// let date_roc = Date::try_new_roc_date(1, 2, 3) |
193 | | /// .expect("Failed to initialize ROC Date instance."); |
194 | | /// |
195 | | /// assert_eq!(date_roc.year().era.0, tinystr!(16, "roc")); |
196 | | /// assert_eq!(date_roc.year().number, 1, "ROC year check failed!"); |
197 | | /// assert_eq!(date_roc.month().ordinal, 2, "ROC month check failed!"); |
198 | | /// assert_eq!(date_roc.day_of_month().0, 3, "ROC day of month check failed!"); |
199 | | /// |
200 | | /// // Convert to an equivalent Gregorian date |
201 | | /// let date_gregorian = date_roc.to_calendar(Gregorian); |
202 | | /// |
203 | | /// assert_eq!(date_gregorian.year().number, 1912, "Gregorian from ROC year check failed!"); |
204 | | /// assert_eq!(date_gregorian.month().ordinal, 2, "Gregorian from ROC month check failed!"); |
205 | | /// assert_eq!(date_gregorian.day_of_month().0, 3, "Gregorian from ROC day of month check failed!"); |
206 | 0 | pub fn try_new_roc_date(year: i32, month: u8, day: u8) -> Result<Date<Roc>, CalendarError> { |
207 | 0 | let iso_year = year.saturating_add(ROC_ERA_OFFSET); |
208 | 0 | Date::try_new_iso_date(iso_year, month, day).map(|d| Date::new_from_iso(d, Roc)) |
209 | 0 | } |
210 | | } |
211 | | |
212 | | impl DateTime<Roc> { |
213 | | /// Construct a new Republic of China calendar datetime from integers. |
214 | | /// |
215 | | /// Years are specified in the "roc" era, Before Minguo dates are negative (year 0 is 1 Before Minguo) |
216 | | /// |
217 | | /// ```rust |
218 | | /// use icu::calendar::DateTime; |
219 | | /// use tinystr::tinystr; |
220 | | /// |
221 | | /// // Create a new ROC DateTime |
222 | | /// let datetime_roc = DateTime::try_new_roc_datetime(1, 2, 3, 13, 1, 0) |
223 | | /// .expect("Failed to initialize ROC DateTime instance."); |
224 | | /// |
225 | | /// assert_eq!(datetime_roc.date.year().era.0, tinystr!(16, "roc")); |
226 | | /// assert_eq!(datetime_roc.date.year().number, 1, "ROC year check failed!"); |
227 | | /// assert_eq!( |
228 | | /// datetime_roc.date.month().ordinal, |
229 | | /// 2, |
230 | | /// "ROC month check failed!" |
231 | | /// ); |
232 | | /// assert_eq!( |
233 | | /// datetime_roc.date.day_of_month().0, |
234 | | /// 3, |
235 | | /// "ROC day of month check failed!" |
236 | | /// ); |
237 | | /// assert_eq!(datetime_roc.time.hour.number(), 13); |
238 | | /// assert_eq!(datetime_roc.time.minute.number(), 1); |
239 | | /// assert_eq!(datetime_roc.time.second.number(), 0); |
240 | | /// ``` |
241 | 0 | pub fn try_new_roc_datetime( |
242 | 0 | year: i32, |
243 | 0 | month: u8, |
244 | 0 | day: u8, |
245 | 0 | hour: u8, |
246 | 0 | minute: u8, |
247 | 0 | second: u8, |
248 | 0 | ) -> Result<DateTime<Roc>, CalendarError> { |
249 | 0 | Ok(DateTime { |
250 | 0 | date: Date::try_new_roc_date(year, month, day)?, |
251 | 0 | time: Time::try_new(hour, minute, second, 0)?, |
252 | | }) |
253 | 0 | } |
254 | | } |
255 | | |
256 | 0 | pub(crate) fn year_as_roc(year: i64) -> types::FormattableYear { |
257 | 0 | let year_i32 = i64_to_saturated_i32(year); |
258 | 0 | let offset_i64 = ROC_ERA_OFFSET as i64; |
259 | 0 | if year > offset_i64 { |
260 | 0 | types::FormattableYear { |
261 | 0 | era: types::Era(tinystr!(16, "roc")), |
262 | 0 | number: year_i32.saturating_sub(ROC_ERA_OFFSET), |
263 | 0 | cyclic: None, |
264 | 0 | related_iso: Some(year_i32), |
265 | 0 | } |
266 | | } else { |
267 | 0 | types::FormattableYear { |
268 | 0 | era: types::Era(tinystr!(16, "roc-inverse")), |
269 | 0 | number: (ROC_ERA_OFFSET + 1).saturating_sub(year_i32), |
270 | 0 | cyclic: None, |
271 | 0 | related_iso: Some(year_i32), |
272 | 0 | } |
273 | | } |
274 | 0 | } |
275 | | |
276 | | #[cfg(test)] |
277 | | mod test { |
278 | | |
279 | | use super::*; |
280 | | use crate::types::Era; |
281 | | use calendrical_calculations::rata_die::RataDie; |
282 | | |
283 | | #[derive(Debug)] |
284 | | struct TestCase { |
285 | | fixed_date: RataDie, |
286 | | iso_year: i32, |
287 | | iso_month: u8, |
288 | | iso_day: u8, |
289 | | expected_year: i32, |
290 | | expected_era: Era, |
291 | | expected_month: u32, |
292 | | expected_day: u32, |
293 | | } |
294 | | |
295 | | fn check_test_case(case: TestCase) { |
296 | | let iso_from_fixed = Iso::iso_from_fixed(case.fixed_date); |
297 | | let roc_from_fixed = Date::new_from_iso(iso_from_fixed, Roc); |
298 | | assert_eq!(roc_from_fixed.year().number, case.expected_year, |
299 | | "Failed year check from fixed: {case:?}\nISO: {iso_from_fixed:?}\nROC: {roc_from_fixed:?}"); |
300 | | assert_eq!(roc_from_fixed.year().era, case.expected_era, |
301 | | "Failed era check from fixed: {case:?}\nISO: {iso_from_fixed:?}\nROC: {roc_from_fixed:?}"); |
302 | | assert_eq!(roc_from_fixed.month().ordinal, case.expected_month, |
303 | | "Failed month check from fixed: {case:?}\nISO: {iso_from_fixed:?}\nROC: {roc_from_fixed:?}"); |
304 | | assert_eq!(roc_from_fixed.day_of_month().0, case.expected_day, |
305 | | "Failed day_of_month check from fixed: {case:?}\nISO: {iso_from_fixed:?}\nROC: {roc_from_fixed:?}"); |
306 | | |
307 | | let iso_from_case = Date::try_new_iso_date(case.iso_year, case.iso_month, case.iso_day) |
308 | | .expect("Failed to initialize ISO date for {case:?}"); |
309 | | let roc_from_case = Date::new_from_iso(iso_from_case, Roc); |
310 | | assert_eq!(iso_from_fixed, iso_from_case, |
311 | | "ISO from fixed not equal to ISO generated from manually-input ymd\nCase: {case:?}\nFixed: {iso_from_fixed:?}\nManual: {iso_from_case:?}"); |
312 | | assert_eq!(roc_from_fixed, roc_from_case, |
313 | | "ROC date from fixed not equal to ROC generated from manually-input ymd\nCase: {case:?}\nFixed: {roc_from_fixed:?}\nManual: {roc_from_case:?}"); |
314 | | } |
315 | | |
316 | | #[test] |
317 | | fn test_roc_current_era() { |
318 | | // Tests that the ROC calendar gives the correct expected day, month, and year for years >= 1912 |
319 | | // (years in the ROC/minguo era) |
320 | | // |
321 | | // Jan 1. 1912 CE = RD 697978 |
322 | | |
323 | | let cases = [ |
324 | | TestCase { |
325 | | fixed_date: RataDie::new(697978), |
326 | | iso_year: 1912, |
327 | | iso_month: 1, |
328 | | iso_day: 1, |
329 | | expected_year: 1, |
330 | | expected_era: Era(tinystr!(16, "roc")), |
331 | | expected_month: 1, |
332 | | expected_day: 1, |
333 | | }, |
334 | | TestCase { |
335 | | fixed_date: RataDie::new(698037), |
336 | | iso_year: 1912, |
337 | | iso_month: 2, |
338 | | iso_day: 29, |
339 | | expected_year: 1, |
340 | | expected_era: Era(tinystr!(16, "roc")), |
341 | | expected_month: 2, |
342 | | expected_day: 29, |
343 | | }, |
344 | | TestCase { |
345 | | fixed_date: RataDie::new(698524), |
346 | | iso_year: 1913, |
347 | | iso_month: 6, |
348 | | iso_day: 30, |
349 | | expected_year: 2, |
350 | | expected_era: Era(tinystr!(16, "roc")), |
351 | | expected_month: 6, |
352 | | expected_day: 30, |
353 | | }, |
354 | | TestCase { |
355 | | fixed_date: RataDie::new(738714), |
356 | | iso_year: 2023, |
357 | | iso_month: 7, |
358 | | iso_day: 13, |
359 | | expected_year: 112, |
360 | | expected_era: Era(tinystr!(16, "roc")), |
361 | | expected_month: 7, |
362 | | expected_day: 13, |
363 | | }, |
364 | | ]; |
365 | | |
366 | | for case in cases { |
367 | | check_test_case(case); |
368 | | } |
369 | | } |
370 | | |
371 | | #[test] |
372 | | fn test_roc_prior_era() { |
373 | | // Tests that the ROC calendar gives the correct expected day, month, and year for years <= 1911 |
374 | | // (years in the ROC/minguo era) |
375 | | // |
376 | | // Jan 1. 1912 CE = RD 697978 |
377 | | let cases = [ |
378 | | TestCase { |
379 | | fixed_date: RataDie::new(697977), |
380 | | iso_year: 1911, |
381 | | iso_month: 12, |
382 | | iso_day: 31, |
383 | | expected_year: 1, |
384 | | expected_era: Era(tinystr!(16, "roc-inverse")), |
385 | | expected_month: 12, |
386 | | expected_day: 31, |
387 | | }, |
388 | | TestCase { |
389 | | fixed_date: RataDie::new(697613), |
390 | | iso_year: 1911, |
391 | | iso_month: 1, |
392 | | iso_day: 1, |
393 | | expected_year: 1, |
394 | | expected_era: Era(tinystr!(16, "roc-inverse")), |
395 | | expected_month: 1, |
396 | | expected_day: 1, |
397 | | }, |
398 | | TestCase { |
399 | | fixed_date: RataDie::new(697612), |
400 | | iso_year: 1910, |
401 | | iso_month: 12, |
402 | | iso_day: 31, |
403 | | expected_year: 2, |
404 | | expected_era: Era(tinystr!(16, "roc-inverse")), |
405 | | expected_month: 12, |
406 | | expected_day: 31, |
407 | | }, |
408 | | TestCase { |
409 | | fixed_date: RataDie::new(696576), |
410 | | iso_year: 1908, |
411 | | iso_month: 2, |
412 | | iso_day: 29, |
413 | | expected_year: 4, |
414 | | expected_era: Era(tinystr!(16, "roc-inverse")), |
415 | | expected_month: 2, |
416 | | expected_day: 29, |
417 | | }, |
418 | | TestCase { |
419 | | fixed_date: RataDie::new(1), |
420 | | iso_year: 1, |
421 | | iso_month: 1, |
422 | | iso_day: 1, |
423 | | expected_year: 1911, |
424 | | expected_era: Era(tinystr!(16, "roc-inverse")), |
425 | | expected_month: 1, |
426 | | expected_day: 1, |
427 | | }, |
428 | | TestCase { |
429 | | fixed_date: RataDie::new(0), |
430 | | iso_year: 0, |
431 | | iso_month: 12, |
432 | | iso_day: 31, |
433 | | expected_year: 1912, |
434 | | expected_era: Era(tinystr!(16, "roc-inverse")), |
435 | | expected_month: 12, |
436 | | expected_day: 31, |
437 | | }, |
438 | | ]; |
439 | | |
440 | | for case in cases { |
441 | | check_test_case(case); |
442 | | } |
443 | | } |
444 | | |
445 | | #[test] |
446 | | fn test_roc_directionality_near_epoch() { |
447 | | // Tests that for a large range of fixed dates near the beginning of the minguo era (CE 1912), |
448 | | // the comparison between those two fixed dates should be equal to the comparison between their |
449 | | // corresponding YMD. |
450 | | let rd_epoch_start = 697978; |
451 | | for i in (rd_epoch_start - 100)..=(rd_epoch_start + 100) { |
452 | | for j in (rd_epoch_start - 100)..=(rd_epoch_start + 100) { |
453 | | let iso_i = Iso::iso_from_fixed(RataDie::new(i)); |
454 | | let iso_j = Iso::iso_from_fixed(RataDie::new(j)); |
455 | | |
456 | | let roc_i = iso_i.to_calendar(Roc); |
457 | | let roc_j = iso_j.to_calendar(Roc); |
458 | | |
459 | | assert_eq!( |
460 | | i.cmp(&j), |
461 | | iso_i.cmp(&iso_j), |
462 | | "ISO directionality inconsistent with directionality for i: {i}, j: {j}" |
463 | | ); |
464 | | assert_eq!( |
465 | | i.cmp(&j), |
466 | | roc_i.cmp(&roc_j), |
467 | | "ROC directionality inconsistent with directionality for i: {i}, j: {j}" |
468 | | ); |
469 | | } |
470 | | } |
471 | | } |
472 | | |
473 | | #[test] |
474 | | fn test_roc_directionality_near_rd_zero() { |
475 | | // Same as `test_directionality_near_epoch`, but with a focus around RD 0 |
476 | | for i in -100..=100 { |
477 | | for j in -100..100 { |
478 | | let iso_i = Iso::iso_from_fixed(RataDie::new(i)); |
479 | | let iso_j = Iso::iso_from_fixed(RataDie::new(j)); |
480 | | |
481 | | let roc_i = iso_i.to_calendar(Roc); |
482 | | let roc_j = iso_j.to_calendar(Roc); |
483 | | |
484 | | assert_eq!( |
485 | | i.cmp(&j), |
486 | | iso_i.cmp(&iso_j), |
487 | | "ISO directionality inconsistent with directionality for i: {i}, j: {j}" |
488 | | ); |
489 | | assert_eq!( |
490 | | i.cmp(&j), |
491 | | roc_i.cmp(&roc_j), |
492 | | "ROC directionality inconsistent with directionality for i: {i}, j: {j}" |
493 | | ); |
494 | | } |
495 | | } |
496 | | } |
497 | | } |