icu_calendar/cal/
indian.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 Indian national calendar.
6//!
7//! ```rust
8//! use icu::calendar::{cal::Indian, Date};
9//!
10//! let date_iso = Date::try_new_iso(1970, 1, 2)
11//!     .expect("Failed to initialize ISO Date instance.");
12//! let date_indian = Date::new_from_iso(date_iso, Indian);
13//!
14//! assert_eq!(date_indian.era_year().year, 1891);
15//! assert_eq!(date_indian.month().ordinal, 10);
16//! assert_eq!(date_indian.day_of_month().0, 12);
17//! ```
18
19use crate::cal::iso::{Iso, IsoDateInner};
20use crate::calendar_arithmetic::{ArithmeticDate, CalendarArithmetic};
21use crate::error::DateError;
22use crate::{types, Calendar, Date, DateDuration, DateDurationUnit, RangeError};
23use calendrical_calculations::rata_die::RataDie;
24use tinystr::tinystr;
25
26/// The [Indian National (Śaka) Calendar](https://en.wikipedia.org/wiki/Indian_national_calendar)
27///
28/// The Indian National calendar is a solar calendar used by the Indian government, with twelve months.
29///
30/// This type can be used with [`Date`] to represent dates in this calendar.
31///
32/// # Era codes
33///
34/// This calendar uses a single era code: `shaka`, with Śaka 0 being 78 CE. Dates before this era use negative years.
35///
36/// # Month codes
37///
38/// This calendar supports 12 solar month codes (`"M01" - "M12"`)
39#[derive(Copy, Clone, Debug, Hash, Default, Eq, PartialEq, PartialOrd, Ord)]
40#[allow(clippy::exhaustive_structs)] // this type is stable
41pub struct Indian;
42
43/// The inner date type used for representing [`Date`]s of [`Indian`]. See [`Date`] and [`Indian`] for more details.
44#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
45pub struct IndianDateInner(ArithmeticDate<Indian>);
46
47impl CalendarArithmetic for Indian {
48    type YearInfo = i32;
49
50    fn days_in_provided_month(year: i32, month: u8) -> u8 {
51        if month == 1 {
52            if Self::provided_year_is_leap(year) {
53                31
54            } else {
55                30
56            }
57        } else if (2..=6).contains(&month) {
58            31
59        } else if (7..=12).contains(&month) {
60            30
61        } else {
62            0
63        }
64    }
65
66    fn months_in_provided_year(_: i32) -> u8 {
67        12
68    }
69
70    fn provided_year_is_leap(year: i32) -> bool {
71        Iso::provided_year_is_leap(year + 78)
72    }
73
74    fn last_month_day_in_provided_year(_year: i32) -> (u8, u8) {
75        (12, 30)
76    }
77
78    fn days_in_provided_year(year: i32) -> u16 {
79        if Self::provided_year_is_leap(year) {
80            366
81        } else {
82            365
83        }
84    }
85}
86
87/// The Śaka era starts on the 81st day of the Gregorian year (March 22 or 21)
88/// which is an 80 day offset. This number should be subtracted from Gregorian dates
89const DAY_OFFSET: u16 = 80;
90/// The Śaka era is 78 years behind Gregorian. This number should be added to Gregorian dates
91const YEAR_OFFSET: i32 = 78;
92
93impl crate::cal::scaffold::UnstableSealed for Indian {}
94impl Calendar for Indian {
95    type DateInner = IndianDateInner;
96    type Year = types::EraYear;
97    fn from_codes(
98        &self,
99        era: Option<&str>,
100        year: i32,
101        month_code: types::MonthCode,
102        day: u8,
103    ) -> Result<Self::DateInner, DateError> {
104        let year = match era {
105            Some("shaka") | None => year,
106            Some(_) => return Err(DateError::UnknownEra),
107        };
108        ArithmeticDate::new_from_codes(self, year, month_code, day).map(IndianDateInner)
109    }
110
111    fn from_rata_die(&self, rd: RataDie) -> Self::DateInner {
112        self.from_iso(Iso.from_rata_die(rd))
113    }
114
115    fn to_rata_die(&self, date: &Self::DateInner) -> RataDie {
116        Iso.to_rata_die(&self.to_iso(date))
117    }
118
119    // Algorithms directly implemented in icu_calendar since they're not from the book
120    fn from_iso(&self, iso: IsoDateInner) -> IndianDateInner {
121        // Get day number in year (1 indexed)
122        let day_of_year_iso = Iso::day_of_year(iso);
123        // Convert to Śaka year
124        let mut year = iso.0.year - YEAR_OFFSET;
125        // This is in the previous Indian year
126        let day_of_year_indian = if day_of_year_iso <= DAY_OFFSET {
127            year -= 1;
128            let n_days = Self::days_in_provided_year(year);
129
130            // calculate day of year in previous year
131            n_days + day_of_year_iso - DAY_OFFSET
132        } else {
133            day_of_year_iso - DAY_OFFSET
134        };
135        IndianDateInner(ArithmeticDate::date_from_year_day(
136            year,
137            day_of_year_indian as u32,
138        ))
139    }
140
141    // Algorithms directly implemented in icu_calendar since they're not from the book
142    fn to_iso(&self, date: &Self::DateInner) -> IsoDateInner {
143        let day_of_year_indian = date.0.day_of_year().0; // 1-indexed
144        let days_in_year = date.0.days_in_year();
145
146        let mut year = date.0.year + YEAR_OFFSET;
147        // days_in_year is a valid day of the year, so we check > not >=
148        let day_of_year_iso = if day_of_year_indian + DAY_OFFSET > days_in_year {
149            year += 1;
150            // calculate day of year in next year
151            day_of_year_indian + DAY_OFFSET - days_in_year
152        } else {
153            day_of_year_indian + DAY_OFFSET
154        };
155        Iso::iso_from_year_day(year, day_of_year_iso)
156    }
157
158    fn months_in_year(&self, date: &Self::DateInner) -> u8 {
159        date.0.months_in_year()
160    }
161
162    fn days_in_year(&self, date: &Self::DateInner) -> u16 {
163        date.0.days_in_year()
164    }
165
166    fn days_in_month(&self, date: &Self::DateInner) -> u8 {
167        date.0.days_in_month()
168    }
169
170    fn offset_date(&self, date: &mut Self::DateInner, offset: DateDuration<Self>) {
171        date.0.offset_date(offset, &());
172    }
173
174    #[allow(clippy::field_reassign_with_default)]
175    fn until(
176        &self,
177        date1: &Self::DateInner,
178        date2: &Self::DateInner,
179        _calendar2: &Self,
180        _largest_unit: DateDurationUnit,
181        _smallest_unit: DateDurationUnit,
182    ) -> DateDuration<Self> {
183        date1.0.until(date2.0, _largest_unit, _smallest_unit)
184    }
185
186    fn year_info(&self, date: &Self::DateInner) -> Self::Year {
187        types::EraYear {
188            era_index: Some(0),
189            era: tinystr!(16, "shaka"),
190            year: self.extended_year(date),
191            ambiguity: types::YearAmbiguity::CenturyRequired,
192        }
193    }
194
195    fn extended_year(&self, date: &Self::DateInner) -> i32 {
196        date.0.extended_year()
197    }
198
199    fn is_in_leap_year(&self, date: &Self::DateInner) -> bool {
200        Self::provided_year_is_leap(date.0.year)
201    }
202
203    fn month(&self, date: &Self::DateInner) -> types::MonthInfo {
204        date.0.month()
205    }
206
207    fn day_of_month(&self, date: &Self::DateInner) -> types::DayOfMonth {
208        date.0.day_of_month()
209    }
210
211    fn day_of_year(&self, date: &Self::DateInner) -> types::DayOfYear {
212        date.0.day_of_year()
213    }
214
215    fn debug_name(&self) -> &'static str {
216        "Indian"
217    }
218
219    fn calendar_algorithm(&self) -> Option<crate::preferences::CalendarAlgorithm> {
220        Some(crate::preferences::CalendarAlgorithm::Indian)
221    }
222}
223
224impl Indian {
225    /// Construct a new Indian Calendar
226    pub fn new() -> Self {
227        Self
228    }
229}
230
231impl Date<Indian> {
232    /// Construct new Indian Date, with year provided in the Śaka era.
233    ///
234    /// ```rust
235    /// use icu::calendar::Date;
236    ///
237    /// let date_indian = Date::try_new_indian(1891, 10, 12)
238    ///     .expect("Failed to initialize Indian Date instance.");
239    ///
240    /// assert_eq!(date_indian.era_year().year, 1891);
241    /// assert_eq!(date_indian.month().ordinal, 10);
242    /// assert_eq!(date_indian.day_of_month().0, 12);
243    /// ```
244    pub fn try_new_indian(year: i32, month: u8, day: u8) -> Result<Date<Indian>, RangeError> {
245        ArithmeticDate::new_from_ordinals(year, month, day)
246            .map(IndianDateInner)
247            .map(|inner| Date::from_raw(inner, Indian))
248    }
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254    use calendrical_calculations::rata_die::RataDie;
255    fn assert_roundtrip(y: i32, m: u8, d: u8, iso_y: i32, iso_m: u8, iso_d: u8) {
256        let indian =
257            Date::try_new_indian(y, m, d).expect("Indian date should construct successfully");
258        let iso = indian.to_iso();
259
260        assert_eq!(
261            iso.era_year().year,
262            iso_y,
263            "{y}-{m}-{d}: ISO year did not match"
264        );
265        assert_eq!(
266            iso.month().ordinal,
267            iso_m,
268            "{y}-{m}-{d}: ISO month did not match"
269        );
270        assert_eq!(
271            iso.day_of_month().0,
272            iso_d,
273            "{y}-{m}-{d}: ISO day did not match"
274        );
275
276        let roundtrip = iso.to_calendar(Indian);
277
278        assert_eq!(
279            roundtrip.era_year().year,
280            indian.era_year().year,
281            "{y}-{m}-{d}: roundtrip year did not match"
282        );
283        assert_eq!(
284            roundtrip.month().ordinal,
285            indian.month().ordinal,
286            "{y}-{m}-{d}: roundtrip month did not match"
287        );
288        assert_eq!(
289            roundtrip.day_of_month(),
290            indian.day_of_month(),
291            "{y}-{m}-{d}: roundtrip day did not match"
292        );
293    }
294
295    #[test]
296    fn roundtrip_indian() {
297        // Ultimately the day of the year will always be identical regardless of it
298        // being a leap year or not
299        // Test dates that occur after and before Chaitra 1 (March 22/21), in all years of
300        // a four-year leap cycle, to ensure that all code paths are tested
301        assert_roundtrip(1944, 6, 7, 2022, 8, 29);
302        assert_roundtrip(1943, 6, 7, 2021, 8, 29);
303        assert_roundtrip(1942, 6, 7, 2020, 8, 29);
304        assert_roundtrip(1941, 6, 7, 2019, 8, 29);
305        assert_roundtrip(1944, 11, 7, 2023, 1, 27);
306        assert_roundtrip(1943, 11, 7, 2022, 1, 27);
307        assert_roundtrip(1942, 11, 7, 2021, 1, 27);
308        assert_roundtrip(1941, 11, 7, 2020, 1, 27);
309    }
310
311    #[derive(Debug)]
312    struct TestCase {
313        iso_year: i32,
314        iso_month: u8,
315        iso_day: u8,
316        expected_year: i32,
317        expected_month: u8,
318        expected_day: u8,
319    }
320
321    fn check_case(case: TestCase) {
322        let iso = Date::try_new_iso(case.iso_year, case.iso_month, case.iso_day).unwrap();
323        let indian = iso.to_calendar(Indian);
324        assert_eq!(
325            indian.era_year().year,
326            case.expected_year,
327            "Year check failed for case: {case:?}"
328        );
329        assert_eq!(
330            indian.month().ordinal,
331            case.expected_month,
332            "Month check failed for case: {case:?}"
333        );
334        assert_eq!(
335            indian.day_of_month().0,
336            case.expected_day,
337            "Day check failed for case: {case:?}"
338        );
339    }
340
341    #[test]
342    fn test_cases_near_epoch_start() {
343        let cases = [
344            TestCase {
345                iso_year: 79,
346                iso_month: 3,
347                iso_day: 23,
348                expected_year: 1,
349                expected_month: 1,
350                expected_day: 2,
351            },
352            TestCase {
353                iso_year: 79,
354                iso_month: 3,
355                iso_day: 22,
356                expected_year: 1,
357                expected_month: 1,
358                expected_day: 1,
359            },
360            TestCase {
361                iso_year: 79,
362                iso_month: 3,
363                iso_day: 21,
364                expected_year: 0,
365                expected_month: 12,
366                expected_day: 30,
367            },
368            TestCase {
369                iso_year: 79,
370                iso_month: 3,
371                iso_day: 20,
372                expected_year: 0,
373                expected_month: 12,
374                expected_day: 29,
375            },
376            TestCase {
377                iso_year: 78,
378                iso_month: 3,
379                iso_day: 21,
380                expected_year: -1,
381                expected_month: 12,
382                expected_day: 30,
383            },
384        ];
385
386        for case in cases {
387            check_case(case);
388        }
389    }
390
391    #[test]
392    fn test_cases_near_rd_zero() {
393        let cases = [
394            TestCase {
395                iso_year: 1,
396                iso_month: 3,
397                iso_day: 22,
398                expected_year: -77,
399                expected_month: 1,
400                expected_day: 1,
401            },
402            TestCase {
403                iso_year: 1,
404                iso_month: 3,
405                iso_day: 21,
406                expected_year: -78,
407                expected_month: 12,
408                expected_day: 30,
409            },
410            TestCase {
411                iso_year: 1,
412                iso_month: 1,
413                iso_day: 1,
414                expected_year: -78,
415                expected_month: 10,
416                expected_day: 11,
417            },
418            TestCase {
419                iso_year: 0,
420                iso_month: 3,
421                iso_day: 21,
422                expected_year: -78,
423                expected_month: 1,
424                expected_day: 1,
425            },
426            TestCase {
427                iso_year: 0,
428                iso_month: 1,
429                iso_day: 1,
430                expected_year: -79,
431                expected_month: 10,
432                expected_day: 11,
433            },
434            TestCase {
435                iso_year: -1,
436                iso_month: 3,
437                iso_day: 21,
438                expected_year: -80,
439                expected_month: 12,
440                expected_day: 30,
441            },
442        ];
443
444        for case in cases {
445            check_case(case);
446        }
447    }
448
449    #[test]
450    fn test_roundtrip_near_rd_zero() {
451        for i in -1000..=1000 {
452            let initial = RataDie::new(i);
453            let result = Date::from_rata_die(initial, Iso)
454                .to_calendar(Indian)
455                .to_iso()
456                .to_rata_die();
457            assert_eq!(
458                initial, result,
459                "Roundtrip failed for initial: {initial:?}, result: {result:?}"
460            );
461        }
462    }
463
464    #[test]
465    fn test_roundtrip_near_epoch_start() {
466        // Epoch start: RD 28570
467        for i in 27570..=29570 {
468            let initial = RataDie::new(i);
469            let result = Date::from_rata_die(initial, Iso)
470                .to_calendar(Indian)
471                .to_iso()
472                .to_rata_die();
473            assert_eq!(
474                initial, result,
475                "Roundtrip failed for initial: {initial:?}, result: {result:?}"
476            );
477        }
478    }
479
480    #[test]
481    fn test_directionality_near_rd_zero() {
482        for i in -100..=100 {
483            for j in -100..=100 {
484                let rd_i = RataDie::new(i);
485                let rd_j = RataDie::new(j);
486
487                let indian_i = Date::from_rata_die(rd_i, Indian);
488                let indian_j = Date::from_rata_die(rd_j, Indian);
489
490                assert_eq!(i.cmp(&j), indian_i.cmp(&indian_j), "Directionality test failed for i: {i}, j: {j}, indian_i: {indian_i:?}, indian_j: {indian_j:?}");
491            }
492        }
493    }
494
495    #[test]
496    fn test_directionality_near_epoch_start() {
497        // Epoch start: RD 28570
498        for i in 28470..=28670 {
499            for j in 28470..=28670 {
500                let indian_i = Date::from_rata_die(RataDie::new(i), Indian);
501                let indian_j = Date::from_rata_die(RataDie::new(j), Indian);
502
503                assert_eq!(i.cmp(&j), indian_i.cmp(&indian_j), "Directionality test failed for i: {i}, j: {j}, indian_i: {indian_i:?}, indian_j: {indian_j:?}");
504            }
505        }
506    }
507}