// This file is part of ICU4X. For terms of use, please see the file // called LICENSE at the top level of the ICU4X source tree // (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). use crate::calendar_arithmetic::ArithmeticDate; use crate::calendar_arithmetic::DateFieldsResolver; use crate::error::{DateError, DateFromFieldsError, EcmaReferenceYearError, UnknownEraError}; use crate::options::DateFromFieldsOptions; use crate::options::{DateAddOptions, DateDifferenceOptions}; use crate::types::DateFields; use crate::{types, Calendar, Date, RangeError}; use calendrical_calculations::rata_die::RataDie; use tinystr::tinystr; /// The [Indian National (Śaka) Calendar](https://en.wikipedia.org/wiki/Indian_national_calendar) /// /// The Indian National calendar is a solar calendar created by the Indian government. /// /// This implementation extends proleptically for dates before the calendar's creation /// in 1879 Śaka (1957 CE). /// /// This corresponds to the `"indian"` [CLDR calendar](https://unicode.org/reports/tr35/#UnicodeCalendarIdentifier). /// /// # Era codes /// /// This calendar uses a single era code: `shaka`, with Śaka 0 being 78 CE. Dates before this era use negative years. /// /// # Months and days /// /// The 12 months are called Chaitra (`M01`, 30 days), Vaisakha (`M02`, 31 days), /// Jyaishtha (`M03`, 31 days), Ashadha (`M04`, 31 days), Sravana (`M05`, 31 days), /// Bhadra (`M06`, 31 days), Asvina (`M07`, 30 days), Kartika (`M08`, 30 days), /// Agrahayana or Margasirsha (`M09`, 30 days), Pausha (`M10`, 30 days), Magha (`M11`, 30 days), /// Phalguna (`M12`, 30 days). /// /// In leap years (years where the concurrent [`Gregorian`](crate::cal::Gregorian) year (`year + 78`) is leap), /// Chaitra gains a 31st day. /// /// Standard years thus have 365 days, and leap years 366. /// /// # Calendar drift /// /// The Indian calendar has the same year lengths and leap year rules as the Gregorian calendar, /// so it experiences the same drift of 1 day in ~7700 years with respect to the seasons. #[derive(Copy, Clone, Debug, Hash, Default, Eq, PartialEq, PartialOrd, Ord)] #[allow(clippy::exhaustive_structs)] // this type is stable pub struct Indian; /// The inner date type used for representing [`Date`]s of [`Indian`]. See [`Date`] and [`Indian`] for more details. #[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)] pub struct IndianDateInner(ArithmeticDate); /// The Śaka era starts on the 81st day of the Gregorian year (March 22 or 21) /// which is an 80 day offset. This number should be subtracted from Gregorian dates const DAY_OFFSET: u16 = 80; /// The Śaka era is 78 years behind Gregorian. This number should be added to Gregorian dates const YEAR_OFFSET: i32 = 78; impl DateFieldsResolver for Indian { type YearInfo = i32; fn days_in_provided_month(year: i32, month: u8) -> u8 { if month == 1 { 30 + calendrical_calculations::gregorian::is_leap_year(year + YEAR_OFFSET) as u8 } else if (2..=6).contains(&month) { 31 } else if (7..=12).contains(&month) { 30 } else { 0 } } fn months_in_provided_year(_: i32) -> u8 { 12 } #[inline] fn year_info_from_era( &self, era: &[u8], era_year: i32, ) -> Result { match era { b"shaka" => Ok(era_year), _ => Err(UnknownEraError), } } #[inline] fn year_info_from_extended(&self, extended_year: i32) -> Self::YearInfo { extended_year } #[inline] fn reference_year_from_month_day( &self, month_code: types::ValidMonthCode, day: u8, ) -> Result { let (ordinal_month, false) = month_code.to_tuple() else { return Err(EcmaReferenceYearError::MonthCodeNotInCalendar); }; // December 31, 1972 occurs on 10th month, 10th day, 1894 Shaka // Note: 1894 Shaka is also a leap year let shaka_year = if ordinal_month < 10 || (ordinal_month == 10 && day <= 10) { 1894 } else { 1893 }; Ok(shaka_year) } } impl crate::cal::scaffold::UnstableSealed for Indian {} impl Calendar for Indian { type DateInner = IndianDateInner; type Year = types::EraYear; type DifferenceError = core::convert::Infallible; fn from_codes( &self, era: Option<&str>, year: i32, month_code: types::MonthCode, day: u8, ) -> Result { ArithmeticDate::from_codes(era, year, month_code, day, self).map(IndianDateInner) } #[cfg(feature = "unstable")] fn from_fields( &self, fields: DateFields, options: DateFromFieldsOptions, ) -> Result { ArithmeticDate::from_fields(fields, options, self).map(IndianDateInner) } // Algorithms directly implemented in icu_calendar since they're not from the book fn from_rata_die(&self, rd: RataDie) -> Self::DateInner { let iso_year = calendrical_calculations::gregorian::year_from_fixed(rd) .unwrap_or_else(|e| e.saturate()); // Get day number in year (1 indexed) let day_of_year_iso = (rd - calendrical_calculations::gregorian::day_before_year(iso_year)) as u16; // Convert to Śaka year let mut year = iso_year - YEAR_OFFSET; // This is in the previous Indian year let day_of_year_indian = if day_of_year_iso <= DAY_OFFSET { year -= 1; let n_days = if calendrical_calculations::gregorian::is_leap_year(year + YEAR_OFFSET) { 366 } else { 365 }; // calculate day of year in previous year n_days + day_of_year_iso - DAY_OFFSET } else { day_of_year_iso - DAY_OFFSET }; let mut month = 1; let mut day = day_of_year_indian as i32; while month <= 12 { let month_days = Self::days_in_provided_month(year, month) as i32; if day <= month_days { break; } else { day -= month_days; month += 1; } } debug_assert!(day <= Self::days_in_provided_month(year, month) as i32); let day = day.try_into().unwrap_or(1); IndianDateInner(ArithmeticDate::new_unchecked(year, month, day)) } // Algorithms directly implemented in icu_calendar since they're not from the book fn to_rata_die(&self, date: &Self::DateInner) -> RataDie { let day_of_year_indian = self.day_of_year(date).0; // 1-indexed let days_in_year = self.days_in_year(date); let mut year_iso = date.0.year + YEAR_OFFSET; // days_in_year is a valid day of the year, so we check > not >= let day_of_year_iso = if day_of_year_indian + DAY_OFFSET > days_in_year { year_iso += 1; // calculate day of year in next year day_of_year_indian + DAY_OFFSET - days_in_year } else { day_of_year_indian + DAY_OFFSET }; calendrical_calculations::gregorian::day_before_year(year_iso) + day_of_year_iso as i64 } fn has_cheap_iso_conversion(&self) -> bool { false } fn months_in_year(&self, date: &Self::DateInner) -> u8 { Self::months_in_provided_year(date.0.year) } fn days_in_year(&self, date: &Self::DateInner) -> u16 { if self.is_in_leap_year(date) { 366 } else { 365 } } fn days_in_month(&self, date: &Self::DateInner) -> u8 { Self::days_in_provided_month(date.0.year, date.0.month) } #[cfg(feature = "unstable")] fn add( &self, date: &Self::DateInner, duration: types::DateDuration, options: DateAddOptions, ) -> Result { date.0.added(duration, self, options).map(IndianDateInner) } #[cfg(feature = "unstable")] fn until( &self, date1: &Self::DateInner, date2: &Self::DateInner, options: DateDifferenceOptions, ) -> Result { Ok(date1.0.until(&date2.0, self, options)) } fn year_info(&self, date: &Self::DateInner) -> Self::Year { let extended_year = date.0.year; types::EraYear { era_index: Some(0), era: tinystr!(16, "shaka"), year: extended_year, extended_year, ambiguity: types::YearAmbiguity::CenturyRequired, } } fn is_in_leap_year(&self, date: &Self::DateInner) -> bool { calendrical_calculations::gregorian::is_leap_year(date.0.year + YEAR_OFFSET) } fn month(&self, date: &Self::DateInner) -> types::MonthInfo { types::MonthInfo::non_lunisolar(date.0.month) } fn day_of_month(&self, date: &Self::DateInner) -> types::DayOfMonth { types::DayOfMonth(date.0.day) } fn day_of_year(&self, date: &Self::DateInner) -> types::DayOfYear { types::DayOfYear( (1..date.0.month) .map(|m| Self::days_in_provided_month(date.0.year, m) as u16) .sum::() + date.0.day as u16, ) } fn debug_name(&self) -> &'static str { "Indian" } fn calendar_algorithm(&self) -> Option { Some(crate::preferences::CalendarAlgorithm::Indian) } } impl Indian { /// Construct a new Indian Calendar pub fn new() -> Self { Self } } impl Date { /// Construct new Indian Date, with year provided in the Śaka era. /// /// ```rust /// use icu::calendar::Date; /// /// let date_indian = Date::try_new_indian(1891, 10, 12) /// .expect("Failed to initialize Indian Date instance."); /// /// assert_eq!(date_indian.era_year().year, 1891); /// assert_eq!(date_indian.month().ordinal, 10); /// assert_eq!(date_indian.day_of_month().0, 12); /// ``` pub fn try_new_indian(year: i32, month: u8, day: u8) -> Result, RangeError> { ArithmeticDate::try_from_ymd(year, month, day) .map(IndianDateInner) .map(|inner| Date::from_raw(inner, Indian)) } } #[cfg(test)] mod tests { use super::*; use calendrical_calculations::rata_die::RataDie; fn assert_roundtrip(y: i32, m: u8, d: u8, iso_y: i32, iso_m: u8, iso_d: u8) { let indian = Date::try_new_indian(y, m, d).expect("Indian date should construct successfully"); let iso = indian.to_iso(); assert_eq!( iso.era_year().year, iso_y, "{y}-{m}-{d}: ISO year did not match" ); assert_eq!( iso.month().ordinal, iso_m, "{y}-{m}-{d}: ISO month did not match" ); assert_eq!( iso.day_of_month().0, iso_d, "{y}-{m}-{d}: ISO day did not match" ); let roundtrip = iso.to_calendar(Indian); assert_eq!( roundtrip.era_year().year, indian.era_year().year, "{y}-{m}-{d}: roundtrip year did not match" ); assert_eq!( roundtrip.month().ordinal, indian.month().ordinal, "{y}-{m}-{d}: roundtrip month did not match" ); assert_eq!( roundtrip.day_of_month(), indian.day_of_month(), "{y}-{m}-{d}: roundtrip day did not match" ); } #[test] fn roundtrip_indian() { // Ultimately the day of the year will always be identical regardless of it // being a leap year or not // Test dates that occur after and before Chaitra 1 (March 22/21), in all years of // a four-year leap cycle, to ensure that all code paths are tested assert_roundtrip(1944, 6, 7, 2022, 8, 29); assert_roundtrip(1943, 6, 7, 2021, 8, 29); assert_roundtrip(1942, 6, 7, 2020, 8, 29); assert_roundtrip(1941, 6, 7, 2019, 8, 29); assert_roundtrip(1944, 11, 7, 2023, 1, 27); assert_roundtrip(1943, 11, 7, 2022, 1, 27); assert_roundtrip(1942, 11, 7, 2021, 1, 27); assert_roundtrip(1941, 11, 7, 2020, 1, 27); } #[derive(Debug)] struct TestCase { iso_year: i32, iso_month: u8, iso_day: u8, expected_year: i32, expected_month: u8, expected_day: u8, } fn check_case(case: TestCase) { let iso = Date::try_new_iso(case.iso_year, case.iso_month, case.iso_day).unwrap(); let indian = iso.to_calendar(Indian); assert_eq!( indian.era_year().year, case.expected_year, "Year check failed for case: {case:?}" ); assert_eq!( indian.month().ordinal, case.expected_month, "Month check failed for case: {case:?}" ); assert_eq!( indian.day_of_month().0, case.expected_day, "Day check failed for case: {case:?}" ); } #[test] fn test_cases_near_epoch_start() { let cases = [ TestCase { iso_year: 79, iso_month: 3, iso_day: 23, expected_year: 1, expected_month: 1, expected_day: 2, }, TestCase { iso_year: 79, iso_month: 3, iso_day: 22, expected_year: 1, expected_month: 1, expected_day: 1, }, TestCase { iso_year: 79, iso_month: 3, iso_day: 21, expected_year: 0, expected_month: 12, expected_day: 30, }, TestCase { iso_year: 79, iso_month: 3, iso_day: 20, expected_year: 0, expected_month: 12, expected_day: 29, }, TestCase { iso_year: 78, iso_month: 3, iso_day: 21, expected_year: -1, expected_month: 12, expected_day: 30, }, ]; for case in cases { check_case(case); } } #[test] fn test_cases_near_rd_zero() { let cases = [ TestCase { iso_year: 1, iso_month: 3, iso_day: 22, expected_year: -77, expected_month: 1, expected_day: 1, }, TestCase { iso_year: 1, iso_month: 3, iso_day: 21, expected_year: -78, expected_month: 12, expected_day: 30, }, TestCase { iso_year: 1, iso_month: 1, iso_day: 1, expected_year: -78, expected_month: 10, expected_day: 11, }, TestCase { iso_year: 0, iso_month: 3, iso_day: 21, expected_year: -78, expected_month: 1, expected_day: 1, }, TestCase { iso_year: 0, iso_month: 1, iso_day: 1, expected_year: -79, expected_month: 10, expected_day: 11, }, TestCase { iso_year: -1, iso_month: 3, iso_day: 21, expected_year: -80, expected_month: 12, expected_day: 30, }, ]; for case in cases { check_case(case); } } #[test] fn test_roundtrip_near_rd_zero() { for i in -1000..=1000 { let initial = RataDie::new(i); let result = Date::from_rata_die(initial, Indian).to_rata_die(); assert_eq!( initial, result, "Roundtrip failed for initial: {initial:?}, result: {result:?}" ); } } #[test] fn test_roundtrip_near_epoch_start() { // Epoch start: RD 28570 for i in 27570..=29570 { let initial = RataDie::new(i); let result = Date::from_rata_die(initial, Indian).to_rata_die(); assert_eq!( initial, result, "Roundtrip failed for initial: {initial:?}, result: {result:?}" ); } } #[test] fn test_directionality_near_rd_zero() { for i in -100..=100 { for j in -100..=100 { let rd_i = RataDie::new(i); let rd_j = RataDie::new(j); let indian_i = Date::from_rata_die(rd_i, Indian); let indian_j = Date::from_rata_die(rd_j, Indian); 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:?}"); } } } #[test] fn test_directionality_near_epoch_start() { // Epoch start: RD 28570 for i in 28470..=28670 { for j in 28470..=28670 { let indian_i = Date::from_rata_die(RataDie::new(i), Indian); let indian_j = Date::from_rata_die(RataDie::new(j), Indian); 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:?}"); } } } }