1use crate::error::year_check;
20use crate::{
21    cal::iso::IsoDateInner, calendar_arithmetic::ArithmeticDate, error::DateError, types, Calendar,
22    Date, Iso, RangeError,
23};
24use calendrical_calculations::rata_die::RataDie;
25use tinystr::tinystr;
26
27const ROC_ERA_OFFSET: i32 = 1911;
30
31#[derive(Copy, Clone, Debug, Default)]
50#[allow(clippy::exhaustive_structs)] pub struct Roc;
52
53#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
55pub struct RocDateInner(IsoDateInner);
56
57impl crate::cal::scaffold::UnstableSealed for Roc {}
58impl Calendar for Roc {
59    type DateInner = RocDateInner;
60    type Year = types::EraYear;
61
62    fn from_codes(
63        &self,
64        era: Option<&str>,
65        year: i32,
66        month_code: crate::types::MonthCode,
67        day: u8,
68    ) -> Result<Self::DateInner, DateError> {
69        let year = match era {
70            Some("roc") | None => ROC_ERA_OFFSET + year_check(year, 1..)?,
71            Some("broc") => ROC_ERA_OFFSET + 1 - year_check(year, 1..)?,
72            Some(_) => return Err(DateError::UnknownEra),
73        };
74
75        ArithmeticDate::new_from_codes(self, year, month_code, day)
76            .map(IsoDateInner)
77            .map(RocDateInner)
78    }
79
80    fn from_rata_die(&self, rd: RataDie) -> Self::DateInner {
81        RocDateInner(Iso.from_rata_die(rd))
82    }
83
84    fn to_rata_die(&self, date: &Self::DateInner) -> RataDie {
85        Iso.to_rata_die(&date.0)
86    }
87
88    fn from_iso(&self, iso: IsoDateInner) -> Self::DateInner {
89        RocDateInner(iso)
90    }
91
92    fn to_iso(&self, date: &Self::DateInner) -> IsoDateInner {
93        date.0
94    }
95
96    fn months_in_year(&self, date: &Self::DateInner) -> u8 {
97        Iso.months_in_year(&date.0)
98    }
99
100    fn days_in_year(&self, date: &Self::DateInner) -> u16 {
101        Iso.days_in_year(&date.0)
102    }
103
104    fn days_in_month(&self, date: &Self::DateInner) -> u8 {
105        Iso.days_in_month(&date.0)
106    }
107
108    fn offset_date(&self, date: &mut Self::DateInner, offset: crate::DateDuration<Self>) {
109        Iso.offset_date(&mut date.0, offset.cast_unit())
110    }
111
112    fn until(
113        &self,
114        date1: &Self::DateInner,
115        date2: &Self::DateInner,
116        _calendar2: &Self,
117        largest_unit: crate::DateDurationUnit,
118        smallest_unit: crate::DateDurationUnit,
119    ) -> crate::DateDuration<Self> {
120        Iso.until(&date1.0, &date2.0, &Iso, largest_unit, smallest_unit)
121            .cast_unit()
122    }
123
124    fn debug_name(&self) -> &'static str {
125        "ROC"
126    }
127
128    fn year_info(&self, date: &Self::DateInner) -> Self::Year {
129        let extended_year = self.extended_year(date);
130        if extended_year > 0 {
131            types::EraYear {
132                era: tinystr!(16, "roc"),
133                era_index: Some(1),
134                year: extended_year,
135                ambiguity: types::YearAmbiguity::CenturyRequired,
136            }
137        } else {
138            types::EraYear {
139                era: tinystr!(16, "broc"),
140                era_index: Some(0),
141                year: 1 - extended_year,
142                ambiguity: types::YearAmbiguity::EraAndCenturyRequired,
143            }
144        }
145    }
146
147    fn extended_year(&self, date: &Self::DateInner) -> i32 {
148        Iso.extended_year(&date.0) - ROC_ERA_OFFSET
149    }
150
151    fn is_in_leap_year(&self, date: &Self::DateInner) -> bool {
152        Iso.is_in_leap_year(&date.0)
153    }
154
155    fn month(&self, date: &Self::DateInner) -> crate::types::MonthInfo {
156        Iso.month(&date.0)
157    }
158
159    fn day_of_month(&self, date: &Self::DateInner) -> crate::types::DayOfMonth {
160        Iso.day_of_month(&date.0)
161    }
162
163    fn day_of_year(&self, date: &Self::DateInner) -> types::DayOfYear {
164        Iso.day_of_year(&date.0)
165    }
166
167    fn calendar_algorithm(&self) -> Option<crate::preferences::CalendarAlgorithm> {
168        Some(crate::preferences::CalendarAlgorithm::Roc)
169    }
170}
171
172impl Date<Roc> {
173    pub fn try_new_roc(year: i32, month: u8, day: u8) -> Result<Date<Roc>, RangeError> {
200        let iso_year = year.saturating_add(ROC_ERA_OFFSET);
201        Date::try_new_iso(iso_year, month, day).map(|d| Date::new_from_iso(d, Roc))
202    }
203}
204
205#[cfg(test)]
206mod test {
207
208    use super::*;
209    use calendrical_calculations::rata_die::RataDie;
210
211    #[derive(Debug)]
212    struct TestCase {
213        rd: RataDie,
214        iso_year: i32,
215        iso_month: u8,
216        iso_day: u8,
217        expected_year: i32,
218        expected_era: &'static str,
219        expected_month: u8,
220        expected_day: u8,
221    }
222
223    fn check_test_case(case: TestCase) {
224        let iso_from_rd = Date::from_rata_die(case.rd, Iso);
225        let roc_from_rd = Date::from_rata_die(case.rd, Roc);
226        assert_eq!(
227            roc_from_rd.era_year().year,
228            case.expected_year,
229            "Failed year check from RD: {case:?}\nISO: {iso_from_rd:?}\nROC: {roc_from_rd:?}"
230        );
231        assert_eq!(
232            roc_from_rd.era_year().era,
233            case.expected_era,
234            "Failed era check from RD: {case:?}\nISO: {iso_from_rd:?}\nROC: {roc_from_rd:?}"
235        );
236        assert_eq!(
237            roc_from_rd.extended_year(),
238            if case.expected_era == "roc" {
239                case.expected_year
240            } else {
241                1 - case.expected_year
242            },
243            "Failed year check from RD: {case:?}\nISO: {iso_from_rd:?}\nROC: {roc_from_rd:?}"
244        );
245        assert_eq!(
246            roc_from_rd.month().ordinal,
247            case.expected_month,
248            "Failed month check from RD: {case:?}\nISO: {iso_from_rd:?}\nROC: {roc_from_rd:?}"
249        );
250        assert_eq!(roc_from_rd.day_of_month().0, case.expected_day,
251            "Failed day_of_month check from RD: {case:?}\nISO: {iso_from_rd:?}\nROC: {roc_from_rd:?}");
252
253        let iso_from_case = Date::try_new_iso(case.iso_year, case.iso_month, case.iso_day)
254            .expect("Failed to initialize ISO date for {case:?}");
255        let roc_from_case = Date::new_from_iso(iso_from_case, Roc);
256        assert_eq!(iso_from_rd, iso_from_case,
257            "ISO from RD not equal to ISO generated from manually-input ymd\nCase: {case:?}\nRD: {iso_from_rd:?}\nManual: {iso_from_case:?}");
258        assert_eq!(roc_from_rd, roc_from_case,
259            "ROC date from RD not equal to ROC generated from manually-input ymd\nCase: {case:?}\nRD: {roc_from_rd:?}\nManual: {roc_from_case:?}");
260    }
261
262    #[test]
263    fn test_roc_current_era() {
264        let cases = [
270            TestCase {
271                rd: RataDie::new(697978),
272                iso_year: 1912,
273                iso_month: 1,
274                iso_day: 1,
275                expected_year: 1,
276                expected_era: "roc",
277                expected_month: 1,
278                expected_day: 1,
279            },
280            TestCase {
281                rd: RataDie::new(698037),
282                iso_year: 1912,
283                iso_month: 2,
284                iso_day: 29,
285                expected_year: 1,
286                expected_era: "roc",
287                expected_month: 2,
288                expected_day: 29,
289            },
290            TestCase {
291                rd: RataDie::new(698524),
292                iso_year: 1913,
293                iso_month: 6,
294                iso_day: 30,
295                expected_year: 2,
296                expected_era: "roc",
297                expected_month: 6,
298                expected_day: 30,
299            },
300            TestCase {
301                rd: RataDie::new(738714),
302                iso_year: 2023,
303                iso_month: 7,
304                iso_day: 13,
305                expected_year: 112,
306                expected_era: "roc",
307                expected_month: 7,
308                expected_day: 13,
309            },
310        ];
311
312        for case in cases {
313            check_test_case(case);
314        }
315    }
316
317    #[test]
318    fn test_roc_prior_era() {
319        let cases = [
324            TestCase {
325                rd: RataDie::new(697977),
326                iso_year: 1911,
327                iso_month: 12,
328                iso_day: 31,
329                expected_year: 1,
330                expected_era: "broc",
331                expected_month: 12,
332                expected_day: 31,
333            },
334            TestCase {
335                rd: RataDie::new(697613),
336                iso_year: 1911,
337                iso_month: 1,
338                iso_day: 1,
339                expected_year: 1,
340                expected_era: "broc",
341                expected_month: 1,
342                expected_day: 1,
343            },
344            TestCase {
345                rd: RataDie::new(697612),
346                iso_year: 1910,
347                iso_month: 12,
348                iso_day: 31,
349                expected_year: 2,
350                expected_era: "broc",
351                expected_month: 12,
352                expected_day: 31,
353            },
354            TestCase {
355                rd: RataDie::new(696576),
356                iso_year: 1908,
357                iso_month: 2,
358                iso_day: 29,
359                expected_year: 4,
360                expected_era: "broc",
361                expected_month: 2,
362                expected_day: 29,
363            },
364            TestCase {
365                rd: RataDie::new(1),
366                iso_year: 1,
367                iso_month: 1,
368                iso_day: 1,
369                expected_year: 1911,
370                expected_era: "broc",
371                expected_month: 1,
372                expected_day: 1,
373            },
374            TestCase {
375                rd: RataDie::new(0),
376                iso_year: 0,
377                iso_month: 12,
378                iso_day: 31,
379                expected_year: 1912,
380                expected_era: "broc",
381                expected_month: 12,
382                expected_day: 31,
383            },
384        ];
385
386        for case in cases {
387            check_test_case(case);
388        }
389    }
390
391    #[test]
392    fn test_roc_directionality_near_epoch() {
393        let rd_epoch_start = 697978;
397        for i in (rd_epoch_start - 100)..=(rd_epoch_start + 100) {
398            for j in (rd_epoch_start - 100)..=(rd_epoch_start + 100) {
399                let iso_i = Date::from_rata_die(RataDie::new(i), Iso);
400                let iso_j = Date::from_rata_die(RataDie::new(j), Iso);
401
402                let roc_i = Date::from_rata_die(RataDie::new(i), Roc);
403                let roc_j = Date::from_rata_die(RataDie::new(j), Roc);
404
405                assert_eq!(
406                    i.cmp(&j),
407                    iso_i.cmp(&iso_j),
408                    "ISO directionality inconsistent with directionality for i: {i}, j: {j}"
409                );
410                assert_eq!(
411                    i.cmp(&j),
412                    roc_i.cmp(&roc_j),
413                    "ROC directionality inconsistent with directionality for i: {i}, j: {j}"
414                );
415            }
416        }
417    }
418
419    #[test]
420    fn test_roc_directionality_near_rd_zero() {
421        for i in -100..=100 {
423            for j in -100..100 {
424                let iso_i = Date::from_rata_die(RataDie::new(i), Iso);
425                let iso_j = Date::from_rata_die(RataDie::new(j), Iso);
426
427                let roc_i = Date::from_rata_die(RataDie::new(i), Roc);
428                let roc_j = Date::from_rata_die(RataDie::new(j), Roc);
429
430                assert_eq!(
431                    i.cmp(&j),
432                    iso_i.cmp(&iso_j),
433                    "ISO directionality inconsistent with directionality for i: {i}, j: {j}"
434                );
435                assert_eq!(
436                    i.cmp(&j),
437                    roc_i.cmp(&roc_j),
438                    "ROC directionality inconsistent with directionality for i: {i}, j: {j}"
439                );
440            }
441        }
442    }
443}