icu_calendar/cal/
buddhist.rs

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 Buddhist calendar.
6//!
7//! ```rust
8//! use icu::calendar::{cal::Buddhist, Date};
9//!
10//! let date_iso = Date::try_new_iso(1970, 1, 2)
11//!     .expect("Failed to initialize ISO Date instance.");
12//! let date_buddhist = Date::new_from_iso(date_iso, Buddhist);
13//!
14//! assert_eq!(date_buddhist.era_year().year, 2513);
15//! assert_eq!(date_buddhist.month().ordinal, 1);
16//! assert_eq!(date_buddhist.day_of_month().0, 2);
17//! ```
18
19use crate::cal::iso::{Iso, IsoDateInner};
20use crate::calendar_arithmetic::ArithmeticDate;
21use crate::error::DateError;
22use crate::{types, Calendar, Date, DateDuration, DateDurationUnit, RangeError};
23use calendrical_calculations::rata_die::RataDie;
24use tinystr::tinystr;
25
26/// The number of years the Buddhist Era is ahead of C.E. by
27///
28/// (1 AD = 544 BE)
29const BUDDHIST_ERA_OFFSET: i32 = 543;
30
31#[derive(Copy, Clone, Debug, Default)]
32/// The [Thai Solar Buddhist Calendar][cal]
33///
34/// The [Thai Solar Buddhist Calendar][cal] is a solar calendar used in Thailand, with twelve months.
35/// The months and days are identical to that of the Gregorian calendar, however the years are counted
36/// differently using the Buddhist Era.
37///
38/// This type can be used with [`Date`] to represent dates in this calendar.
39///
40/// [cal]: https://en.wikipedia.org/wiki/Thai_solar_calendar
41///
42/// # Era codes
43///
44/// This calendar uses a single era code `be`, with 1 Buddhist Era being 543 BCE. Dates before this era use negative years.
45///
46/// # Month codes
47///
48/// This calendar supports 12 solar month codes (`"M01" - "M12"`)
49#[allow(clippy::exhaustive_structs)] // this type is stable
50pub struct Buddhist;
51
52impl crate::cal::scaffold::UnstableSealed for Buddhist {}
53impl Calendar for Buddhist {
54    type DateInner = IsoDateInner;
55    type Year = types::EraYear;
56
57    fn from_codes(
58        &self,
59        era: Option<&str>,
60        year: i32,
61        month_code: types::MonthCode,
62        day: u8,
63    ) -> Result<Self::DateInner, DateError> {
64        match era {
65            Some("be") | None => {}
66            _ => return Err(DateError::UnknownEra),
67        }
68        let year = year - BUDDHIST_ERA_OFFSET;
69
70        ArithmeticDate::new_from_codes(self, year, month_code, day).map(IsoDateInner)
71    }
72
73    fn from_iso(&self, iso: IsoDateInner) -> Self::DateInner {
74        iso
75    }
76
77    fn to_iso(&self, date: &Self::DateInner) -> IsoDateInner {
78        *date
79    }
80
81    fn from_rata_die(&self, rd: RataDie) -> Self::DateInner {
82        Iso.from_rata_die(rd)
83    }
84
85    fn to_rata_die(&self, date: &Self::DateInner) -> RataDie {
86        Iso.to_rata_die(date)
87    }
88
89    fn months_in_year(&self, date: &Self::DateInner) -> u8 {
90        Iso.months_in_year(date)
91    }
92
93    fn days_in_year(&self, date: &Self::DateInner) -> u16 {
94        Iso.days_in_year(date)
95    }
96
97    fn days_in_month(&self, date: &Self::DateInner) -> u8 {
98        Iso.days_in_month(date)
99    }
100
101    fn offset_date(&self, date: &mut Self::DateInner, offset: DateDuration<Self>) {
102        Iso.offset_date(date, offset.cast_unit())
103    }
104
105    #[allow(clippy::field_reassign_with_default)] // it's more clear this way
106    fn until(
107        &self,
108        date1: &Self::DateInner,
109        date2: &Self::DateInner,
110        _calendar2: &Self,
111        largest_unit: DateDurationUnit,
112        smallest_unit: DateDurationUnit,
113    ) -> DateDuration<Self> {
114        Iso.until(date1, date2, &Iso, largest_unit, smallest_unit)
115            .cast_unit()
116    }
117
118    /// The calendar-specific year represented by `date`
119    fn year_info(&self, date: &Self::DateInner) -> Self::Year {
120        types::EraYear {
121            era: tinystr!(16, "be"),
122            era_index: Some(0),
123            year: self.extended_year(date),
124            ambiguity: types::YearAmbiguity::CenturyRequired,
125        }
126    }
127
128    fn extended_year(&self, date: &Self::DateInner) -> i32 {
129        Iso.extended_year(date) + BUDDHIST_ERA_OFFSET
130    }
131
132    fn is_in_leap_year(&self, date: &Self::DateInner) -> bool {
133        Iso.is_in_leap_year(date)
134    }
135
136    /// The calendar-specific month represented by `date`
137    fn month(&self, date: &Self::DateInner) -> types::MonthInfo {
138        Iso.month(date)
139    }
140
141    /// The calendar-specific day-of-month represented by `date`
142    fn day_of_month(&self, date: &Self::DateInner) -> types::DayOfMonth {
143        Iso.day_of_month(date)
144    }
145
146    /// Information of the day of the year
147    fn day_of_year(&self, date: &Self::DateInner) -> types::DayOfYear {
148        Iso.day_of_year(date)
149    }
150
151    fn debug_name(&self) -> &'static str {
152        "Buddhist"
153    }
154
155    fn calendar_algorithm(&self) -> Option<crate::preferences::CalendarAlgorithm> {
156        Some(crate::preferences::CalendarAlgorithm::Buddhist)
157    }
158}
159
160impl Date<Buddhist> {
161    /// Construct a new Buddhist Date.
162    ///
163    /// Years are specified as BE years.
164    ///
165    /// ```rust
166    /// use icu::calendar::Date;
167    ///
168    /// let date_buddhist = Date::try_new_buddhist(1970, 1, 2)
169    ///     .expect("Failed to initialize Buddhist Date instance.");
170    ///
171    /// assert_eq!(date_buddhist.era_year().year, 1970);
172    /// assert_eq!(date_buddhist.month().ordinal, 1);
173    /// assert_eq!(date_buddhist.day_of_month().0, 2);
174    /// ```
175    pub fn try_new_buddhist(year: i32, month: u8, day: u8) -> Result<Date<Buddhist>, RangeError> {
176        Date::try_new_iso(year - BUDDHIST_ERA_OFFSET, month, day)
177            .map(|d| Date::new_from_iso(d, Buddhist))
178    }
179}
180
181#[cfg(test)]
182mod test {
183    use calendrical_calculations::rata_die::RataDie;
184
185    use super::*;
186
187    #[test]
188    fn test_buddhist_roundtrip_near_rd_zero() {
189        for i in -10000..=10000 {
190            let rd = RataDie::new(i);
191            let iso1 = Date::from_rata_die(rd, Iso);
192            let buddhist = iso1.to_calendar(Buddhist);
193            let iso2 = buddhist.to_calendar(Iso);
194            let result = iso2.to_rata_die();
195            assert_eq!(rd, result);
196        }
197    }
198
199    #[test]
200    fn test_buddhist_roundtrip_near_epoch() {
201        // Buddhist epoch start RD: -198326
202        for i in -208326..=-188326 {
203            let rd = RataDie::new(i);
204            let iso1 = Date::from_rata_die(rd, Iso);
205            let buddhist = iso1.to_calendar(Buddhist);
206            let iso2 = buddhist.to_calendar(Iso);
207            let result = iso2.to_rata_die();
208            assert_eq!(rd, result);
209        }
210    }
211
212    #[test]
213    fn test_buddhist_directionality_near_rd_zero() {
214        for i in -100..=100 {
215            for j in -100..=100 {
216                let iso_i = Date::from_rata_die(RataDie::new(i), Iso);
217                let iso_j = Date::from_rata_die(RataDie::new(j), Iso);
218
219                let buddhist_i = Date::new_from_iso(iso_i, Buddhist);
220                let buddhist_j = Date::new_from_iso(iso_j, Buddhist);
221
222                assert_eq!(
223                    i.cmp(&j),
224                    iso_i.cmp(&iso_j),
225                    "ISO directionality inconsistent with directionality for i: {i}, j: {j}"
226                );
227
228                assert_eq!(
229                    i.cmp(&j),
230                    buddhist_i.cmp(&buddhist_j),
231                    "Buddhist directionality inconsistent with directionality for i: {i}, j: {j}"
232                );
233            }
234        }
235    }
236
237    #[test]
238    fn test_buddhist_directionality_near_epoch() {
239        // Buddhist epoch start RD: -198326
240        for i in -198426..=-198226 {
241            for j in -198426..=-198226 {
242                let iso_i = Date::from_rata_die(RataDie::new(i), Iso);
243                let iso_j = Date::from_rata_die(RataDie::new(j), Iso);
244
245                let buddhist_i = Date::new_from_iso(iso_i, Buddhist);
246                let buddhist_j = Date::new_from_iso(iso_j, Buddhist);
247
248                assert_eq!(
249                    i.cmp(&j),
250                    iso_i.cmp(&iso_j),
251                    "ISO directionality inconsistent with directionality for i: {i}, j: {j}"
252                );
253
254                assert_eq!(
255                    i.cmp(&j),
256                    buddhist_i.cmp(&buddhist_j),
257                    "Buddhist directionality inconsistent with directionality for i: {i}, j: {j}"
258                );
259            }
260        }
261    }
262
263    #[derive(Debug)]
264    struct TestCase {
265        iso_year: i32,
266        iso_month: u8,
267        iso_day: u8,
268        buddhist_year: i32,
269        buddhist_month: u8,
270        buddhist_day: u8,
271    }
272
273    fn check_test_case(case: TestCase) {
274        let iso_year = case.iso_year;
275        let iso_month = case.iso_month;
276        let iso_day = case.iso_day;
277        let buddhist_year = case.buddhist_year;
278        let buddhist_month = case.buddhist_month;
279        let buddhist_day = case.buddhist_day;
280
281        let iso1 = Date::try_new_iso(iso_year, iso_month, iso_day).unwrap();
282        let buddhist1 = iso1.to_calendar(Buddhist);
283        assert_eq!(
284            buddhist1.era_year().year,
285            buddhist_year,
286            "Iso -> Buddhist year check failed for case: {case:?}"
287        );
288        assert_eq!(
289            buddhist1.month().ordinal,
290            buddhist_month,
291            "Iso -> Buddhist month check failed for case: {case:?}"
292        );
293        assert_eq!(
294            buddhist1.day_of_month().0,
295            buddhist_day,
296            "Iso -> Buddhist day check failed for case: {case:?}"
297        );
298
299        let buddhist2 =
300            Date::try_new_buddhist(buddhist_year, buddhist_month, buddhist_day).unwrap();
301        let iso2 = buddhist2.to_calendar(Iso);
302        assert_eq!(
303            iso2.era_year().year,
304            iso_year,
305            "Buddhist -> Iso year check failed for case: {case:?}"
306        );
307        assert_eq!(
308            iso2.month().ordinal,
309            iso_month,
310            "Buddhist -> Iso month check failed for case: {case:?}"
311        );
312        assert_eq!(
313            iso2.day_of_month().0,
314            iso_day,
315            "Buddhist -> Iso day check failed for case: {case:?}"
316        );
317    }
318
319    #[test]
320    fn test_buddhist_cases_near_rd_zero() {
321        let cases = [
322            TestCase {
323                iso_year: -100,
324                iso_month: 2,
325                iso_day: 15,
326                buddhist_year: 443,
327                buddhist_month: 2,
328                buddhist_day: 15,
329            },
330            TestCase {
331                iso_year: -3,
332                iso_month: 10,
333                iso_day: 29,
334                buddhist_year: 540,
335                buddhist_month: 10,
336                buddhist_day: 29,
337            },
338            TestCase {
339                iso_year: 0,
340                iso_month: 12,
341                iso_day: 31,
342                buddhist_year: 543,
343                buddhist_month: 12,
344                buddhist_day: 31,
345            },
346            TestCase {
347                iso_year: 1,
348                iso_month: 1,
349                iso_day: 1,
350                buddhist_year: 544,
351                buddhist_month: 1,
352                buddhist_day: 1,
353            },
354            TestCase {
355                iso_year: 4,
356                iso_month: 2,
357                iso_day: 29,
358                buddhist_year: 547,
359                buddhist_month: 2,
360                buddhist_day: 29,
361            },
362        ];
363
364        for case in cases {
365            check_test_case(case);
366        }
367    }
368
369    #[test]
370    fn test_buddhist_cases_near_epoch() {
371        // 1 BE = 543 BCE = -542 ISO
372        let cases = [
373            TestCase {
374                iso_year: -554,
375                iso_month: 12,
376                iso_day: 31,
377                buddhist_year: -11,
378                buddhist_month: 12,
379                buddhist_day: 31,
380            },
381            TestCase {
382                iso_year: -553,
383                iso_month: 1,
384                iso_day: 1,
385                buddhist_year: -10,
386                buddhist_month: 1,
387                buddhist_day: 1,
388            },
389            TestCase {
390                iso_year: -544,
391                iso_month: 8,
392                iso_day: 31,
393                buddhist_year: -1,
394                buddhist_month: 8,
395                buddhist_day: 31,
396            },
397            TestCase {
398                iso_year: -543,
399                iso_month: 5,
400                iso_day: 12,
401                buddhist_year: 0,
402                buddhist_month: 5,
403                buddhist_day: 12,
404            },
405            TestCase {
406                iso_year: -543,
407                iso_month: 12,
408                iso_day: 31,
409                buddhist_year: 0,
410                buddhist_month: 12,
411                buddhist_day: 31,
412            },
413            TestCase {
414                iso_year: -542,
415                iso_month: 1,
416                iso_day: 1,
417                buddhist_year: 1,
418                buddhist_month: 1,
419                buddhist_day: 1,
420            },
421            TestCase {
422                iso_year: -541,
423                iso_month: 7,
424                iso_day: 9,
425                buddhist_year: 2,
426                buddhist_month: 7,
427                buddhist_day: 9,
428            },
429        ];
430
431        for case in cases {
432            check_test_case(case);
433        }
434    }
435}