Coverage Report

Created: 2025-07-11 06:39

/rust/registry/src/index.crates.io-6f17d22bba15001f/icu_calendar-1.5.2/src/indian.rs
Line
Count
Source (jump to first uncovered line)
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
34
use crate::any_calendar::AnyCalendarKind;
35
use crate::calendar_arithmetic::{ArithmeticDate, CalendarArithmetic};
36
use crate::iso::Iso;
37
use crate::{types, Calendar, CalendarError, Date, DateDuration, DateDurationUnit, DateTime, Time};
38
use 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
57
pub 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)]
61
pub struct IndianDateInner(ArithmeticDate<Indian>);
62
63
impl CalendarArithmetic for Indian {
64
    type YearInfo = ();
65
66
0
    fn month_days(year: i32, month: u8, _data: ()) -> u8 {
67
0
        if month == 1 {
68
0
            if Self::is_leap_year(year, ()) {
69
0
                31
70
            } else {
71
0
                30
72
            }
73
0
        } else if (2..=6).contains(&month) {
74
0
            31
75
0
        } else if (7..=12).contains(&month) {
76
0
            30
77
        } else {
78
0
            0
79
        }
80
0
    }
81
82
0
    fn months_for_every_year(_: i32, _data: ()) -> u8 {
83
0
        12
84
0
    }
85
86
0
    fn is_leap_year(year: i32, _data: ()) -> bool {
87
0
        Iso::is_leap_year(year + 78, ())
88
0
    }
89
90
0
    fn last_month_day_in_year(_year: i32, _data: ()) -> (u8, u8) {
91
0
        (12, 30)
92
0
    }
93
94
0
    fn days_in_provided_year(year: i32, _data: ()) -> u16 {
95
0
        if Self::is_leap_year(year, ()) {
96
0
            366
97
        } else {
98
0
            365
99
        }
100
0
    }
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
105
const DAY_OFFSET: u16 = 80;
106
/// The Saka calendar is 78 years behind Gregorian. This number should be added to Gregorian dates
107
const YEAR_OFFSET: i32 = 78;
108
109
impl Calendar for Indian {
110
    type DateInner = IndianDateInner;
111
0
    fn date_from_codes(
112
0
        &self,
113
0
        era: types::Era,
114
0
        year: i32,
115
0
        month_code: types::MonthCode,
116
0
        day: u8,
117
0
    ) -> Result<Self::DateInner, CalendarError> {
118
0
        if era.0 != tinystr!(16, "saka") && era.0 != tinystr!(16, "indian") {
119
0
            return Err(CalendarError::UnknownEra(era.0, self.debug_name()));
120
0
        }
121
0
122
0
        ArithmeticDate::new_from_codes(self, year, month_code, day).map(IndianDateInner)
123
0
    }
124
125
    // Algorithms directly implemented in icu_calendar since they're not from the book
126
0
    fn date_from_iso(&self, iso: Date<Iso>) -> IndianDateInner {
127
0
        // Get day number in year (1 indexed)
128
0
        let day_of_year_iso = Iso::day_of_year(*iso.inner());
129
0
        // Convert to Saka year
130
0
        let mut year = iso.inner().0.year - YEAR_OFFSET;
131
        // This is in the previous Indian year
132
0
        let day_of_year_indian = if day_of_year_iso <= DAY_OFFSET {
133
0
            year -= 1;
134
0
            let n_days = Self::days_in_provided_year(year, ());
135
0
136
0
            // calculate day of year in previous year
137
0
            n_days + day_of_year_iso - DAY_OFFSET
138
        } else {
139
0
            day_of_year_iso - DAY_OFFSET
140
        };
141
0
        IndianDateInner(ArithmeticDate::date_from_year_day(
142
0
            year,
143
0
            day_of_year_indian as u32,
144
0
        ))
145
0
    }
146
147
    // Algorithms directly implemented in icu_calendar since they're not from the book
148
0
    fn date_to_iso(&self, date: &Self::DateInner) -> Date<Iso> {
149
0
        let day_of_year_indian = date.0.day_of_year();
150
0
        let days_in_year = date.0.days_in_year();
151
0
152
0
        let mut year = date.0.year + YEAR_OFFSET;
153
0
        let day_of_year_iso = if day_of_year_indian + DAY_OFFSET >= days_in_year {
154
0
            year += 1;
155
0
            // calculate day of year in next year
156
0
            day_of_year_indian + DAY_OFFSET - days_in_year
157
        } else {
158
0
            day_of_year_indian + DAY_OFFSET
159
        };
160
161
0
        Iso::iso_from_year_day(year, day_of_year_iso)
162
0
    }
163
164
0
    fn months_in_year(&self, date: &Self::DateInner) -> u8 {
165
0
        date.0.months_in_year()
166
0
    }
167
168
0
    fn days_in_year(&self, date: &Self::DateInner) -> u16 {
169
0
        date.0.days_in_year()
170
0
    }
171
172
0
    fn days_in_month(&self, date: &Self::DateInner) -> u8 {
173
0
        date.0.days_in_month()
174
0
    }
175
176
0
    fn day_of_week(&self, date: &Self::DateInner) -> types::IsoWeekday {
177
0
        Iso.day_of_week(Indian.date_to_iso(date).inner())
178
0
    }
179
180
0
    fn offset_date(&self, date: &mut Self::DateInner, offset: DateDuration<Self>) {
181
0
        date.0.offset_date(offset, &());
182
0
    }
183
184
    #[allow(clippy::field_reassign_with_default)]
185
0
    fn until(
186
0
        &self,
187
0
        date1: &Self::DateInner,
188
0
        date2: &Self::DateInner,
189
0
        _calendar2: &Self,
190
0
        _largest_unit: DateDurationUnit,
191
0
        _smallest_unit: DateDurationUnit,
192
0
    ) -> DateDuration<Self> {
193
0
        date1.0.until(date2.0, _largest_unit, _smallest_unit)
194
0
    }
195
196
0
    fn year(&self, date: &Self::DateInner) -> types::FormattableYear {
197
0
        types::FormattableYear {
198
0
            era: types::Era(tinystr!(16, "saka")),
199
0
            number: date.0.year,
200
0
            cyclic: None,
201
0
            related_iso: None,
202
0
        }
203
0
    }
204
205
0
    fn is_in_leap_year(&self, date: &Self::DateInner) -> bool {
206
0
        Self::is_leap_year(date.0.year, ())
207
0
    }
208
209
0
    fn month(&self, date: &Self::DateInner) -> types::FormattableMonth {
210
0
        date.0.month()
211
0
    }
212
213
0
    fn day_of_month(&self, date: &Self::DateInner) -> types::DayOfMonth {
214
0
        date.0.day_of_month()
215
0
    }
216
217
0
    fn day_of_year_info(&self, date: &Self::DateInner) -> types::DayOfYearInfo {
218
0
        let prev_year = types::FormattableYear {
219
0
            era: types::Era(tinystr!(16, "saka")),
220
0
            number: date.0.year - 1,
221
0
            cyclic: None,
222
0
            related_iso: None,
223
0
        };
224
0
        let next_year = types::FormattableYear {
225
0
            era: types::Era(tinystr!(16, "saka")),
226
0
            number: date.0.year + 1,
227
0
            cyclic: None,
228
0
            related_iso: None,
229
0
        };
230
0
        types::DayOfYearInfo {
231
0
            day_of_year: date.0.day_of_year(),
232
0
            days_in_year: date.0.days_in_year(),
233
0
            prev_year,
234
0
            days_in_prev_year: Indian::days_in_year_direct(date.0.year - 1),
235
0
            next_year,
236
0
        }
237
0
    }
238
239
0
    fn debug_name(&self) -> &'static str {
240
0
        "Indian"
241
0
    }
242
243
0
    fn any_calendar_kind(&self) -> Option<AnyCalendarKind> {
244
0
        Some(AnyCalendarKind::Indian)
245
0
    }
246
}
247
248
impl Indian {
249
    /// Construct a new Indian Calendar
250
0
    pub fn new() -> Self {
251
0
        Self
252
0
    }
253
254
0
    fn days_in_year_direct(year: i32) -> u16 {
255
0
        if Indian::is_leap_year(year, ()) {
256
0
            366
257
        } else {
258
0
            365
259
        }
260
0
    }
261
}
262
263
impl 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
0
    pub fn try_new_indian_date(
277
0
        year: i32,
278
0
        month: u8,
279
0
        day: u8,
280
0
    ) -> Result<Date<Indian>, CalendarError> {
281
0
        ArithmeticDate::new_from_ordinals(year, month, day)
282
0
            .map(IndianDateInner)
283
0
            .map(|inner| Date::from_raw(inner, Indian))
284
0
    }
285
}
286
287
impl 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
0
    pub fn try_new_indian_datetime(
305
0
        year: i32,
306
0
        month: u8,
307
0
        day: u8,
308
0
        hour: u8,
309
0
        minute: u8,
310
0
        second: u8,
311
0
    ) -> Result<DateTime<Indian>, CalendarError> {
312
0
        Ok(DateTime {
313
0
            date: Date::try_new_indian_date(year, month, day)?,
314
0
            time: Time::try_new(hour, minute, second, 0)?,
315
        })
316
0
    }
317
}
318
319
#[cfg(test)]
320
mod 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
}