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