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