From 4a02b84533f35f271f60071229e63238c70d36ea Mon Sep 17 00:00:00 2001 From: Kevin Ness <46825870+nekevss@users.noreply.github.com> Date: Sun, 23 Feb 2025 00:05:29 -0600 Subject: [PATCH 1/4] Integrate MonthCode into the public API over TinyAsciiStr --- src/builtins/compiled/zoneddatetime.rs | 4 +- src/builtins/core/calendar.rs | 15 +- src/builtins/core/calendar/types.rs | 205 +++++++++++++++++-------- src/builtins/core/date.rs | 20 +-- src/builtins/core/datetime.rs | 10 +- src/builtins/core/month_day.rs | 6 +- src/builtins/core/year_month.rs | 4 +- src/builtins/core/zoneddatetime.rs | 8 +- src/lib.rs | 6 +- 9 files changed, 176 insertions(+), 102 deletions(-) diff --git a/src/builtins/compiled/zoneddatetime.rs b/src/builtins/compiled/zoneddatetime.rs index a1f095f9e..ef62ecacc 100644 --- a/src/builtins/compiled/zoneddatetime.rs +++ b/src/builtins/compiled/zoneddatetime.rs @@ -5,7 +5,7 @@ use crate::{ ArithmeticOverflow, DifferenceSettings, Disambiguation, DisplayCalendar, DisplayOffset, DisplayTimeZone, OffsetDisambiguation, ToStringRoundingOptions, }, - Duration, PlainDate, PlainDateTime, PlainTime, TemporalError, TemporalResult, + Duration, MonthCode, PlainDate, PlainDateTime, PlainTime, TemporalError, TemporalResult, }; use alloc::string::String; use tinystr::TinyAsciiStr; @@ -58,7 +58,7 @@ impl ZonedDateTime { /// Returns the `ZonedDateTime`'s calendar month code. /// /// Enable with the `compiled_data` feature flag. - pub fn month_code(&self) -> TemporalResult> { + pub fn month_code(&self) -> TemporalResult { let provider = TZ_PROVIDER .lock() .map_err(|_| TemporalError::general("Unable to acquire lock"))?; diff --git a/src/builtins/core/calendar.rs b/src/builtins/core/calendar.rs index 0d966c47d..304101598 100644 --- a/src/builtins/core/calendar.rs +++ b/src/builtins/core/calendar.rs @@ -40,8 +40,8 @@ use super::{PartialDate, ZonedDateTime}; mod era; mod types; -pub use types::ResolvedCalendarFields; -pub(crate) use types::{ascii_four_to_integer, month_to_month_code}; +pub(crate) use types::month_to_month_code; +pub use types::{MonthCode, ResolvedCalendarFields}; use era::EraInfo; @@ -232,7 +232,7 @@ impl Calendar { // Resolve month and monthCode; return PlainDate::new_with_overflow( resolved_fields.era_year.year, - resolved_fields.month_code.as_iso_month_integer()?, + resolved_fields.month_code.to_month_integer(), resolved_fields.day, self.clone(), overflow, @@ -267,7 +267,7 @@ impl Calendar { let resolved_fields = ResolvedCalendarFields::try_from_partial(partial, overflow)?; if self.is_iso() { return PlainMonthDay::new_with_overflow( - resolved_fields.month_code.as_iso_month_integer()?, + resolved_fields.month_code.to_month_integer(), resolved_fields.day, self.clone(), overflow, @@ -290,7 +290,7 @@ impl Calendar { if self.is_iso() { return PlainYearMonth::new_with_overflow( resolved_fields.era_year.year, - resolved_fields.month_code.as_iso_month_integer()?, + resolved_fields.month_code.to_month_integer(), Some(resolved_fields.day), self.clone(), overflow, @@ -401,9 +401,10 @@ impl Calendar { } /// `CalendarMonthCode` - pub fn month_code(&self, iso_date: &IsoDate) -> TemporalResult> { + pub fn month_code(&self, iso_date: &IsoDate) -> TemporalResult { if self.is_iso() { - return Ok(iso_date.as_icu4x()?.month().standard_code.0); + let mc = iso_date.as_icu4x()?.month().standard_code.0; + return Ok(MonthCode(mc)); } Err(TemporalError::range().with_message("Not yet implemented.")) diff --git a/src/builtins/core/calendar/types.rs b/src/builtins/core/calendar/types.rs index 1221a291e..e053dfb58 100644 --- a/src/builtins/core/calendar/types.rs +++ b/src/builtins/core/calendar/types.rs @@ -40,9 +40,9 @@ impl ResolvedCalendarFields { .ok_or(TemporalError::r#type().with_message("Required day field is empty."))?; let day = if overflow == ArithmeticOverflow::Constrain { - constrain_iso_day(era_year.year, ascii_four_to_integer(month_code)?, day) + constrain_iso_day(era_year.year, month_code.to_month_integer(), day) } else { - if !is_valid_iso_day(era_year.year, ascii_four_to_integer(month_code)?, day) { + if !is_valid_iso_day(era_year.year, month_code.to_month_integer(), day) { return Err( TemporalError::range().with_message("day value is not in a valid range.") ); @@ -51,12 +51,12 @@ impl ResolvedCalendarFields { }; return Ok(Self { era_year, - month_code: MonthCode(month_code), + month_code, day, }); } - let month_code = MonthCode::try_from_partial_date(partial_date, &partial_date.calendar)?; + let month_code = MonthCode::try_from_partial_date(partial_date)?; let day = partial_date .day .ok_or(TemporalError::r#type().with_message("Required day field is empty."))?; @@ -148,11 +148,11 @@ const MONTH_THIRTEEN: TinyAsciiStr<4> = tinystr!(4, "M13"); // bounds. In other words, it is totally possible for a value to be // passed in that is { month: 300 } with overflow::constrain. /// MonthCode struct v2 -#[derive(Debug)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct MonthCode(pub(crate) TinyAsciiStr<4>); impl MonthCode { - pub fn try_new(month_code: &TinyAsciiStr<4>, calendar: &Calendar) -> TemporalResult { + pub(crate) fn validate(&self, calendar: &Calendar) -> TemporalResult<()> { const COMMON_MONTH_CODES: [TinyAsciiStr<4>; 12] = [ MONTH_ONE, MONTH_TWO, @@ -183,109 +183,146 @@ impl MonthCode { MONTH_TWELVE_LEAP, ]; - if COMMON_MONTH_CODES.contains(month_code) { - return Ok(MonthCode(*month_code)); + if COMMON_MONTH_CODES.contains(&self.0) { + return Ok(()); } match calendar.identifier() { - "chinese" | "dangi" if LUNAR_LEAP_MONTHS.contains(month_code) => { - Ok(MonthCode(*month_code)) - } - "coptic" | "ethiopic" | "ethiopicaa" if MONTH_THIRTEEN == *month_code => { - Ok(MonthCode(*month_code)) - } - "hebrew" if MONTH_FIVE_LEAP == *month_code => Ok(MonthCode(*month_code)), + "chinese" | "dangi" if LUNAR_LEAP_MONTHS.contains(&self.0) => Ok(()), + "coptic" | "ethiopic" | "ethiopicaa" if MONTH_THIRTEEN == self.0 => Ok(()), + "hebrew" if MONTH_FIVE_LEAP == self.0 => Ok(()), _ => Err(TemporalError::range() .with_message("MonthCode was not valid for the current calendar.")), } } - pub(crate) fn try_from_partial_date( - partial_date: &PartialDate, - calendar: &Calendar, - ) -> TemporalResult { + pub(crate) fn try_from_partial_date(partial_date: &PartialDate) -> TemporalResult { match partial_date { PartialDate { month: Some(month), month_code: None, + calendar, .. - } => Self::try_new(&month_to_month_code(*month)?, calendar), + } => { + let month_code = month_to_month_code(*month)?; + month_code.validate(calendar)?; + Ok(month_code) + } PartialDate { month_code: Some(month_code), month: None, + calendar, .. - } => Self::try_new(month_code, calendar), + } => { + month_code.validate(calendar)?; + Ok(*month_code) + } PartialDate { month: Some(month), month_code: Some(month_code), + calendar, .. } => { are_month_and_month_code_resolvable(*month, month_code)?; - Self::try_new(month_code, calendar) + month_code.validate(calendar)?; + Ok(*month_code) } _ => Err(TemporalError::r#type() .with_message("Month or monthCode is required to determine date.")), } } - pub fn as_iso_month_integer(&self) -> TemporalResult { + /// Returns the `MonthCode` as an integer + pub fn to_month_integer(&self) -> u8 { ascii_four_to_integer(self.0) } + + /// Returns whether the `MonthCode` is a leap month. + pub fn is_leap_month(&self) -> bool { + let bytes = self.0.all_bytes(); + bytes[3] == b'L' + } + + pub fn as_str(&self) -> &str { + self.0.as_str() + } + + pub fn as_tinystr(&self) -> TinyAsciiStr<4> { + self.0 + } + + pub fn try_from_utf8(src: &[u8]) -> TemporalResult { + if !(3..=4).contains(&src.len()) { + return Err(TemporalError::range()); + } + + let inner = TinyAsciiStr::<4>::try_from_utf8(src).map_err(|_e| TemporalError::range())?; + + let bytes = inner.all_bytes(); + if bytes[0] != b'M' { + return Err( + TemporalError::range().with_message("First month code character must be 'M'") + ); + } + if !bytes[1].is_ascii_digit() || !bytes[2].is_ascii_digit() { + return Err(TemporalError::range().with_message("Invalid month code digit")); + } + if src.len() == 4 && bytes[3] != b'L' { + return Err(TemporalError::range().with_message("Leap month code must end with 'L'")); + } + + Ok(Self(inner)) + } +} + +impl core::str::FromStr for MonthCode { + type Err = TemporalError; + fn from_str(s: &str) -> Result { + Self::try_from_utf8(s.as_bytes()) + } } // NOTE: This is a greedy function, should handle differently for all calendars. -pub(crate) fn month_to_month_code(month: u8) -> TemporalResult> { - match month { - 1 => Ok(MONTH_ONE), - 2 => Ok(MONTH_TWO), - 3 => Ok(MONTH_THREE), - 4 => Ok(MONTH_FOUR), - 5 => Ok(MONTH_FIVE), - 6 => Ok(MONTH_SIX), - 7 => Ok(MONTH_SEVEN), - 8 => Ok(MONTH_EIGHT), - 9 => Ok(MONTH_NINE), - 10 => Ok(MONTH_TEN), - 11 => Ok(MONTH_ELEVEN), - 12 => Ok(MONTH_TWELVE), - 13 => Ok(MONTH_THIRTEEN), - _ => Err(TemporalError::range().with_message("Month not in a valid range.")), +pub(crate) fn month_to_month_code(month: u8) -> TemporalResult { + if !(1..=13).contains(&month) { + return Err(TemporalError::range().with_message("Month not in a valid range.")); } + let first = month / 10; + let second = month % 10; + let tinystr = TinyAsciiStr::<4>::try_from_raw([b'M', first + 48, second + 48, b'\0']) + .map_err(|e| TemporalError::range().with_message(format!("tinystr error {e}")))?; + Ok(MonthCode(tinystr)) } -fn are_month_and_month_code_resolvable(month: u8, mc: &TinyAsciiStr<4>) -> TemporalResult<()> { - if month != ascii_four_to_integer(*mc)? { +fn are_month_and_month_code_resolvable(month: u8, mc: &MonthCode) -> TemporalResult<()> { + if month != mc.to_month_integer() { return Err(TemporalError::range() .with_message("Month and monthCode values could not be resolved.")); } Ok(()) } -// NOTE: This is a greedy function, should handle differently for all calendars. -pub(crate) fn ascii_four_to_integer(mc: TinyAsciiStr<4>) -> TemporalResult { - match mc { - MONTH_ONE => Ok(1), - MONTH_TWO => Ok(2), - MONTH_THREE => Ok(3), - MONTH_FOUR => Ok(4), - MONTH_FIVE => Ok(5), - MONTH_SIX => Ok(6), - MONTH_SEVEN => Ok(7), - MONTH_EIGHT => Ok(8), - MONTH_NINE => Ok(9), - MONTH_TEN => Ok(10), - MONTH_ELEVEN => Ok(11), - MONTH_TWELVE => Ok(12), - _ => Err(TemporalError::range() - .with_message(format!("MonthCode is not supported: {}", mc.as_str()))), - } +// Potentially greedy. Need to verify for all calendars that +// the code interger aligns with the month integer, which +// may require calendar info +pub(crate) fn ascii_four_to_integer(mc: TinyAsciiStr<4>) -> u8 { + let bytes = mc.all_bytes(); + // Invariant: first and second character are ascii digits. + debug_assert!(bytes[1].is_ascii_digit()); + debug_assert!(bytes[2].is_ascii_digit()); + let first = ascii_digit_to_int(bytes[1]) * 10; + first + ascii_digit_to_int(bytes[2]) +} + +const fn ascii_digit_to_int(ascii_digit: u8) -> u8 { + ascii_digit - 48 } fn resolve_iso_month( - mc: Option>, + mc: Option, month: Option, overflow: ArithmeticOverflow, -) -> TemporalResult> { +) -> TemporalResult { match (mc, month) { (None, None) => { Err(TemporalError::r#type().with_message("Month or monthCode must be provided.")) @@ -303,12 +340,10 @@ fn resolve_iso_month( } (Some(mc), None) => { // Check that monthCode is parsable. - let _ = ascii_four_to_integer(mc)?; Ok(mc) } (Some(mc), Some(month)) => { - let month_code_int = ascii_four_to_integer(mc)?; - if month != month_code_int { + if month != mc.to_month_integer() { return Err(TemporalError::range() .with_message("month and monthCode could not be resolved.")); } @@ -319,6 +354,8 @@ fn resolve_iso_month( #[cfg(test)] mod tests { + use core::str::FromStr; + use tinystr::tinystr; use crate::{ @@ -326,7 +363,43 @@ mod tests { options::ArithmeticOverflow, }; - use super::ResolvedCalendarFields; + use super::{month_to_month_code, MonthCode, ResolvedCalendarFields}; + + #[test] + fn valid_month_code() { + let month_code = MonthCode::from_str("M01").unwrap(); + assert!(!month_code.is_leap_month()); + assert_eq!(month_code.to_month_integer(), 1); + + let month_code = MonthCode::from_str("M12").unwrap(); + assert!(!month_code.is_leap_month()); + assert_eq!(month_code.to_month_integer(), 12); + + let month_code = MonthCode::from_str("M13L").unwrap(); + assert!(month_code.is_leap_month()); + assert_eq!(month_code.to_month_integer(), 13); + } + + #[test] + fn invalid_month_code() { + let _ = MonthCode::from_str("01").unwrap_err(); + let _ = MonthCode::from_str("N01").unwrap_err(); + let _ = MonthCode::from_str("M01R").unwrap_err(); + let _ = MonthCode::from_str("M1").unwrap_err(); + let _ = MonthCode::from_str("M1L").unwrap_err(); + } + + #[test] + fn month_to_mc() { + let mc = month_to_month_code(1).unwrap(); + assert_eq!(mc.as_str(), "M01"); + + let mc = month_to_month_code(13).unwrap(); + assert_eq!(mc.as_str(), "M13"); + + let _ = month_to_month_code(0).unwrap_err(); + let _ = month_to_month_code(14).unwrap_err(); + } #[test] fn day_overflow_test() { @@ -350,7 +423,7 @@ mod tests { let bad_fields = PartialDate { year: Some(1976), month: Some(11), - month_code: Some(tinystr!(4, "M12")), + month_code: Some(MonthCode(tinystr!(4, "M12"))), day: Some(18), ..Default::default() }; diff --git a/src/builtins/core/date.rs b/src/builtins/core/date.rs index 94d121843..9d09696b9 100644 --- a/src/builtins/core/date.rs +++ b/src/builtins/core/date.rs @@ -12,13 +12,13 @@ use crate::{ parsers::{parse_date_time, IxdtfStringBuilder}, primitive::FiniteF64, provider::NeverProvider, - TemporalError, TemporalResult, TemporalUnwrap, TimeZone, + MonthCode, TemporalError, TemporalResult, TemporalUnwrap, TimeZone, }; use alloc::{format, string::String}; use core::{cmp::Ordering, str::FromStr}; use super::{ - calendar::{ascii_four_to_integer, month_to_month_code}, + calendar::month_to_month_code, duration::{normalized::NormalizedDurationRecord, TimeDuration}, PlainMonthDay, PlainYearMonth, }; @@ -35,7 +35,7 @@ pub struct PartialDate { // A potentially set `month` field. pub month: Option, // A potentially set `month_code` field. - pub month_code: Option>, + pub month_code: Option, // A potentially set `day` field. pub day: Option, // A potentially set `era` field. @@ -105,7 +105,7 @@ macro_rules! impl_with_fallback_method { let (month, month_code) = match (self.month, self.month_code) { (Some(month), Some(mc)) => (Some(month), Some(mc)), (Some(month), None) => (Some(month), Some(month_to_month_code(month)?)), - (None, Some(mc)) => (Some(ascii_four_to_integer(mc)?).map(Into::into), Some(mc)), + (None, Some(mc)) => (Some(mc.to_month_integer()).map(Into::into), Some(mc)), (None, None) => ( Some(fallback.month()?).map(Into::into), Some(fallback.month_code()?), @@ -497,7 +497,7 @@ impl PlainDate { } /// Returns the calendar month code value. - pub fn month_code(&self) -> TemporalResult> { + pub fn month_code(&self) -> TemporalResult { self.calendar.month_code(&self.iso) } @@ -849,7 +849,7 @@ mod tests { assert_eq!(with_year.month().unwrap(), 11); assert_eq!( with_year.month_code().unwrap(), - TinyAsciiStr::<4>::from_str("M11").unwrap() + MonthCode::from_str("M11").unwrap() ); assert_eq!(with_year.day().unwrap(), 18); @@ -863,13 +863,13 @@ mod tests { assert_eq!(with_month.month().unwrap(), 5); assert_eq!( with_month.month_code().unwrap(), - TinyAsciiStr::<4>::from_str("M05").unwrap() + MonthCode::from_str("M05").unwrap() ); assert_eq!(with_month.day().unwrap(), 18); // Month Code let partial = PartialDate { - month_code: Some(tinystr!(4, "M05")), + month_code: Some(MonthCode(tinystr!(4, "M05"))), ..Default::default() }; let with_mc = base.with(partial, None).unwrap(); @@ -877,7 +877,7 @@ mod tests { assert_eq!(with_mc.month().unwrap(), 5); assert_eq!( with_mc.month_code().unwrap(), - TinyAsciiStr::<4>::from_str("M05").unwrap() + MonthCode::from_str("M05").unwrap() ); assert_eq!(with_mc.day().unwrap(), 18); @@ -891,7 +891,7 @@ mod tests { assert_eq!(with_day.month().unwrap(), 11); assert_eq!( with_day.month_code().unwrap(), - TinyAsciiStr::<4>::from_str("M11").unwrap() + MonthCode::from_str("M11").unwrap() ); assert_eq!(with_day.day().unwrap(), 17); } diff --git a/src/builtins/core/datetime.rs b/src/builtins/core/datetime.rs index 2d23b7a57..c2487980e 100644 --- a/src/builtins/core/datetime.rs +++ b/src/builtins/core/datetime.rs @@ -13,7 +13,7 @@ use crate::{ }, parsers::{parse_date_time, IxdtfStringBuilder}, provider::NeverProvider, - temporal_assert, TemporalError, TemporalResult, TemporalUnwrap, TimeZone, + temporal_assert, MonthCode, TemporalError, TemporalResult, TemporalUnwrap, TimeZone, }; use alloc::string::String; use core::{cmp::Ordering, str::FromStr}; @@ -503,7 +503,7 @@ impl PlainDateTime { } /// Returns the calendar month code value. - pub fn month_code(&self) -> TemporalResult> { + pub fn month_code(&self) -> TemporalResult { self.calendar.month_code(&self.iso.date) } @@ -711,7 +711,7 @@ mod tests { }, parsers::Precision, primitive::FiniteF64, - TemporalResult, + MonthCode, TemporalResult, }; fn assert_datetime( @@ -720,7 +720,7 @@ mod tests { ) { assert_eq!(dt.year().unwrap(), fields.0); assert_eq!(dt.month().unwrap(), fields.1); - assert_eq!(dt.month_code().unwrap(), fields.2); + assert_eq!(dt.month_code().unwrap(), MonthCode(fields.2)); assert_eq!(dt.day().unwrap(), fields.3); assert_eq!(dt.hour(), fields.4); assert_eq!(dt.minute(), fields.5); @@ -813,7 +813,7 @@ mod tests { // Test monthCode let partial = PartialDateTime { date: PartialDate { - month_code: Some(tinystr!(4, "M05")), + month_code: Some(MonthCode(tinystr!(4, "M05"))), ..Default::default() }, time: PartialTime::default(), diff --git a/src/builtins/core/month_day.rs b/src/builtins/core/month_day.rs index 25dce5b3d..226f046a4 100644 --- a/src/builtins/core/month_day.rs +++ b/src/builtins/core/month_day.rs @@ -3,13 +3,11 @@ use alloc::string::String; use core::str::FromStr; -use tinystr::TinyAsciiStr; - use crate::{ iso::IsoDate, options::{ArithmeticOverflow, DisplayCalendar}, parsers::{FormattableCalendar, FormattableDate, FormattableMonthDay}, - Calendar, TemporalError, TemporalResult, TemporalUnwrap, + Calendar, MonthCode, TemporalError, TemporalResult, TemporalUnwrap, }; use super::{PartialDate, PlainDate}; @@ -96,7 +94,7 @@ impl PlainMonthDay { /// Returns the `monthCode` value of `MonthDay`. #[inline] - pub fn month_code(&self) -> TemporalResult> { + pub fn month_code(&self) -> TemporalResult { self.calendar.month_code(&self.iso) } diff --git a/src/builtins/core/year_month.rs b/src/builtins/core/year_month.rs index 054e4be97..4124c3592 100644 --- a/src/builtins/core/year_month.rs +++ b/src/builtins/core/year_month.rs @@ -10,7 +10,7 @@ use crate::{ options::{ArithmeticOverflow, DifferenceOperation, DifferenceSettings, DisplayCalendar}, parsers::{FormattableCalendar, FormattableDate, FormattableYearMonth}, utils::pad_iso_year, - Calendar, TemporalError, TemporalResult, TemporalUnwrap, + Calendar, MonthCode, TemporalError, TemporalResult, TemporalUnwrap, }; use super::{Duration, PartialDate, PlainDate}; @@ -127,7 +127,7 @@ impl PlainYearMonth { } /// Returns the calendar month code of the current `PlainYearMonth` - pub fn month_code(&self) -> TemporalResult> { + pub fn month_code(&self) -> TemporalResult { self.calendar().month_code(&self.iso) } diff --git a/src/builtins/core/zoneddatetime.rs b/src/builtins/core/zoneddatetime.rs index d30ffbf6c..c08a6a888 100644 --- a/src/builtins/core/zoneddatetime.rs +++ b/src/builtins/core/zoneddatetime.rs @@ -27,7 +27,7 @@ use crate::{ rounding::{IncrementRounder, Round}, temporal_assert, time::EpochNanoseconds, - Sign, TemporalError, TemporalResult, TemporalUnwrap, + MonthCode, Sign, TemporalError, TemporalResult, TemporalUnwrap, }; /// A struct representing a partial `ZonedDateTime`. @@ -497,7 +497,7 @@ impl ZonedDateTime { pub fn month_code_with_provider( &self, provider: &impl TimeZoneProvider, - ) -> TemporalResult> { + ) -> TemporalResult { let iso = self.tz.get_iso_datetime_for(&self.instant, provider)?; let dt = PlainDateTime::new_unchecked(iso, self.calendar.clone()); self.calendar.month_code(&dt.iso.date) @@ -1101,7 +1101,7 @@ mod tests { partial::{PartialDate, PartialTime, PartialZonedDateTime}, primitive::FiniteF64, tzdb::FsTzdbProvider, - Calendar, TimeZone, + Calendar, MonthCode, TimeZone, }; use core::str::FromStr; use tinystr::tinystr; @@ -1160,7 +1160,7 @@ mod tests { let partial = PartialZonedDateTime { date: PartialDate { year: Some(1970), - month_code: Some(tinystr!(4, "M01")), + month_code: Some(MonthCode(tinystr!(4, "M01"))), day: Some(1), ..Default::default() }, diff --git a/src/lib.rs b/src/lib.rs index 153fe910e..03d6c2636 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -96,8 +96,10 @@ pub mod time { } pub use crate::builtins::{ - calendar::Calendar, core::timezone::TimeZone, DateDuration, Duration, Instant, Now, PlainDate, - PlainDateTime, PlainMonthDay, PlainTime, PlainYearMonth, TimeDuration, ZonedDateTime, + calendar::{Calendar, MonthCode}, + core::timezone::TimeZone, + DateDuration, Duration, Instant, Now, PlainDate, PlainDateTime, PlainMonthDay, PlainTime, + PlainYearMonth, TimeDuration, ZonedDateTime, }; /// A library specific trait for unwrapping assertions. From d2dc289c864a9314156e3c72d32889b7a847af74 Mon Sep 17 00:00:00 2001 From: Kevin Ness <46825870+nekevss@users.noreply.github.com> Date: Sun, 23 Feb 2025 00:21:45 -0600 Subject: [PATCH 2/4] Adjust temporal_capi accordingly --- temporal_capi/src/calendar.rs | 2 +- temporal_capi/src/plain_date.rs | 6 ++++-- temporal_capi/src/plain_date_time.rs | 2 +- temporal_capi/src/plain_month_day.rs | 2 +- temporal_capi/src/plain_year_month.rs | 2 +- 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/temporal_capi/src/calendar.rs b/temporal_capi/src/calendar.rs index 76da3dad8..2fc825f63 100644 --- a/temporal_capi/src/calendar.rs +++ b/temporal_capi/src/calendar.rs @@ -150,7 +150,7 @@ pub mod ffi { .month_code(&date.into()) .map_err(Into::::into)?; // throw away the error, this should always succeed - let _ = write.write_str(&code); + let _ = write.write_str(code.as_str()); Ok(()) } pub fn day(&self, date: IsoDate) -> Result { diff --git a/temporal_capi/src/plain_date.rs b/temporal_capi/src/plain_date.rs index 154a1d634..b95f09673 100644 --- a/temporal_capi/src/plain_date.rs +++ b/temporal_capi/src/plain_date.rs @@ -1,3 +1,5 @@ +use temporal_rs::MonthCode; + use crate::error::ffi::TemporalError; #[diplomat::bridge] @@ -166,7 +168,7 @@ pub mod ffi { pub fn month_code(&self, write: &mut DiplomatWrite) -> Result<(), TemporalError> { let code = self.0.month_code().map_err(Into::::into)?; // throw away the error, this should always succeed - let _ = write.write_str(&code); + let _ = write.write_str(code.as_str()); Ok(()) } pub fn day(&self) -> Result { @@ -258,7 +260,7 @@ impl TryFrom> for temporal_rs::partial::PartialDate { None } else { Some( - TinyAsciiStr::try_from_utf8(other.month_code.into()) + MonthCode::try_from_utf8(other.month_code.into()) .map_err(|_| TemporalError::syntax())?, ) }; diff --git a/temporal_capi/src/plain_date_time.rs b/temporal_capi/src/plain_date_time.rs index 8d604ccb6..dfd3593ce 100644 --- a/temporal_capi/src/plain_date_time.rs +++ b/temporal_capi/src/plain_date_time.rs @@ -156,7 +156,7 @@ pub mod ffi { pub fn month_code(&self, write: &mut DiplomatWrite) -> Result<(), TemporalError> { let code = self.0.month_code().map_err(Into::::into)?; // throw away the error, this should always succeed - let _ = write.write_str(&code); + let _ = write.write_str(code.as_str()); Ok(()) } pub fn day(&self) -> Result { diff --git a/temporal_capi/src/plain_month_day.rs b/temporal_capi/src/plain_month_day.rs index f88a33216..4c0cc5073 100644 --- a/temporal_capi/src/plain_month_day.rs +++ b/temporal_capi/src/plain_month_day.rs @@ -61,7 +61,7 @@ pub mod ffi { pub fn month_code(&self, write: &mut DiplomatWrite) -> Result<(), TemporalError> { let code = self.0.month_code().map_err(Into::::into)?; // throw away the error, this should always succeed - let _ = write.write_str(&code); + let _ = write.write_str(code.as_str()); Ok(()) } diff --git a/temporal_capi/src/plain_year_month.rs b/temporal_capi/src/plain_year_month.rs index d23187226..cdf481407 100644 --- a/temporal_capi/src/plain_year_month.rs +++ b/temporal_capi/src/plain_year_month.rs @@ -68,7 +68,7 @@ pub mod ffi { pub fn month_code(&self, write: &mut DiplomatWrite) -> Result<(), TemporalError> { let code = self.0.month_code().map_err(Into::::into)?; // throw away the error, this should always succeed - let _ = write.write_str(&code); + let _ = write.write_str(code.as_str()); Ok(()) } From aa275d81fd31daa1c5866afbb8734b54605ddcce Mon Sep 17 00:00:00 2001 From: Kevin Ness <46825870+nekevss@users.noreply.github.com> Date: Sun, 23 Feb 2025 09:08:21 -0600 Subject: [PATCH 3/4] Edit comments for correctness --- src/builtins/core/calendar/types.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/builtins/core/calendar/types.rs b/src/builtins/core/calendar/types.rs index e053dfb58..bcf3fb86c 100644 --- a/src/builtins/core/calendar/types.rs +++ b/src/builtins/core/calendar/types.rs @@ -303,11 +303,11 @@ fn are_month_and_month_code_resolvable(month: u8, mc: &MonthCode) -> TemporalRes } // Potentially greedy. Need to verify for all calendars that -// the code interger aligns with the month integer, which +// the month code integer aligns with the month integer, which // may require calendar info pub(crate) fn ascii_four_to_integer(mc: TinyAsciiStr<4>) -> u8 { let bytes = mc.all_bytes(); - // Invariant: first and second character are ascii digits. + // Invariant: second and third character (index 1 and 2) are ascii digits. debug_assert!(bytes[1].is_ascii_digit()); debug_assert!(bytes[2].is_ascii_digit()); let first = ascii_digit_to_int(bytes[1]) * 10; From 71ab8be869a953f1bbf0410a813dc683938e0eff Mon Sep 17 00:00:00 2001 From: Kevin Ness <46825870+nekevss@users.noreply.github.com> Date: Sun, 23 Feb 2025 09:41:38 -0600 Subject: [PATCH 4/4] Small refactor after merge --- src/builtins/core/calendar.rs | 8 ++-- src/builtins/core/calendar/types.rs | 62 ++++++++++++----------------- src/iso.rs | 1 + src/utils.rs | 2 + 4 files changed, 33 insertions(+), 40 deletions(-) diff --git a/src/builtins/core/calendar.rs b/src/builtins/core/calendar.rs index c93ce3f19..d2ba85fae 100644 --- a/src/builtins/core/calendar.rs +++ b/src/builtins/core/calendar.rs @@ -9,7 +9,7 @@ use alloc::string::String; use alloc::vec::Vec; use core::str::FromStr; use icu_calendar::types::{Era as IcuEra, MonthCode as IcuMonthCode, MonthInfo, YearInfo}; -use types::ResolveType; +use types::ResolutionType; use crate::{ builtins::core::{ @@ -228,7 +228,7 @@ impl Calendar { overflow: ArithmeticOverflow, ) -> TemporalResult { let resolved_fields = - ResolvedCalendarFields::try_from_partial(partial, overflow, ResolveType::Date)?; + ResolvedCalendarFields::try_from_partial(partial, overflow, ResolutionType::Date)?; if self.is_iso() { // Resolve month and monthCode; @@ -267,7 +267,7 @@ impl Calendar { overflow: ArithmeticOverflow, ) -> TemporalResult { let resolved_fields = - ResolvedCalendarFields::try_from_partial(partial, overflow, ResolveType::MonthDay)?; + ResolvedCalendarFields::try_from_partial(partial, overflow, ResolutionType::MonthDay)?; if self.is_iso() { return PlainMonthDay::new_with_overflow( resolved_fields.month_code.to_month_integer(), @@ -290,7 +290,7 @@ impl Calendar { overflow: ArithmeticOverflow, ) -> TemporalResult { let resolved_fields = - ResolvedCalendarFields::try_from_partial(partial, overflow, ResolveType::YearMonth)?; + ResolvedCalendarFields::try_from_partial(partial, overflow, ResolutionType::YearMonth)?; if self.is_iso() { return PlainYearMonth::new_with_overflow( resolved_fields.era_year.year, diff --git a/src/builtins/core/calendar/types.rs b/src/builtins/core/calendar/types.rs index af9fd879b..4326067c9 100644 --- a/src/builtins/core/calendar/types.rs +++ b/src/builtins/core/calendar/types.rs @@ -12,7 +12,7 @@ use crate::{TemporalError, TemporalResult}; use crate::builtins::core::{calendar::Calendar, PartialDate}; #[derive(Debug, Clone, Copy, PartialEq)] -pub enum ResolveType { +pub enum ResolutionType { Date, YearMonth, MonthDay, @@ -32,25 +32,13 @@ impl ResolvedCalendarFields { pub fn try_from_partial( partial_date: &PartialDate, overflow: ArithmeticOverflow, - resolve_type: ResolveType, + resolve_type: ResolutionType, ) -> TemporalResult { - let era_year = EraYear::try_from_partial_values_and_calendar( - partial_date.year, - partial_date.era, - partial_date.era_year, - &partial_date.calendar, - )?; + let era_year = EraYear::try_from_partial_date(partial_date)?; if partial_date.calendar.is_iso() { let month_code = resolve_iso_month(partial_date.month_code, partial_date.month, overflow)?; - let day = if resolve_type != ResolveType::YearMonth { - partial_date - .day - .ok_or(TemporalError::r#type().with_message("Required day field is empty."))? - } else { - partial_date.day.unwrap_or(1) - }; - + let day = resolve_day(partial_date.day, resolve_type == ResolutionType::YearMonth)?; let day = if overflow == ArithmeticOverflow::Constrain { constrain_iso_day(era_year.year, month_code.to_month_integer(), day) } else { @@ -69,13 +57,8 @@ impl ResolvedCalendarFields { } let month_code = MonthCode::try_from_partial_date(partial_date)?; - let day = if resolve_type != ResolveType::YearMonth { - partial_date - .day - .ok_or(TemporalError::r#type().with_message("Required day field is empty."))? - } else { - partial_date.day.unwrap_or(1) - }; + let day = resolve_day(partial_date.day, resolve_type == ResolutionType::YearMonth)?; + // TODO: Constrain day to calendar range for month? Ok(Self { era_year, @@ -85,6 +68,14 @@ impl ResolvedCalendarFields { } } +fn resolve_day(day: Option, is_year_month: bool) -> TemporalResult { + if is_year_month { + Ok(day.unwrap_or(1)) + } else { + day.ok_or(TemporalError::r#type().with_message("Required day field is empty.")) + } +} + #[derive(Debug)] pub struct Era(pub(crate) TinyAsciiStr<16>); @@ -95,15 +86,10 @@ pub struct EraYear { } impl EraYear { - pub(crate) fn try_from_partial_values_and_calendar( - year: Option, - era: Option>, - era_year: Option, - calendar: &Calendar, - ) -> TemporalResult { - match (year, era, era_year) { + pub(crate) fn try_from_partial_date(partial: &PartialDate) -> TemporalResult { + match (partial.year, partial.era, partial.era_year) { (Some(year), None, None) => { - let Some(era) = calendar.get_calendar_default_era() else { + let Some(era) = partial.calendar.get_calendar_default_era() else { return Err(TemporalError::r#type() .with_message("Era is required for the provided calendar.")); }; @@ -113,7 +99,7 @@ impl EraYear { }) } (None, Some(era), Some(era_year)) => { - let Some(era_info) = calendar.get_era_info(&era) else { + let Some(era_info) = partial.calendar.get_era_info(&era) else { return Err(TemporalError::range().with_message("Invalid era provided.")); }; if !era_info.range.contains(&era_year) { @@ -299,6 +285,7 @@ impl core::str::FromStr for MonthCode { } // NOTE: This is a greedy function, should handle differently for all calendars. +#[inline] pub(crate) fn month_to_month_code(month: u8) -> TemporalResult { if !(1..=13).contains(&month) { return Err(TemporalError::range().with_message("Month not in a valid range.")); @@ -310,6 +297,7 @@ pub(crate) fn month_to_month_code(month: u8) -> TemporalResult { Ok(MonthCode(tinystr)) } +#[inline] fn are_month_and_month_code_resolvable(month: u8, mc: &MonthCode) -> TemporalResult<()> { if month != mc.to_month_integer() { return Err(TemporalError::range() @@ -321,6 +309,7 @@ fn are_month_and_month_code_resolvable(month: u8, mc: &MonthCode) -> TemporalRes // Potentially greedy. Need to verify for all calendars that // the month code integer aligns with the month integer, which // may require calendar info +#[inline] pub(crate) fn ascii_four_to_integer(mc: TinyAsciiStr<4>) -> u8 { let bytes = mc.all_bytes(); // Invariant: second and third character (index 1 and 2) are ascii digits. @@ -330,6 +319,7 @@ pub(crate) fn ascii_four_to_integer(mc: TinyAsciiStr<4>) -> u8 { first + ascii_digit_to_int(bytes[2]) } +#[inline] const fn ascii_digit_to_int(ascii_digit: u8) -> u8 { ascii_digit - 48 } @@ -376,7 +366,7 @@ mod tests { use crate::{ builtins::{ - calendar::types::ResolveType, + calendar::types::ResolutionType, core::{calendar::Calendar, PartialDate}, }, options::ArithmeticOverflow, @@ -450,7 +440,7 @@ mod tests { let err = ResolvedCalendarFields::try_from_partial( &bad_fields, ArithmeticOverflow::Reject, - ResolveType::Date, + ResolutionType::Date, ); assert!(err.is_err()); } @@ -466,7 +456,7 @@ mod tests { let err = ResolvedCalendarFields::try_from_partial( &bad_fields, ArithmeticOverflow::Reject, - ResolveType::Date, + ResolutionType::Date, ); assert!(err.is_err()); @@ -474,7 +464,7 @@ mod tests { let err = ResolvedCalendarFields::try_from_partial( &bad_fields, ArithmeticOverflow::Reject, - ResolveType::Date, + ResolutionType::Date, ); assert!(err.is_err()); } diff --git a/src/iso.rs b/src/iso.rs index ae18a777d..df7158667 100644 --- a/src/iso.rs +++ b/src/iso.rs @@ -994,6 +994,7 @@ fn balance_iso_year_month(year: i32, month: i32) -> (i32, u8) { (y, m as u8) } +/// Note: month is 1 based. #[inline] pub(crate) fn constrain_iso_day(year: i32, month: u8, day: u8) -> u8 { let days_in_month = utils::iso_days_in_month(year, month.into()); diff --git a/src/utils.rs b/src/utils.rs index 6a0856038..69cd3941d 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -148,6 +148,8 @@ pub(crate) fn epoch_seconds_to_day_of_month(t: i64) -> u16 { // NOTE: below was the iso methods in temporal::calendar -> Need to be reassessed. /// 12.2.31 `ISODaysInMonth ( year, month )` +/// +/// NOTE: month is 1 based pub(crate) fn iso_days_in_month(year: i32, month: i32) -> u8 { match month { 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,