Skip to main content

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