diff --git a/components/calendar/src/cal/abstract_gregorian.rs b/components/calendar/src/cal/abstract_gregorian.rs index 0967e4d1776..93e9f42e5b3 100644 --- a/components/calendar/src/cal/abstract_gregorian.rs +++ b/components/calendar/src/cal/abstract_gregorian.rs @@ -104,7 +104,7 @@ impl DateFieldsResolver for AbstractGregorian { #[inline] fn reference_year_from_month_day( &self, - _month_code: types::MonthCode, + _month_code: types::ValidMonthCode, _day: u8, ) -> Result { Ok(REFERENCE_YEAR) diff --git a/components/calendar/src/cal/chinese.rs b/components/calendar/src/cal/chinese.rs index a96a324e40c..4e3e0ed1469 100644 --- a/components/calendar/src/cal/chinese.rs +++ b/components/calendar/src/cal/chinese.rs @@ -13,7 +13,7 @@ use crate::error::{ use crate::options::{DateAddOptions, DateDifferenceOptions}; use crate::options::{DateFromFieldsOptions, Overflow}; use crate::provider::chinese_based::PackedChineseBasedYearInfo; -use crate::types::{MonthCode, MonthInfo}; +use crate::types::{MonthInfo, ValidMonthCode}; use crate::AsCalendar; use crate::{types, Calendar, Date}; use calendrical_calculations::chinese_based::{ @@ -22,7 +22,6 @@ use calendrical_calculations::chinese_based::{ use calendrical_calculations::rata_die::RataDie; use icu_locale_core::preferences::extensions::unicode::keywords::CalendarAlgorithm; use icu_provider::prelude::*; -use tinystr::tinystr; #[path = "chinese/china_data.rs"] mod china_data; @@ -122,10 +121,11 @@ pub trait Rules: Clone + core::fmt::Debug + crate::cal::scaffold::UnstableSealed /// [`MissingFieldsStrategy::Ecma`]: crate::options::MissingFieldsStrategy::Ecma fn ecma_reference_year( &self, - _month_code: types::MonthCode, + // TODO: Consider accepting ValidMonthCode + _month_code: (u8, bool), _day: u8, ) -> Result { - Err(EcmaReferenceYearError::NotEnoughFields) + Err(EcmaReferenceYearError::Unimplemented) } /// The debug name for the calendar defined by these [`Rules`]. @@ -205,12 +205,12 @@ impl Rules for China { fn ecma_reference_year( &self, - month_code: types::MonthCode, + month_code: (u8, bool), day: u8, ) -> Result { - let (number, is_leap) = month_code.try_parse()?; + let (number, is_leap) = month_code; // Computed by `generate_reference_years` - Ok(match (number, is_leap, day > 29) { + let extended_year = match (number, is_leap, day > 29) { (1, false, false) => 1972, (1, false, true) => 1970, (1, true, false) => 1898, @@ -263,7 +263,8 @@ impl Rules for China { (12, true, false) => 1878, (12, true, true) => 1783, _ => return Err(EcmaReferenceYearError::UnknownMonthCodeForCalendar), - }) + }; + Ok(extended_year) } fn calendar_algorithm(&self) -> Option { @@ -386,12 +387,12 @@ impl Rules for Korea { fn ecma_reference_year( &self, - month_code: types::MonthCode, + month_code: (u8, bool), day: u8, ) -> Result { - let (number, is_leap) = month_code.try_parse()?; + let (number, is_leap) = month_code; // Computed by `generate_reference_years` - Ok(match (number, is_leap, day > 29) { + let extended_year = match (number, is_leap, day > 29) { (1, false, false) => 1972, (1, false, true) => 1970, (1, true, false) => 1898, @@ -444,7 +445,8 @@ impl Rules for Korea { (12, true, false) => 1878, (12, true, true) => 1783, _ => return Err(EcmaReferenceYearError::UnknownMonthCodeForCalendar), - }) + }; + Ok(extended_year) } fn calendar_algorithm(&self) -> Option { @@ -577,18 +579,18 @@ impl DateFieldsResolver for LunarChinese { #[inline] fn reference_year_from_month_day( &self, - month_code: types::MonthCode, + month_code: types::ValidMonthCode, day: u8, ) -> Result { - Ok(self - .0 - .year_data(self.0.ecma_reference_year(month_code, day)?)) + self.0 + .ecma_reference_year(month_code.to_tuple(), day) + .map(|y| self.0.year_data(y)) } fn ordinal_month_from_code( &self, year: &Self::YearInfo, - month_code: types::MonthCode, + month_code: types::ValidMonthCode, options: DateFromFieldsOptions, ) -> Result { match year.parse_month_code(month_code) { @@ -975,25 +977,11 @@ impl LunarChineseYearData { // ordinally `month 2`, zero-indexed) // 14 is a sentinel value let leap_month = self.leap_month().unwrap_or(14); - let code_inner = if leap_month == month { + let valid_month_code = if leap_month == month { // Month cannot be 1 because a year cannot have a leap month before the first actual month, // and the maximum num of months ina leap year is 13. debug_assert!((2..=13).contains(&month)); - match month { - 2 => tinystr!(4, "M01L"), - 3 => tinystr!(4, "M02L"), - 4 => tinystr!(4, "M03L"), - 5 => tinystr!(4, "M04L"), - 6 => tinystr!(4, "M05L"), - 7 => tinystr!(4, "M06L"), - 8 => tinystr!(4, "M07L"), - 9 => tinystr!(4, "M08L"), - 10 => tinystr!(4, "M09L"), - 11 => tinystr!(4, "M10L"), - 12 => tinystr!(4, "M11L"), - 13 => tinystr!(4, "M12L"), - _ => tinystr!(4, "und"), - } + ValidMonthCode::new_unchecked(month - 1, true) } else { let mut adjusted_ordinal = month; if month > leap_month { @@ -1005,27 +993,14 @@ impl LunarChineseYearData { adjusted_ordinal -= 1; } debug_assert!((1..=12).contains(&adjusted_ordinal)); - match adjusted_ordinal { - 1 => tinystr!(4, "M01"), - 2 => tinystr!(4, "M02"), - 3 => tinystr!(4, "M03"), - 4 => tinystr!(4, "M04"), - 5 => tinystr!(4, "M05"), - 6 => tinystr!(4, "M06"), - 7 => tinystr!(4, "M07"), - 8 => tinystr!(4, "M08"), - 9 => tinystr!(4, "M09"), - 10 => tinystr!(4, "M10"), - 11 => tinystr!(4, "M11"), - 12 => tinystr!(4, "M12"), - _ => tinystr!(4, "und"), - } + ValidMonthCode::new_unchecked(adjusted_ordinal, false) }; - let code = MonthCode(code_inner); + let month_code = valid_month_code.to_month_code(); MonthInfo { ordinal: month, - standard_code: code, - formatting_code: code, + valid_standard_code: valid_month_code, + standard_code: month_code, + formatting_code: month_code, } } @@ -1056,12 +1031,12 @@ impl LunarChineseYearData { } /// Get the ordinal lunar month from a code for chinese-based calendars. - fn parse_month_code(self, code: MonthCode) -> ComputedOrdinalMonth { + fn parse_month_code(self, code: ValidMonthCode) -> ComputedOrdinalMonth { // 14 is a sentinel value, greater than all other months, for the purpose of computation only; // it is impossible to actually have 14 months in a year. let leap_month = self.leap_month().unwrap_or(14); - let Some((unadjusted @ 1..13, leap)) = code.parsed() else { + let (unadjusted @ 1..13, leap) = code.to_tuple() else { return ComputedOrdinalMonth::NotFound; }; @@ -1561,12 +1536,13 @@ mod test { (13, tinystr!(4, "M12")), ]; for ordinal_code_pair in codes { - let code = MonthCode(ordinal_code_pair.1); - let ordinal = year.parse_month_code(code); + let print_code = ordinal_code_pair.1; + let valid_code = MonthCode(ordinal_code_pair.1).validated().unwrap(); + let ordinal = year.parse_month_code(valid_code); assert_eq!( ordinal, ComputedOrdinalMonth::Exact(ordinal_code_pair.0), - "Code to ordinal failed for year: {}, code: {code}", + "Code to ordinal failed for year: {}, code: {print_code}", year.related_iso ); } @@ -1577,10 +1553,6 @@ mod test { let non_leap_year = 4659; let leap_year = 4660; let invalid_codes = [ - (non_leap_year, tinystr!(4, "M2")), - (leap_year, tinystr!(4, "M0")), - (non_leap_year, tinystr!(4, "J01")), - (leap_year, tinystr!(4, "3M")), (non_leap_year, tinystr!(4, "M04L")), (leap_year, tinystr!(4, "M04L")), (non_leap_year, tinystr!(4, "M13")), @@ -1589,8 +1561,8 @@ mod test { for (year, code) in invalid_codes { // construct using ::default() to force recomputation let year = LunarChinese::new_china().0.year_data(year); - let code = MonthCode(code); - let ordinal = year.parse_month_code(code); + let valid_code = MonthCode(code).validated().unwrap(); + let ordinal = year.parse_month_code(valid_code); assert!( !matches!(ordinal, ComputedOrdinalMonth::Exact(_)), "Invalid month code failed for year: {}, code: {code}", @@ -1774,7 +1746,7 @@ mod test { .0 .parse() .unwrap(); - if new_lunar_month == lunar_month.parsed().unwrap().0 { + if new_lunar_month == lunar_month.validated().unwrap().number() { lunar_month = MonthCode::new_leap(new_lunar_month).unwrap(); } else { lunar_month = MonthCode::new_normal(new_lunar_month).unwrap(); diff --git a/components/calendar/src/cal/coptic.rs b/components/calendar/src/cal/coptic.rs index d218bfe08d3..7d6c249f4f6 100644 --- a/components/calendar/src/cal/coptic.rs +++ b/components/calendar/src/cal/coptic.rs @@ -106,7 +106,7 @@ impl DateFieldsResolver for Coptic { #[inline] fn reference_year_from_month_day( &self, - month_code: types::MonthCode, + month_code: types::ValidMonthCode, day: u8, ) -> Result { Coptic::reference_year_from_month_day(month_code, day) @@ -116,10 +116,10 @@ impl DateFieldsResolver for Coptic { fn ordinal_month_from_code( &self, _year: &Self::YearInfo, - month_code: types::MonthCode, + month_code: types::ValidMonthCode, _options: DateFromFieldsOptions, ) -> Result { - match month_code.try_parse()? { + match month_code.to_tuple() { (month_number @ 1..=13, false) => Ok(month_number), _ => Err(MonthCodeError::UnknownMonthCodeForCalendar), } @@ -128,10 +128,10 @@ impl DateFieldsResolver for Coptic { impl Coptic { pub(crate) fn reference_year_from_month_day( - month_code: types::MonthCode, + month_code: types::ValidMonthCode, day: u8, ) -> Result { - let (ordinal_month, _is_leap) = month_code.try_parse()?; + let ordinal_month = month_code.number(); // December 31, 1972 occurs on 4th month, 22nd day, 1689 AM let anno_martyrum_year = if ordinal_month < 4 || (ordinal_month == 4 && day <= 22) { 1689 diff --git a/components/calendar/src/cal/ethiopian.rs b/components/calendar/src/cal/ethiopian.rs index d012c61c6ac..8923d966e45 100644 --- a/components/calendar/src/cal/ethiopian.rs +++ b/components/calendar/src/cal/ethiopian.rs @@ -102,7 +102,7 @@ impl DateFieldsResolver for Ethiopian { #[inline] fn reference_year_from_month_day( &self, - month_code: types::MonthCode, + month_code: types::ValidMonthCode, day: u8, ) -> Result { crate::cal::Coptic::reference_year_from_month_day(month_code, day) @@ -112,10 +112,10 @@ impl DateFieldsResolver for Ethiopian { fn ordinal_month_from_code( &self, _year: &Self::YearInfo, - month_code: types::MonthCode, + month_code: types::ValidMonthCode, _options: DateFromFieldsOptions, ) -> Result { - match month_code.try_parse()? { + match month_code.to_tuple() { (month_number @ 1..=13, false) => Ok(month_number), _ => Err(MonthCodeError::UnknownMonthCodeForCalendar), } diff --git a/components/calendar/src/cal/hebrew.rs b/components/calendar/src/cal/hebrew.rs index c0e7d3e712c..6fafedab56e 100644 --- a/components/calendar/src/cal/hebrew.rs +++ b/components/calendar/src/cal/hebrew.rs @@ -2,6 +2,8 @@ // called LICENSE at the top level of the ICU4X source tree // (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). +use core::cmp::Ordering; + use crate::cal::iso::{Iso, IsoDateInner}; use crate::calendar_arithmetic::ArithmeticDateBuilder; use crate::calendar_arithmetic::{ @@ -12,7 +14,7 @@ use crate::error::{ }; use crate::options::{DateAddOptions, DateDifferenceOptions}; use crate::options::{DateFromFieldsOptions, Overflow}; -use crate::types::{DateFields, MonthInfo}; +use crate::types::{DateFields, MonthInfo, ValidMonthCode}; use crate::RangeError; use crate::{types, Calendar, Date}; use ::tinystr::tinystr; @@ -139,10 +141,11 @@ impl DateFieldsResolver for Hebrew { fn reference_year_from_month_day( &self, - month_code: types::MonthCode, + month_code: types::ValidMonthCode, day: u8, ) -> Result { - month_code.try_parse()?; // return InvalidMonthCode + // Match statements are more readable with strings. + let month_code = month_code.to_month_code(); let month_code_str = month_code.0.as_str(); // December 31, 1972 occurs on 4th month, 26th day, 5733 AM let hebrew_year = match month_code_str { @@ -183,11 +186,12 @@ impl DateFieldsResolver for Hebrew { fn ordinal_month_from_code( &self, year: &Self::YearInfo, - month_code: types::MonthCode, + month_code: types::ValidMonthCode, options: DateFromFieldsOptions, ) -> Result { + // Match statements are more readable with strings. + let month_code = month_code.to_month_code(); let is_leap_year = year.keviyah.is_leap(); - month_code.try_parse()?; // return InvalidMonthCode let month_code_str = month_code.0.as_str(); let ordinal_month = if is_leap_year { match month_code_str { @@ -243,50 +247,25 @@ impl DateFieldsResolver for Hebrew { year: &Self::YearInfo, ordinal_month: u8, ) -> types::MonthInfo { - let mut ordinal = ordinal_month; let is_leap_year = Self::provided_year_is_leap(*year); - if is_leap_year { - if ordinal == 6 { - return types::MonthInfo { - ordinal, - standard_code: types::MonthCode(tinystr!(4, "M05L")), - formatting_code: types::MonthCode(tinystr!(4, "M05L")), - }; - } else if ordinal == 7 { - return types::MonthInfo { - ordinal, - // Adar II is the same as Adar and has the same code - standard_code: types::MonthCode(tinystr!(4, "M06")), - formatting_code: types::MonthCode(tinystr!(4, "M06L")), - }; - } - } - - if is_leap_year && ordinal > 6 { - ordinal -= 1; - } - - let code = match ordinal { - 1 => tinystr!(4, "M01"), - 2 => tinystr!(4, "M02"), - 3 => tinystr!(4, "M03"), - 4 => tinystr!(4, "M04"), - 5 => tinystr!(4, "M05"), - 6 => tinystr!(4, "M06"), - 7 => tinystr!(4, "M07"), - 8 => tinystr!(4, "M08"), - 9 => tinystr!(4, "M09"), - 10 => tinystr!(4, "M10"), - 11 => tinystr!(4, "M11"), - 12 => tinystr!(4, "M12"), - _ => tinystr!(4, "und"), + let valid_month_code = match (ordinal_month.cmp(&6), is_leap_year) { + (Ordering::Less, _) | (_, false) => ValidMonthCode::new_unchecked(ordinal_month, false), + (Ordering::Equal, true) => ValidMonthCode::new_unchecked(5, true), + (Ordering::Greater, true) => ValidMonthCode::new_unchecked(ordinal_month - 1, false), + }; + let standard_code = valid_month_code.to_month_code(); + let formatting_code = if is_leap_year && ordinal_month == 7 { + ValidMonthCode::new_unchecked(6, true).to_month_code() // M06L + } else { + standard_code }; types::MonthInfo { ordinal: ordinal_month, - standard_code: types::MonthCode(code), - formatting_code: types::MonthCode(code), + valid_standard_code: valid_month_code, + standard_code, + formatting_code, } } } diff --git a/components/calendar/src/cal/hijri.rs b/components/calendar/src/cal/hijri.rs index e75e644378e..4fd76348fcc 100644 --- a/components/calendar/src/cal/hijri.rs +++ b/components/calendar/src/cal/hijri.rs @@ -84,10 +84,11 @@ pub trait Rules: Clone + Debug + crate::cal::scaffold::UnstableSealed { /// [`MissingFieldsStrategy::Ecma`]: crate::options::MissingFieldsStrategy::Ecma fn ecma_reference_year( &self, - _month_code: types::MonthCode, + // TODO: Consider accepting ValidMonthCode + _month_code: (u8, bool), _day: u8, ) -> Result { - Err(EcmaReferenceYearError::NotEnoughFields) + Err(EcmaReferenceYearError::Unimplemented) } /// The BCP-47 [`CalendarAlgorithm`] for the Hijri calendar using these rules, if defined. @@ -253,14 +254,14 @@ impl Rules for UmmAlQura { fn ecma_reference_year( &self, - month_code: types::MonthCode, + month_code: (u8, bool), day: u8, ) -> Result { - let (ordinal_month, false) = month_code.try_parse()? else { + let (ordinal_month, false) = month_code else { return Err(EcmaReferenceYearError::UnknownMonthCodeForCalendar); }; - Ok(match (ordinal_month, day) { + let extended_year = match (ordinal_month, day) { (1, _) => 1392, (2, 30..) => 1390, (2, _) => 1392, @@ -281,7 +282,8 @@ impl Rules for UmmAlQura { (12, 30..) => 1390, (12, _) => 1391, _ => return Err(EcmaReferenceYearError::UnknownMonthCodeForCalendar), - }) + }; + Ok(extended_year) } fn debug_name(&self) -> &'static str { @@ -343,10 +345,10 @@ impl Rules for TabularAlgorithm { fn ecma_reference_year( &self, - month_code: types::MonthCode, + month_code: (u8, bool), day: u8, ) -> Result { - let (ordinal_month, false) = month_code.try_parse()? else { + let (ordinal_month, false) = month_code else { return Err(EcmaReferenceYearError::UnknownMonthCodeForCalendar); }; @@ -610,9 +612,7 @@ fn computer_reference_years() { where C: CalendarArithmetic, { - let (ordinal_month, _is_leap) = month_code - .parsed() - .ok_or(DateError::UnknownMonthCode(month_code))?; + let ordinal_month = month_code.validated().unwrap().number(); let dec_31 = Date::from_rata_die( crate::cal::abstract_gregorian::LAST_DAY_OF_REFERENCE_YEAR, crate::Ref(cal), @@ -744,12 +744,12 @@ impl DateFieldsResolver for Hijri { #[inline] fn reference_year_from_month_day( &self, - month_code: types::MonthCode, + month_code: types::ValidMonthCode, day: u8, ) -> Result { - Ok(self - .0 - .year_data(self.0.ecma_reference_year(month_code, day)?)) + self.0 + .ecma_reference_year(month_code.to_tuple(), day) + .map(|y| self.0.year_data(y)) } } diff --git a/components/calendar/src/cal/indian.rs b/components/calendar/src/cal/indian.rs index 9cb55bd496f..2ff06e71436 100644 --- a/components/calendar/src/cal/indian.rs +++ b/components/calendar/src/cal/indian.rs @@ -99,10 +99,10 @@ impl DateFieldsResolver for Indian { #[inline] fn reference_year_from_month_day( &self, - month_code: types::MonthCode, + month_code: types::ValidMonthCode, day: u8, ) -> Result { - let (ordinal_month, _is_leap) = month_code.try_parse()?; + let ordinal_month = month_code.number(); // 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) { diff --git a/components/calendar/src/cal/julian.rs b/components/calendar/src/cal/julian.rs index 74e32ae5909..19cb8f67cbf 100644 --- a/components/calendar/src/cal/julian.rs +++ b/components/calendar/src/cal/julian.rs @@ -125,10 +125,10 @@ impl DateFieldsResolver for Julian { #[inline] fn reference_year_from_month_day( &self, - month_code: types::MonthCode, + month_code: types::ValidMonthCode, day: u8, ) -> Result { - let (ordinal_month, _is_leap) = month_code.try_parse()?; + let ordinal_month = month_code.number(); // December 31, 1972 occurs on 12th month, 18th day, 1972 Old Style // Note: 1972 is a leap year let julian_year = if ordinal_month < 12 || (ordinal_month == 12 && day <= 18) { diff --git a/components/calendar/src/cal/persian.rs b/components/calendar/src/cal/persian.rs index ac9b2ec2193..00848ac4613 100644 --- a/components/calendar/src/cal/persian.rs +++ b/components/calendar/src/cal/persian.rs @@ -98,10 +98,10 @@ impl DateFieldsResolver for Persian { #[inline] fn reference_year_from_month_day( &self, - month_code: types::MonthCode, + month_code: types::ValidMonthCode, day: u8, ) -> Result { - let (ordinal_month, _is_leap) = month_code.try_parse()?; + let ordinal_month = month_code.number(); // December 31, 1972 occurs on 10th month, 10th day, 1351 AP let persian_year = if ordinal_month < 10 || (ordinal_month == 10 && day <= 10) { 1351 diff --git a/components/calendar/src/calendar_arithmetic.rs b/components/calendar/src/calendar_arithmetic.rs index 84ced09ac8b..29fa37d59c0 100644 --- a/components/calendar/src/calendar_arithmetic.rs +++ b/components/calendar/src/calendar_arithmetic.rs @@ -8,7 +8,7 @@ use crate::error::{ }; use crate::options::{DateAddOptions, DateDifferenceOptions}; use crate::options::{DateFromFieldsOptions, MissingFieldsStrategy, Overflow}; -use crate::types::{DateDuration, DateDurationUnit, DateFields, DayOfYear, MonthCode}; +use crate::types::{DateDuration, DateDurationUnit, DateFields, DayOfYear, ValidMonthCode}; use crate::{types, Calendar, DateError, RangeError}; use core::cmp::Ordering; use core::convert::TryInto; @@ -16,7 +16,6 @@ use core::fmt::Debug; use core::hash::{Hash, Hasher}; use core::marker::PhantomData; use core::ops::RangeInclusive; -use tinystr::tinystr; /// The range ±2²⁷. We use i32::MIN since it is -2³¹ const VALID_YEAR_RANGE: RangeInclusive = (i32::MIN / 16)..=-(i32::MIN / 16); @@ -167,7 +166,7 @@ pub(crate) trait DateFieldsResolver: Calendar { /// day for the given month. fn reference_year_from_month_day( &self, - month_code: MonthCode, + month_code: ValidMonthCode, day: u8, ) -> Result; @@ -178,10 +177,10 @@ pub(crate) trait DateFieldsResolver: Calendar { fn ordinal_month_from_code( &self, _year: &Self::YearInfo, - month_code: MonthCode, + month_code: ValidMonthCode, _options: DateFromFieldsOptions, ) -> Result { - match month_code.try_parse()? { + match month_code.to_tuple() { (month_number @ 1..=12, false) => Ok(month_number), _ => Err(MonthCodeError::UnknownMonthCodeForCalendar), } @@ -198,17 +197,13 @@ pub(crate) trait DateFieldsResolver: Calendar { _year: &Self::YearInfo, ordinal_month: u8, ) -> types::MonthInfo { - let code = match MonthCode::new_normal(ordinal_month) { - Some(code) => code, - None => { - debug_assert!(false, "ordinal month out of range!"); - MonthCode(tinystr!(4, "und")) - } - }; + let valid_month_code = ValidMonthCode::new_unchecked(ordinal_month, false); + let month_code = valid_month_code.to_month_code(); types::MonthInfo { ordinal: ordinal_month, - standard_code: code, - formatting_code: code, + valid_standard_code: valid_month_code, + standard_code: month_code, + formatting_code: month_code, } } } @@ -410,7 +405,7 @@ impl ArithmeticDate { // 1. Let _m0_ be MonthCodeToOrdinal(_calendar_, _y0_, ! ConstrainMonthCode(_calendar_, _y0_, _parts_.[[MonthCode]], ~constrain~)). let base_month_code = cal .month_code_from_ordinal(&self.year, self.month) - .standard_code; + .valid_standard_code; let constrain = DateFromFieldsOptions { overflow: Some(Overflow::Constrain), ..Default::default() @@ -507,26 +502,24 @@ impl ArithmeticDate { // 1. Let _y0_ be _parts_.[[Year]] + _duration_.[[Years]]. let y0 = cal.year_info_from_extended(duration.add_years_to(self.year.to_extended_year())); // 1. Let _m0_ be MonthCodeToOrdinal(_calendar_, _y0_, ! ConstrainMonthCode(_calendar_, _y0_, _parts_.[[MonthCode]], _overflow_)). - let base_month_code = cal - .month_code_from_ordinal(&self.year, self.month) - .standard_code; + let base_month = cal.month_code_from_ordinal(&self.year, self.month); let m0 = cal .ordinal_month_from_code( &y0, - base_month_code, + base_month.valid_standard_code, DateFromFieldsOptions::from_add_options(options), ) .map_err(|e| { // TODO: Use a narrower error type here. For now, convert into DateError. match e { MonthCodeError::InvalidMonthCode => { - DateError::UnknownMonthCode(base_month_code) + DateError::UnknownMonthCode(base_month.standard_code) } MonthCodeError::UnknownMonthCodeForCalendar => { - DateError::UnknownMonthCode(base_month_code) + DateError::UnknownMonthCode(base_month.standard_code) } MonthCodeError::UnknownMonthCodeForYear => { - DateError::UnknownMonthCode(base_month_code) + DateError::UnknownMonthCode(base_month.standard_code) } } })?; @@ -720,6 +713,8 @@ where return Err(DateFromFieldsError::NotEnoughFields); } + let mut valid_month_code = None; + let year = { // NOTE: The year/extendedyear range check is important to avoid arithmetic // overflow in `year_info_from_era` and `year_info_from_extended`. It @@ -739,7 +734,9 @@ where MissingFieldsStrategy::Ecma => { match (fields.month_code, fields.ordinal_month) { (Some(month_code), None) => { - cal.reference_year_from_month_day(month_code, day)? + let validated = month_code.validated()?; + valid_month_code = Some(validated); + cal.reference_year_from_month_day(validated, day)? } _ => return Err(DateFromFieldsError::NotEnoughFields), } @@ -767,7 +764,11 @@ where let month = { match fields.month_code { Some(month_code) => { - let computed_month = cal.ordinal_month_from_code(&year, month_code, options)?; + let validated = match valid_month_code { + Some(validated) => validated, + None => month_code.validated()?, + }; + let computed_month = cal.ordinal_month_from_code(&year, validated, options)?; if let Some(ordinal_month) = fields.ordinal_month { if computed_month != ordinal_month { return Err(DateFromFieldsError::InconsistentMonth); diff --git a/components/calendar/src/error.rs b/components/calendar/src/error.rs index f51e6c8f95d..ba13ba20601 100644 --- a/components/calendar/src/error.rs +++ b/components/calendar/src/error.rs @@ -325,16 +325,17 @@ impl From for DateFromFieldsError { } } -/// Internal narrow error type for functions that only fail on invalid month codes +/// Internal narrow error type for functions that only fail on parsing month codes +#[derive(Debug)] pub(crate) enum MonthCodeParseError { - InvalidMonthCode, + InvalidSyntax, } -impl From for EcmaReferenceYearError { +impl From for DateFromFieldsError { #[inline] fn from(value: MonthCodeParseError) -> Self { match value { - MonthCodeParseError::InvalidMonthCode => EcmaReferenceYearError::InvalidMonthCode, + MonthCodeParseError::InvalidSyntax => DateFromFieldsError::InvalidMonthCode, } } } @@ -343,7 +344,7 @@ impl From for MonthCodeError { #[inline] fn from(value: MonthCodeParseError) -> Self { match value { - MonthCodeParseError::InvalidMonthCode => MonthCodeError::InvalidMonthCode, + MonthCodeParseError::InvalidSyntax => MonthCodeError::InvalidMonthCode, } } } @@ -376,9 +377,8 @@ mod inner { #[allow(missing_docs)] // TODO: fix when graduating #[non_exhaustive] pub enum EcmaReferenceYearError { - InvalidMonthCode, + Unimplemented, UnknownMonthCodeForCalendar, - NotEnoughFields, } } @@ -391,11 +391,10 @@ impl From for DateFromFieldsError { #[inline] fn from(value: EcmaReferenceYearError) -> Self { match value { - EcmaReferenceYearError::InvalidMonthCode => DateFromFieldsError::InvalidMonthCode, + EcmaReferenceYearError::Unimplemented => DateFromFieldsError::NotEnoughFields, EcmaReferenceYearError::UnknownMonthCodeForCalendar => { DateFromFieldsError::UnknownMonthCodeForCalendar } - EcmaReferenceYearError::NotEnoughFields => DateFromFieldsError::NotEnoughFields, } } } diff --git a/components/calendar/src/types.rs b/components/calendar/src/types.rs index e79f6d89b20..d8209e07c03 100644 --- a/components/calendar/src/types.rs +++ b/components/calendar/src/types.rs @@ -174,6 +174,8 @@ pub struct CyclicYear { pub struct MonthCode(pub TinyStr4); impl MonthCode { + pub(crate) const SENTINEL: MonthCode = MonthCode(tinystr::tinystr!(4, "und")); + /// Returns an option which is `Some` containing the non-month version of a leap month /// if the [`MonthCode`] this method is called upon is a leap month, and `None` otherwise. /// This method assumes the [`MonthCode`] is valid. @@ -187,23 +189,30 @@ impl MonthCode { } /// Get the month number and whether or not it is leap from the month code pub fn parsed(self) -> Option<(u8, bool)> { - self.try_parse().ok() + let valid_month_code = self.validated().ok()?; + Some((valid_month_code.number(), valid_month_code.is_leap())) } - pub(crate) fn try_parse(self) -> Result<(u8, bool), MonthCodeParseError> { + + /// Validates the syntax and returns a [`ValidMonthCode`], from which the + /// month number and leap month status can be read. + pub(crate) fn validated(self) -> Result { // Match statements on tinystrs are annoying so instead // we calculate it from the bytes directly let bytes = self.0.all_bytes(); let is_leap = bytes[3] == b'L'; if bytes[0] != b'M' { - return Err(MonthCodeParseError::InvalidMonthCode); + return Err(MonthCodeParseError::InvalidSyntax); } let b1 = bytes[1]; let b2 = bytes[2]; if !b1.is_ascii_digit() || !b2.is_ascii_digit() { - return Err(MonthCodeParseError::InvalidMonthCode); + return Err(MonthCodeParseError::InvalidSyntax); } - Ok(((b1 - b'0') * 10 + b2 - b'0', is_leap)) + Ok(ValidMonthCode { + number: (b1 - b'0') * 10 + b2 - b'0', + is_leap, + }) } /// Construct a "normal" month code given a number ("Mxx"). @@ -274,6 +283,76 @@ impl fmt::Display for MonthCode { } } +/// A [`MonthCode`] that has been parsed into its internal representation. +#[derive(Copy, Clone, Debug, PartialEq)] +pub(crate) struct ValidMonthCode { + /// Month number between 0 and 99 + number: u8, + is_leap: bool, +} + +impl ValidMonthCode { + /// Create a new ValidMonthCode without checking that the number is between 0 and 99 + #[inline] + pub(crate) fn new_unchecked(number: u8, is_leap: bool) -> Self { + debug_assert!(number <= 99); + Self { number, is_leap } + } + + /// Returns the month number according to the month code. + /// + /// This is NOT the same as the ordinal month! + /// + /// # Examples + /// + /// ```ignore + /// use icu::calendar::Date; + /// use icu::calendar::cal::Hebrew; + /// + /// let hebrew_date = Date::try_new_iso(2024, 7, 1).unwrap().to_calendar(Hebrew); + /// let month_info = hebrew_date.month(); + /// + /// // Hebrew year 5784 was a leap year, so the ordinal month and month number diverge. + /// assert_eq!(month_info.ordinal, 10); + /// assert_eq!(month_info.valid_month_code.number(), 9); + /// ``` + #[inline] + pub fn number(self) -> u8 { + self.number + } + + /// Returns whether the month is a leap month. + /// + /// This is true for intercalary months in [`Hebrew`] and [`LunarChinese`]. + /// + /// [`Hebrew`]: crate::cal::Hebrew + /// [`LunarChinese`]: crate::cal::LunarChinese + #[inline] + pub fn is_leap(self) -> bool { + self.is_leap + } + + #[inline] + pub(crate) fn to_tuple(self) -> (u8, bool) { + (self.number, self.is_leap) + } + + pub(crate) fn to_month_code(self) -> MonthCode { + let option = if self.is_leap { + MonthCode::new_leap(self.number) + } else { + MonthCode::new_normal(self.number) + }; + option.unwrap_or_else(|| { + debug_assert!( + false, + "ValidMonthCode invariants guarantee conversion to MonthCode" + ); + MonthCode::SENTINEL + }) + } +} + /// Representation of a formattable month. #[derive(Copy, Clone, Debug, PartialEq)] #[non_exhaustive] @@ -296,6 +375,9 @@ pub struct MonthInfo { /// [`Date::try_from_fields`]: crate::Date::try_from_fields pub standard_code: MonthCode, + /// Same as [`Self::standard_code`] but with invariants validated. + pub(crate) valid_standard_code: ValidMonthCode, + /// A month code, useable for formatting. /// /// Does NOT necessarily round-trip through `Date` constructors like [`Date::try_new_from_codes`] and [`Date::try_from_fields`]. @@ -315,15 +397,12 @@ impl MonthInfo { /// if there are leap months in the year, rather it is associated with the Nth month of a "regular" /// year. There may be multiple month Ns in a year pub fn month_number(self) -> u8 { - self.standard_code - .parsed() - .map(|(i, _)| i) - .unwrap_or(self.ordinal) + self.valid_standard_code.number() } /// Get whether the month is a leap month pub fn is_leap(self) -> bool { - self.standard_code.parsed().map(|(_, l)| l).unwrap_or(false) + self.valid_standard_code.is_leap() } }