diff --git a/components/datetime/src/fields/length.rs b/components/datetime/src/fields/length.rs index 3a9ca12e875..d2412e7c225 100644 --- a/components/datetime/src/fields/length.rs +++ b/components/datetime/src/fields/length.rs @@ -50,12 +50,8 @@ pub enum FieldLength { /// [LDML documentation in UTS 35](https://unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns) /// for more details. Six, - /// A fixed size format for numeric-only fields that is at most 127 digits. - Fixed(u8), /// FieldLength::One (numeric), but overridden with a different numbering system NumericOverride(FieldNumericOverrides), - /// A time zone field with non-standard rules. - TimeZoneFallbackOverride(TimeZoneFallbackOverride), } /// First index used for numeric overrides in compact FieldLength representation @@ -65,12 +61,6 @@ pub enum FieldLength { const FIRST_NUMERIC_OVERRIDE: u8 = 17; /// Last index used for numeric overrides const LAST_NUMERIC_OVERRIDE: u8 = 31; -/// First index used for time zone fallback overrides -const FIRST_TIME_ZONE_FALLBACK_OVERRIDE: u8 = 32; -/// Last index used for time zone fallback overrides -const LAST_TIME_ZONE_FALLBACK_OVERRIDE: u8 = 40; -/// First index used for fixed size formats in compact FieldLength representation -const FIRST_FIXED: u8 = 128; impl FieldLength { #[inline] @@ -85,10 +75,6 @@ impl FieldLength { FieldLength::NumericOverride(o) => FIRST_NUMERIC_OVERRIDE .saturating_add(*o as u8) .min(LAST_NUMERIC_OVERRIDE), - FieldLength::TimeZoneFallbackOverride(o) => FIRST_TIME_ZONE_FALLBACK_OVERRIDE - .saturating_add(*o as u8) - .min(LAST_TIME_ZONE_FALLBACK_OVERRIDE), - FieldLength::Fixed(p) => FIRST_FIXED.saturating_add(*p), /* truncate to at most 127 digits to avoid overflow */ } } @@ -104,14 +90,6 @@ impl FieldLength { idx if (FIRST_NUMERIC_OVERRIDE..=LAST_NUMERIC_OVERRIDE).contains(&idx) => { Self::NumericOverride((idx - FIRST_NUMERIC_OVERRIDE).try_into()?) } - idx if (FIRST_TIME_ZONE_FALLBACK_OVERRIDE..=LAST_TIME_ZONE_FALLBACK_OVERRIDE) - .contains(&idx) => - { - Self::TimeZoneFallbackOverride( - (idx - FIRST_TIME_ZONE_FALLBACK_OVERRIDE).try_into()?, - ) - } - idx if idx >= FIRST_FIXED => Self::Fixed(idx - FIRST_FIXED), _ => return Err(LengthError::InvalidLength), }) } @@ -127,10 +105,6 @@ impl FieldLength { FieldLength::Narrow => 5, FieldLength::Six => 6, FieldLength::NumericOverride(o) => FIRST_NUMERIC_OVERRIDE as usize + o as usize, - FieldLength::TimeZoneFallbackOverride(o) => { - FIRST_TIME_ZONE_FALLBACK_OVERRIDE as usize + o as usize - } - FieldLength::Fixed(p) => p as usize, } } @@ -244,44 +218,3 @@ impl fmt::Display for FieldNumericOverrides { self.as_str().fmt(f) } } - -/// Time zone fallback overrides to support configurations not found -/// in the CLDR datetime field symbol table. -#[derive(Debug, Eq, PartialEq, Clone, Copy, Ord, PartialOrd)] -#[cfg_attr( - feature = "datagen", - derive(serde::Serialize, databake::Bake), - databake(path = icu_datetime::fields), -)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize))] -#[non_exhaustive] -pub enum TimeZoneFallbackOverride { - /// The short form of this time zone field, - /// but fall back directly to GMT. - ShortOrGmt = 0, -} - -impl TryFrom for TimeZoneFallbackOverride { - type Error = LengthError; - fn try_from(other: u8) -> Result { - Ok(match other { - 0 => Self::ShortOrGmt, - _ => return Err(LengthError::InvalidLength), - }) - } -} - -// impl TimeZoneFallbackOverride { -// /// Convert this to the corresponding string code -// pub fn as_str(self) -> &'static str { -// match self { -// Self::ShortOrGmt => "ShortOrGmt", -// } -// } -// } - -// impl fmt::Display for TimeZoneFallbackOverride { -// fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { -// self.as_str().fmt(f) -// } -// } diff --git a/components/datetime/src/fields/mod.rs b/components/datetime/src/fields/mod.rs index bed53fe6cd3..5d529f0e589 100644 --- a/components/datetime/src/fields/mod.rs +++ b/components/datetime/src/fields/mod.rs @@ -10,7 +10,7 @@ mod length; pub(crate) mod symbols; use displaydoc::Display; -pub use length::{FieldLength, FieldNumericOverrides, LengthError, TimeZoneFallbackOverride}; +pub use length::{FieldLength, FieldNumericOverrides, LengthError}; pub use symbols::*; use core::{ @@ -69,6 +69,7 @@ impl Field { FieldSymbol::Minute => TextOrNumeric::Numeric, FieldSymbol::Second(second) => second.get_length_type(self.length), FieldSymbol::TimeZone(zone) => zone.get_length_type(self.length), + FieldSymbol::DecimalSecond(_) => TextOrNumeric::Numeric, } } } @@ -94,14 +95,8 @@ impl From<(FieldSymbol, FieldLength)> for Field { impl TryFrom<(FieldSymbol, usize)> for Field { type Error = Error; fn try_from(input: (FieldSymbol, usize)) -> Result { - let length = if input.0 != FieldSymbol::Second(crate::fields::Second::FractionalSecond) { - FieldLength::from_idx(input.1 as u8).map_err(|_| Self::Error::InvalidLength(input.0))? - } else if input.1 <= 127 { - FieldLength::from_idx(128 + input.1 as u8) - .map_err(|_| Self::Error::InvalidLength(input.0))? - } else { - return Err(Self::Error::InvalidLength(input.0)); - }; + let length = FieldLength::from_idx(input.1 as u8) + .map_err(|_| Self::Error::InvalidLength(input.0))?; Ok(Self { symbol: input.0, length, diff --git a/components/datetime/src/fields/symbols.rs b/components/datetime/src/fields/symbols.rs index e08dc8b03d2..a28c0f01a76 100644 --- a/components/datetime/src/fields/symbols.rs +++ b/components/datetime/src/fields/symbols.rs @@ -4,6 +4,8 @@ #[cfg(any(feature = "datagen", feature = "experimental"))] use crate::fields::FieldLength; +#[cfg(any(feature = "datagen", feature = "experimental"))] +use crate::neo_skeleton::FractionalSecondDigits; use core::{cmp::Ordering, convert::TryFrom}; use displaydoc::Display; use icu_provider::prelude::*; @@ -55,10 +57,13 @@ pub enum FieldSymbol { Hour(Hour), /// Minute number within an hour. Minute, - /// Seconds number within a minute, including fractional seconds, or milliseconds within a day. + /// Seconds integer within a minute or milliseconds within a day. Second(Second), /// Time zone as a name, a zone ID, or a ISO 8601 numerical offset. TimeZone(TimeZone), + /// Seconds with fractional digits. If seconds are an integer, + /// [`FieldSymbol::Second`] is used. + DecimalSecond(DecimalSecond), } impl FieldSymbol { @@ -110,6 +115,7 @@ impl FieldSymbol { FieldSymbol::Minute => (8, 0), FieldSymbol::Second(second) => (9, second.idx()), FieldSymbol::TimeZone(tz) => (10, tz.idx()), + FieldSymbol::DecimalSecond(second) => (11, second.idx()), }; let result = high << 4; result | low @@ -134,13 +140,14 @@ impl FieldSymbol { 8 if low == 0 => Self::Minute, 9 => Self::Second(Second::from_idx(low)?), 10 => Self::TimeZone(TimeZone::from_idx(low)?), + 11 => Self::DecimalSecond(DecimalSecond::from_idx(low)?), _ => return Err(SymbolError::InvalidIndex(idx)), }) } /// Returns the index associated with this FieldSymbol. #[cfg(any(feature = "datagen", feature = "experimental"))] // only referenced in experimental code - fn discriminant_idx(&self) -> u8 { + fn idx_for_skeleton(&self) -> u8 { match self { FieldSymbol::Era => 0, FieldSymbol::Year(_) => 1, @@ -151,16 +158,37 @@ impl FieldSymbol { FieldSymbol::DayPeriod(_) => 6, FieldSymbol::Hour(_) => 7, FieldSymbol::Minute => 8, - FieldSymbol::Second(_) => 9, + FieldSymbol::Second(_) | FieldSymbol::DecimalSecond(_) => 9, FieldSymbol::TimeZone(_) => 10, } } /// Compares this enum with other solely based on the enum variant, /// ignoring the enum's data. + /// + /// Second and DecimalSecond are considered equal. #[cfg(any(feature = "datagen", feature = "experimental"))] // only referenced in experimental code - pub(crate) fn discriminant_cmp(&self, other: &Self) -> Ordering { - self.discriminant_idx().cmp(&other.discriminant_idx()) + pub(crate) fn skeleton_cmp(&self, other: &Self) -> Ordering { + self.idx_for_skeleton().cmp(&other.idx_for_skeleton()) + } + + #[cfg(any(feature = "datagen", feature = "experimental"))] + pub(crate) fn from_fractional_second_digits( + fractional_second_digits: FractionalSecondDigits, + ) -> Self { + use FractionalSecondDigits::*; + match fractional_second_digits { + F0 => FieldSymbol::Second(Second::Second), + F1 => FieldSymbol::DecimalSecond(DecimalSecond::SecondF1), + F2 => FieldSymbol::DecimalSecond(DecimalSecond::SecondF2), + F3 => FieldSymbol::DecimalSecond(DecimalSecond::SecondF3), + F4 => FieldSymbol::DecimalSecond(DecimalSecond::SecondF4), + F5 => FieldSymbol::DecimalSecond(DecimalSecond::SecondF5), + F6 => FieldSymbol::DecimalSecond(DecimalSecond::SecondF6), + F7 => FieldSymbol::DecimalSecond(DecimalSecond::SecondF7), + F8 => FieldSymbol::DecimalSecond(DecimalSecond::SecondF8), + F9 => FieldSymbol::DecimalSecond(DecimalSecond::SecondF9), + } } /// UTS 35 defines several 1 and 2 symbols to be the same as 3 symbols (abbreviated). @@ -273,15 +301,23 @@ impl FieldSymbol { Self::Hour(Hour::H24) => 21, Self::Minute => 22, Self::Second(Second::Second) => 23, - Self::Second(Second::FractionalSecond) => 24, - Self::Second(Second::Millisecond) => 25, - Self::TimeZone(TimeZone::LowerZ) => 26, - Self::TimeZone(TimeZone::UpperZ) => 27, - Self::TimeZone(TimeZone::UpperO) => 28, - Self::TimeZone(TimeZone::LowerV) => 29, - Self::TimeZone(TimeZone::UpperV) => 30, - Self::TimeZone(TimeZone::LowerX) => 31, - Self::TimeZone(TimeZone::UpperX) => 32, + Self::Second(Second::Millisecond) => 24, + Self::DecimalSecond(DecimalSecond::SecondF1) => 31, + Self::DecimalSecond(DecimalSecond::SecondF2) => 32, + Self::DecimalSecond(DecimalSecond::SecondF3) => 33, + Self::DecimalSecond(DecimalSecond::SecondF4) => 34, + Self::DecimalSecond(DecimalSecond::SecondF5) => 35, + Self::DecimalSecond(DecimalSecond::SecondF6) => 36, + Self::DecimalSecond(DecimalSecond::SecondF7) => 37, + Self::DecimalSecond(DecimalSecond::SecondF8) => 38, + Self::DecimalSecond(DecimalSecond::SecondF9) => 39, + Self::TimeZone(TimeZone::LowerZ) => 100, + Self::TimeZone(TimeZone::UpperZ) => 101, + Self::TimeZone(TimeZone::UpperO) => 102, + Self::TimeZone(TimeZone::LowerV) => 103, + Self::TimeZone(TimeZone::UpperV) => 104, + Self::TimeZone(TimeZone::LowerX) => 105, + Self::TimeZone(TimeZone::UpperX) => 106, } } } @@ -314,6 +350,7 @@ impl TryFrom for FieldSymbol { }) .or_else(|_| Second::try_from(ch).map(Self::Second)) .or_else(|_| TimeZone::try_from(ch).map(Self::TimeZone)) + // Note: char-to-enum conversion for DecimalSecond is handled directly in the parser } } @@ -331,6 +368,8 @@ impl From for char { FieldSymbol::Minute => 'm', FieldSymbol::Second(second) => second.into(), FieldSymbol::TimeZone(time_zone) => time_zone.into(), + // Note: This is only used for representing the integer portion + FieldSymbol::DecimalSecond(_) => 's', } } } @@ -509,10 +548,6 @@ impl LengthType for Month { FieldLength::Wide => TextOrNumeric::Text, FieldLength::Narrow => TextOrNumeric::Text, FieldLength::Six => TextOrNumeric::Text, - FieldLength::Fixed(_) | FieldLength::TimeZoneFallbackOverride(_) => { - debug_assert!(false, "Invalid field length for month"); - TextOrNumeric::Text - } } } } @@ -553,19 +588,18 @@ field_type!( HourULE ); +// NOTE: 'S' FractionalSecond is represented via DecimalSecond, +// so it is not included in the Second enum. + field_type!( /// An enum for the possible symbols of a second field in a date pattern. Second; { /// Field symbol for second (numeric). 's' => Second = 0, - /// Field symbol for fractional second (numeric). - /// - /// Produces the number of digits specified by the field length. - 'S' => FractionalSecond = 1, /// Field symbol for milliseconds in day (numeric). /// /// This field behaves exactly like a composite of all time-related fields, not including the zone fields. - 'A' => Millisecond = 2, + 'A' => Millisecond = 1, }; Numeric; SecondULE @@ -697,3 +731,55 @@ impl LengthType for TimeZone { } } } + +/// A second field with fractional digits. +#[derive( + Debug, Eq, PartialEq, Ord, PartialOrd, Clone, Copy, yoke::Yokeable, zerofrom::ZeroFrom, +)] +#[cfg_attr( + feature = "datagen", + derive(serde::Serialize, databake::Bake), + databake(path = icu_datetime::fields), +)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize))] +#[allow(clippy::enum_variant_names)] +#[repr(u8)] +#[zerovec::make_ule(DecimalSecondULE)] +#[zerovec::derive(Debug)] +#[allow(clippy::exhaustive_enums)] // used in data struct +pub enum DecimalSecond { + /// A second with 1 fractional digit: "1.0" + SecondF1 = 1, + /// A second with 2 fractional digits: "1.00" + SecondF2 = 2, + /// A second with 3 fractional digits: "1.000" + SecondF3 = 3, + /// A second with 4 fractional digits: "1.0000" + SecondF4 = 4, + /// A second with 5 fractional digits: "1.00000" + SecondF5 = 5, + /// A second with 6 fractional digits: "1.000000" + SecondF6 = 6, + /// A second with 7 fractional digits: "1.0000000" + SecondF7 = 7, + /// A second with 8 fractional digits: "1.00000000" + SecondF8 = 8, + /// A second with 9 fractional digits: "1.000000000" + SecondF9 = 9, +} + +impl DecimalSecond { + #[inline] + pub(crate) fn idx(self) -> u8 { + self as u8 + } + #[inline] + pub(crate) fn from_idx(idx: u8) -> Result { + Self::new_from_u8(idx).ok_or(SymbolError::InvalidIndex(idx)) + } +} +impl From for FieldSymbol { + fn from(input: DecimalSecond) -> Self { + Self::DecimalSecond(input) + } +} diff --git a/components/datetime/src/format/datetime.rs b/components/datetime/src/format/datetime.rs index ddb4734af3d..70406682d2c 100644 --- a/components/datetime/src/format/datetime.rs +++ b/components/datetime/src/format/datetime.rs @@ -29,7 +29,6 @@ use crate::time_zone::{ use super::FormattingOptions; use core::fmt::{self, Write}; -use core::iter::Peekable; use fixed_decimal::FixedDecimal; use icu_calendar::types::{ Era, {DayOfWeekInMonth, IsoWeekday, MonthCode}, @@ -170,9 +169,7 @@ where { if let Some(fdf) = fixed_decimal_format { match length { - FieldLength::One - | FieldLength::NumericOverride(_) - | FieldLength::TimeZoneFallbackOverride(_) => {} + FieldLength::One | FieldLength::NumericOverride(_) => {} FieldLength::TwoDigit => { num.pad_start(2); num.set_max_position(2); @@ -189,10 +186,6 @@ where FieldLength::Six => { num.pad_start(6); } - FieldLength::Fixed(p) => { - num.pad_start(p as i16); - num.set_max_position(p as i16); - } } fdf.format(&num).write_to(result)?; @@ -255,8 +248,7 @@ where ZS: ZoneSymbols<'data>, { let mut r = Ok(()); - let mut iter = pattern_items.peekable(); - while let Some(item) = iter.next() { + for item in pattern_items { match item { PatternItem::Literal(ch) => w.write_char(ch)?, PatternItem::Field(Field { @@ -275,7 +267,6 @@ where PatternItem::Field(field) => { r = r.and(try_write_field( field, - &mut iter, pattern_metadata, formatting_options, datetime, @@ -359,7 +350,6 @@ pub enum DateTimeWriteError { #[allow(clippy::too_many_arguments)] pub(crate) fn try_write_field<'data, W, DS, TS>( field: fields::Field, - iter: &mut Peekable>, pattern_metadata: PatternMetadata, formatting_options: FormattingOptions, datetime: &ExtractedDateTimeInput, @@ -386,11 +376,23 @@ where }) } + #[cfg_attr(not(feature = "experimental"), allow(unused_mut))] + let mut field_symbol = field.symbol; + #[cfg(feature = "experimental")] + if let Some(fractional_second_digits) = formatting_options.fractional_second_digits { + if matches!( + field_symbol, + FieldSymbol::Second(fields::Second::Second) | FieldSymbol::DecimalSecond(_) + ) { + field_symbol = FieldSymbol::from_fractional_second_digits(fractional_second_digits); + } + } + let mut field_length = field.length; if formatting_options.force_2_digit_month_day_week_hour && field_length == FieldLength::One && matches!( - field.symbol, + field_symbol, FieldSymbol::Month(_) | FieldSymbol::Day(_) | FieldSymbol::Week(_) @@ -400,7 +402,7 @@ where field_length = FieldLength::TwoDigit; } - Ok(match (field.symbol, field_length) { + Ok(match (field_symbol, field_length) { (FieldSymbol::Era, l) => match datetime.year() { None => { write_value_missing(w, field)?; @@ -696,62 +698,28 @@ where Err(DateTimeWriteError::MissingInputField("second")) } Some(second) => { - match match (iter.peek(), formatting_options.fractional_second_digits) { - ( - Some(&PatternItem::Field( - next_field @ Field { - symbol: FieldSymbol::Second(Second::FractionalSecond), - length: FieldLength::Fixed(p), - }, - )), - _, - ) => { - // Fractional second digits via field symbol - iter.next(); // Advance over nanosecond symbol - Some((-(p as i16), Some(next_field), datetime.nanosecond())) - } - (_, Some(p)) => { - // Fractional second digits via semantic option - Some((-(p as i16), None, datetime.nanosecond())) - } - _ => None, - } { - Some((_, maybe_next_field, None)) => { - // Request to format nanoseconds but we don't have nanoseconds - let seconds_result = - try_write_number(w, fdf, usize::from(second).into(), l)?; - if let Some(next_field) = maybe_next_field { - write_value_missing(w, next_field)?; - } - // Return the earlier error - seconds_result.and(Err(DateTimeWriteError::MissingInputField("nanosecond"))) - } - Some((position, maybe_next_field, Some(ns))) => { - // Formatting with fractional seconds - let mut s = FixedDecimal::from(usize::from(second)); - let _infallible = s.concatenate_end( - FixedDecimal::from(usize::from(ns)).multiplied_pow10(-9), - ); - debug_assert!(_infallible.is_ok()); - s.pad_end(position); - if maybe_next_field.is_none() { - // Truncate on semantic option but not "S" field - // TODO: Does this make sense? - s.trunc(position); - } - try_write_number(w, fdf, s, l)? - } - None => { - // Normal seconds formatting with no fractional second digits - try_write_number(w, fdf, usize::from(second).into(), l)? - } - } + // Normal seconds formatting with no fractional second digits + try_write_number(w, fdf, usize::from(second).into(), l)? } }, - (FieldSymbol::Second(Second::FractionalSecond), _) => { - // Fractional second not following second or with invalid length - write_value_missing(w, field)?; - Err(DateTimeWriteError::UnsupportedField(field)) + (FieldSymbol::DecimalSecond(decimal_second), l) => { + match (datetime.second(), datetime.nanosecond()) { + (None, _) | (_, None) => { + write_value_missing(w, field)?; + Err(DateTimeWriteError::MissingInputField("second")) + } + (Some(second), Some(ns)) => { + // Formatting with fractional seconds + let mut s = FixedDecimal::from(usize::from(second)); + let _infallible = + s.concatenate_end(FixedDecimal::from(usize::from(ns)).multiplied_pow10(-9)); + debug_assert!(_infallible.is_ok()); + let position = -(decimal_second as i16); + s.trunc(position); + s.pad_end(position); + try_write_number(w, fdf, s, l)? + } + } } (FieldSymbol::DayPeriod(period), l) => match datetime.hour() { None => { diff --git a/components/datetime/src/format/mod.rs b/components/datetime/src/format/mod.rs index b7e3897fcc3..9284f6d01be 100644 --- a/components/datetime/src/format/mod.rs +++ b/components/datetime/src/format/mod.rs @@ -19,6 +19,4 @@ pub(crate) struct FormattingOptions { pub(crate) force_2_digit_month_day_week_hour: bool, #[cfg(feature = "experimental")] pub(crate) fractional_second_digits: Option, - #[cfg(not(feature = "experimental"))] - pub(crate) fractional_second_digits: Option, } diff --git a/components/datetime/src/format/neo.rs b/components/datetime/src/format/neo.rs index 855c741d835..79564db5b44 100644 --- a/components/datetime/src/format/neo.rs +++ b/components/datetime/src/format/neo.rs @@ -394,7 +394,7 @@ size_test!( /// // Missing data is filled in on a best-effort basis, and an error is signaled. /// assert_try_writeable_parts_eq!( /// names.with_pattern(&pattern).format(&CustomTimeZone::new_empty()), -/// "It is: {E} {M} {d} {y} {G} at {h}:{m}:{s}{S} {a} {GMT+?}", +/// "It is: {E} {M} {d} {y} {G} at {h}:{m}:{s} {a} {GMT+?}", /// Err(DateTimeWriteError::MissingInputField("iso_weekday")), /// [ /// (7, 10, Part::ERROR), // {E} @@ -405,9 +405,8 @@ size_test!( /// (30, 33, Part::ERROR), // {h} /// (34, 37, Part::ERROR), // {m} /// (38, 41, Part::ERROR), // {s} -/// (41, 44, Part::ERROR), // {S} -/// (45, 48, Part::ERROR), // {a} -/// (49, 56, Part::ERROR), // {GMT+?} +/// (42, 45, Part::ERROR), // {a} +/// (46, 53, Part::ERROR), // {GMT+?} /// ] /// ); /// ``` @@ -2094,6 +2093,7 @@ impl RawDateTimeNames { FieldSymbol::Hour(_) => numeric_field = Some(field), FieldSymbol::Minute => numeric_field = Some(field), FieldSymbol::Second(_) => numeric_field = Some(field), + FieldSymbol::DecimalSecond(_) => numeric_field = Some(field), }; } diff --git a/components/datetime/src/format/zoned_datetime.rs b/components/datetime/src/format/zoned_datetime.rs index 1b961e25bf1..ae81bd10546 100644 --- a/components/datetime/src/format/zoned_datetime.rs +++ b/components/datetime/src/format/zoned_datetime.rs @@ -73,8 +73,7 @@ where TS: TimeSymbols, { let mut r = Ok(()); - let mut iter = pattern.items.iter().peekable(); - while let Some(item) = iter.next() { + for item in pattern.items.iter() { match item { PatternItem::Literal(ch) => w.write_char(ch)?, PatternItem::Field(Field { @@ -88,7 +87,6 @@ where PatternItem::Field(field) => { r = r.and(datetime::try_write_field( field, - &mut iter, pattern.metadata, Default::default(), datetime, diff --git a/components/datetime/src/options/components.rs b/components/datetime/src/options/components.rs index 029c202470f..0b3565e5412 100644 --- a/components/datetime/src/options/components.rs +++ b/components/datetime/src/options/components.rs @@ -84,6 +84,7 @@ use crate::{ fields::{self, Field, FieldLength, FieldSymbol}, + neo_skeleton::FractionalSecondDigits, pattern::{ runtime::{Pattern, PatternPlurals}, PatternItem, @@ -126,7 +127,7 @@ pub struct Bag { /// Include the second such as "3" or "03". pub second: Option, /// Specify the number of fractional second digits such as 1 (".3") or 3 (".003"). - pub fractional_second: Option, + pub fractional_second: Option, /// Include the time zone, such as "GMT+05:00". pub time_zone_name: Option, @@ -350,26 +351,25 @@ impl Bag { } if let Some(second) = self.second { + let symbol = match self.fractional_second { + None => FieldSymbol::Second(fields::Second::Second), + Some(fractional_second) => { + FieldSymbol::from_fractional_second_digits(fractional_second) + } + }; // s 8, 12 Numeric: minimum digits // ss 08, 12 Numeric: 2 digits, zero pad if needed fields.push(Field { - symbol: FieldSymbol::Second(fields::Second::Second), + symbol, length: match second { Numeric::Numeric => FieldLength::One, Numeric::TwoDigit => FieldLength::TwoDigit, }, }); + // S - Fractional seconds. Represented as DecimalSecond. // A - Milliseconds in day. Not used in skeletons. } - if let Some(precision) = self.fractional_second { - // S - Fractional seconds. - fields.push(Field { - symbol: FieldSymbol::Second(fields::Second::FractionalSecond), - length: FieldLength::Fixed(precision), - }); - } - if self.time_zone_name.is_some() { // Only the lower "v" field is used in skeletons. fields.push(Field { @@ -657,14 +657,8 @@ impl<'data> From<&Pattern<'data>> for Bag { | FieldLength::NumericOverride(_) | FieldLength::TwoDigit | FieldLength::Abbreviated => Text::Short, - FieldLength::TimeZoneFallbackOverride(_) => { - debug_assert!(false, "unexpected length for era field"); - Text::Short - } FieldLength::Wide => Text::Long, - FieldLength::Narrow | FieldLength::Six | FieldLength::Fixed(_) => { - Text::Narrow - } + FieldLength::Narrow | FieldLength::Six => Text::Narrow, }); } FieldSymbol::Year(year) => { @@ -687,16 +681,10 @@ impl<'data> From<&Pattern<'data>> for Bag { bag.month = Some(match field.length { FieldLength::One => Month::Numeric, FieldLength::NumericOverride(_) => Month::Numeric, - FieldLength::TimeZoneFallbackOverride(_) => { - debug_assert!(false, "unexpected length for month field"); - Month::Numeric - } FieldLength::TwoDigit => Month::TwoDigit, FieldLength::Abbreviated => Month::Short, FieldLength::Wide => Month::Long, - FieldLength::Narrow | FieldLength::Six | FieldLength::Fixed(_) => { - Month::Narrow - } + FieldLength::Narrow | FieldLength::Six => Month::Narrow, }); } FieldSymbol::Week(week) => { @@ -757,15 +745,9 @@ impl<'data> From<&Pattern<'data>> for Bag { // 'ccc, MMM d. y' unimplemented!("Numeric stand-alone fields are not supported.") } - FieldLength::TimeZoneFallbackOverride(_) => { - debug_assert!(false, "unexpected length for weekday field"); - Text::Short - } FieldLength::Abbreviated => Text::Short, FieldLength::Wide => Text::Long, - FieldLength::Narrow | FieldLength::Six | FieldLength::Fixed(_) => { - Text::Narrow - } + FieldLength::Narrow | FieldLength::Six => Text::Narrow, }, fields::Weekday::Local => unimplemented!("fields::Weekday::Local"), }); @@ -801,18 +783,29 @@ impl<'data> From<&Pattern<'data>> for Bag { _ => Numeric::Numeric, }); } - fields::Second::FractionalSecond => { - if let FieldLength::Fixed(p) = field.length { - if p > 0 { - bag.fractional_second = Some(p); - } - } - } fields::Second::Millisecond => { // fields::Second::Millisecond is not implemented (#1834) } } } + FieldSymbol::DecimalSecond(decimal_second) => { + use FractionalSecondDigits::*; + bag.second = Some(match field.length { + FieldLength::TwoDigit => Numeric::TwoDigit, + _ => Numeric::Numeric, + }); + bag.fractional_second = Some(match decimal_second { + fields::DecimalSecond::SecondF1 => F1, + fields::DecimalSecond::SecondF2 => F2, + fields::DecimalSecond::SecondF3 => F3, + fields::DecimalSecond::SecondF4 => F4, + fields::DecimalSecond::SecondF5 => F5, + fields::DecimalSecond::SecondF6 => F6, + fields::DecimalSecond::SecondF7 => F7, + fields::DecimalSecond::SecondF8 => F8, + fields::DecimalSecond::SecondF9 => F9, + }); + } FieldSymbol::TimeZone(time_zone_name) => { bag.time_zone_name = Some(match time_zone_name { fields::TimeZone::LowerZ => match field.length { @@ -856,7 +849,7 @@ mod test { hour: Some(Numeric::Numeric), minute: Some(Numeric::Numeric), second: Some(Numeric::Numeric), - fractional_second: Some(3), + fractional_second: Some(FractionalSecondDigits::F3), ..Default::default() }; @@ -869,10 +862,9 @@ mod test { (Symbol::Day(fields::Day::DayOfMonth), Length::One).into(), (Symbol::Hour(fields::Hour::H23), Length::One).into(), (Symbol::Minute, Length::One).into(), - (Symbol::Second(fields::Second::Second), Length::One).into(), ( - Symbol::Second(fields::Second::FractionalSecond), - Length::Fixed(3) + Symbol::DecimalSecond(fields::DecimalSecond::SecondF3), + Length::One ) .into(), ] diff --git a/components/datetime/src/pattern/item/mod.rs b/components/datetime/src/pattern/item/mod.rs index f1d560b60ad..444c4dbdde8 100644 --- a/components/datetime/src/pattern/item/mod.rs +++ b/components/datetime/src/pattern/item/mod.rs @@ -35,14 +35,8 @@ impl From<(FieldSymbol, FieldLength)> for PatternItem { impl TryFrom<(FieldSymbol, u8)> for PatternItem { type Error = PatternError; fn try_from(input: (FieldSymbol, u8)) -> Result { - let length = if input.0 != FieldSymbol::Second(crate::fields::Second::FractionalSecond) { - FieldLength::from_idx(input.1).map_err(|_| PatternError::FieldLengthInvalid(input.0))? - } else if input.1 <= 127 { - FieldLength::from_idx(128 + input.1) - .map_err(|_| PatternError::FieldLengthInvalid(input.0))? - } else { - return Err(PatternError::FieldLengthInvalid(input.0)); - }; + let length = FieldLength::from_idx(input.1) + .map_err(|_| PatternError::FieldLengthInvalid(input.0))?; Ok(Self::Field(Field { symbol: input.0, length, @@ -56,14 +50,8 @@ impl TryFrom<(char, u8)> for PatternItem { let symbol = FieldSymbol::try_from(input.0).map_err(|_| PatternError::InvalidSymbol(input.0))?; - let length = if symbol != FieldSymbol::Second(crate::fields::Second::FractionalSecond) { - FieldLength::from_idx(input.1).map_err(|_| PatternError::FieldLengthInvalid(symbol))? - } else if input.1 <= 127 { - FieldLength::from_idx(128 + input.1) - .map_err(|_| PatternError::FieldLengthInvalid(symbol))? - } else { - return Err(PatternError::FieldLengthInvalid(symbol)); - }; + let length = + FieldLength::from_idx(input.1).map_err(|_| PatternError::FieldLengthInvalid(symbol))?; Ok(Self::Field(Field { symbol, length })) } } diff --git a/components/datetime/src/pattern/mod.rs b/components/datetime/src/pattern/mod.rs index 0b42340121d..7f7e8dfb217 100644 --- a/components/datetime/src/pattern/mod.rs +++ b/components/datetime/src/pattern/mod.rs @@ -88,10 +88,8 @@ impl From<&PatternItem> for TimeGranularity { PatternItem::Field(field) => match field.symbol { fields::FieldSymbol::Hour(_) => Self::Hours, fields::FieldSymbol::Minute => Self::Minutes, - fields::FieldSymbol::Second(s) => match s { - fields::Second::FractionalSecond => Self::Nanoseconds, - fields::Second::Millisecond | fields::Second::Second => Self::Seconds, - }, + fields::FieldSymbol::Second(_) => Self::Seconds, + fields::FieldSymbol::DecimalSecond(_) => Self::Nanoseconds, _ => Self::None, }, _ => Self::None, diff --git a/components/datetime/src/pattern/reference/display.rs b/components/datetime/src/pattern/reference/display.rs index 2c37f4f4f54..d737c2281c7 100644 --- a/components/datetime/src/pattern/reference/display.rs +++ b/components/datetime/src/pattern/reference/display.rs @@ -10,6 +10,7 @@ use super::{ super::{GenericPatternItem, PatternItem}, GenericPattern, Pattern, }; +use crate::fields::FieldSymbol; use alloc::string::String; use core::fmt::{self, Write}; @@ -84,6 +85,12 @@ impl fmt::Display for Pattern { for _ in 0..field.length.to_len() { formatter.write_char(ch)?; } + if let FieldSymbol::DecimalSecond(decimal_second) = field.symbol { + formatter.write_char('.')?; + for _ in 0..(decimal_second as u8) { + formatter.write_char('S')?; + } + } } PatternItem::Literal(ch) => { buffer.push(*ch); diff --git a/components/datetime/src/pattern/reference/parser.rs b/components/datetime/src/pattern/reference/parser.rs index 5bcf85f49b8..50903c5f777 100644 --- a/components/datetime/src/pattern/reference/parser.rs +++ b/components/datetime/src/pattern/reference/parser.rs @@ -7,16 +7,101 @@ use super::{ super::{GenericPatternItem, PatternItem}, GenericPattern, Pattern, }; -use crate::fields::FieldSymbol; +use crate::fields::{self, Field, FieldLength, FieldSymbol}; use alloc::string::String; use alloc::vec; use alloc::vec::Vec; -use core::convert::{TryFrom, TryInto}; +use core::{ + convert::{TryFrom, TryInto}, + mem, +}; + +#[derive(Debug, PartialEq)] +struct SegmentSymbol { + symbol: FieldSymbol, + length: u8, +} + +impl SegmentSymbol { + fn finish(self, result: &mut Vec) -> Result<(), PatternError> { + (self.symbol, self.length) + .try_into() + .map(|item| result.push(item)) + } +} + +#[derive(Debug, PartialEq)] +struct SegmentSecondSymbol { + integer_digits: u8, + seen_decimal_separator: bool, + fraction_digits: u8, +} + +impl SegmentSecondSymbol { + fn finish(self, result: &mut Vec) -> Result<(), PatternError> { + let second_symbol = FieldSymbol::Second(fields::Second::Second); + let symbol = if self.fraction_digits == 0 { + second_symbol + } else { + let decimal_second = fields::DecimalSecond::from_idx(self.fraction_digits) + .map_err(|_| PatternError::FieldLengthInvalid(second_symbol))?; + FieldSymbol::DecimalSecond(decimal_second) + }; + let length = FieldLength::from_idx(self.integer_digits) + .map_err(|_| PatternError::FieldLengthInvalid(symbol))?; + result.push(PatternItem::Field(Field { symbol, length })); + if self.seen_decimal_separator && self.fraction_digits == 0 { + result.push(PatternItem::Literal('.')); + } + Ok(()) + } +} + +#[derive(Debug, PartialEq)] +struct SegmentLiteral { + literal: String, + quoted: bool, +} + +impl SegmentLiteral { + fn finish(self, result: &mut Vec) -> Result<(), PatternError> { + if !self.literal.is_empty() { + result.extend(self.literal.chars().map(PatternItem::from)); + } + Ok(()) + } + + fn finish_generic(self, result: &mut Vec) -> Result<(), PatternError> { + if !self.literal.is_empty() { + result.extend(self.literal.chars().map(GenericPatternItem::from)); + } + Ok(()) + } +} #[derive(Debug, PartialEq)] enum Segment { - Symbol { symbol: FieldSymbol, length: u8 }, - Literal { literal: String, quoted: bool }, + Symbol(SegmentSymbol), + SecondSymbol(SegmentSecondSymbol), + Literal(SegmentLiteral), +} + +impl Segment { + fn finish(self, result: &mut Vec) -> Result<(), PatternError> { + match self { + Self::Symbol(v) => v.finish(result), + Self::SecondSymbol(v) => v.finish(result), + Self::Literal(v) => v.finish(result), + } + } + + fn finish_generic(self, result: &mut Vec) -> Result<(), PatternError> { + match self { + Self::Symbol(_) => unreachable!("no symbols in generic pattern"), + Self::SecondSymbol(_) => unreachable!("no symbols in generic pattern"), + Self::Literal(v) => v.finish_generic(result), + } + } } #[derive(Debug)] @@ -29,10 +114,10 @@ impl<'p> Parser<'p> { pub fn new(source: &'p str) -> Self { Self { source, - state: Segment::Literal { + state: Segment::Literal(SegmentLiteral { literal: String::new(), quoted: false, - }, + }), } } @@ -44,39 +129,40 @@ impl<'p> Parser<'p> { ) -> Result { if ch == '\'' { match (&mut self.state, chars.peek() == Some(&'\'')) { - ( - Segment::Literal { - ref mut literal, .. - }, - true, - ) => { - literal.push('\''); + (Segment::Literal(ref mut literal), true) => { + literal.literal.push('\''); chars.next(); } - (Segment::Literal { ref mut quoted, .. }, false) => { - *quoted = !*quoted; + (Segment::Literal(ref mut literal), false) => { + literal.quoted = !literal.quoted; } - (Segment::Symbol { symbol, length }, true) => { - result.push((*symbol, *length).try_into()?); - self.state = Segment::Literal { - literal: String::from(ch), - quoted: false, - }; + (state, true) => { + mem::replace( + state, + Segment::Literal(SegmentLiteral { + literal: String::from(ch), + quoted: false, + }), + ) + .finish(result)?; chars.next(); } - (Segment::Symbol { symbol, length }, false) => { - result.push((*symbol, *length).try_into()?); - self.state = Segment::Literal { - literal: String::new(), - quoted: true, - }; + (state, false) => { + mem::replace( + state, + Segment::Literal(SegmentLiteral { + literal: String::new(), + quoted: true, + }), + ) + .finish(result)?; } } Ok(true) - } else if let Segment::Literal { + } else if let Segment::Literal(SegmentLiteral { ref mut literal, quoted: true, - } = self.state + }) = self.state { literal.push(ch); Ok(true) @@ -92,25 +178,20 @@ impl<'p> Parser<'p> { ) -> Result { if ch == '\'' { match (&mut self.state, chars.peek() == Some(&'\'')) { - ( - Segment::Literal { - ref mut literal, .. - }, - true, - ) => { - literal.push('\''); + (Segment::Literal(literal), true) => { + literal.literal.push('\''); chars.next(); } - (Segment::Literal { ref mut quoted, .. }, false) => { - *quoted = !*quoted; + (Segment::Literal(literal), false) => { + literal.quoted = !literal.quoted; } _ => unreachable!("Generic pattern has no symbols."), } Ok(true) - } else if let Segment::Literal { + } else if let Segment::Literal(SegmentLiteral { ref mut literal, quoted: true, - } = self.state + }) = self.state { literal.push(ch); Ok(true) @@ -119,39 +200,6 @@ impl<'p> Parser<'p> { } } - fn collect_segment(state: Segment, result: &mut Vec) -> Result<(), PatternError> { - match state { - Segment::Symbol { symbol, length } => { - result.push((symbol, length).try_into()?); - } - Segment::Literal { quoted, .. } if quoted => { - return Err(PatternError::UnclosedLiteral); - } - Segment::Literal { literal, .. } => { - result.extend(literal.chars().map(PatternItem::from)); - } - } - Ok(()) - } - - fn collect_generic_segment( - state: Segment, - result: &mut Vec, - ) -> Result<(), PatternError> { - match state { - Segment::Literal { quoted, .. } if quoted => { - return Err(PatternError::UnclosedLiteral); - } - Segment::Literal { literal, .. } => { - if !literal.is_empty() { - result.extend(literal.chars().map(GenericPatternItem::from)) - } - } - _ => unreachable!("Generic pattern has no symbols."), - } - Ok(()) - } - pub fn parse(mut self) -> Result, PatternError> { let mut chars = self.source.chars().peekable(); let mut result = vec![]; @@ -159,39 +207,78 @@ impl<'p> Parser<'p> { while let Some(ch) = chars.next() { if !self.handle_quoted_literal(ch, &mut chars, &mut result)? { if let Ok(new_symbol) = FieldSymbol::try_from(ch) { - match self.state { - Segment::Symbol { + match &mut self.state { + Segment::Symbol(SegmentSymbol { ref symbol, ref mut length, - } if new_symbol == *symbol => { + }) if new_symbol == *symbol => { *length += 1; } - segment => { - Self::collect_segment(segment, &mut result)?; - self.state = Segment::Symbol { - symbol: new_symbol, - length: 1, - }; + Segment::SecondSymbol(SegmentSecondSymbol { + ref mut integer_digits, + seen_decimal_separator: false, + .. + }) if matches!(new_symbol, FieldSymbol::Second(fields::Second::Second)) => { + *integer_digits += 1; + } + state => { + mem::replace( + state, + if matches!(new_symbol, FieldSymbol::Second(fields::Second::Second)) + { + Segment::SecondSymbol(SegmentSecondSymbol { + integer_digits: 1, + seen_decimal_separator: false, + fraction_digits: 0, + }) + } else { + Segment::Symbol(SegmentSymbol { + symbol: new_symbol, + length: 1, + }) + }, + ) + .finish(&mut result)?; } } } else { - match self.state { - Segment::Symbol { symbol, length } => { - result.push((symbol, length).try_into()?); - self.state = Segment::Literal { - literal: String::from(ch), - quoted: false, - }; + match &mut self.state { + Segment::SecondSymbol( + second_symbol @ SegmentSecondSymbol { + seen_decimal_separator: false, + .. + }, + ) if ch == '.' => second_symbol.seen_decimal_separator = true, + Segment::SecondSymbol(second_symbol) if ch == 'S' => { + // Note: this accepts both "ssSSS" and "ss.SSS" + // We say we've seen the separator to switch to fraction mode + second_symbol.seen_decimal_separator = true; + second_symbol.fraction_digits += 1; + } + Segment::Literal(literal) => literal.literal.push(ch), + state => { + mem::replace( + state, + Segment::Literal(SegmentLiteral { + literal: String::from(ch), + quoted: false, + }), + ) + .finish(&mut result)?; } - Segment::Literal { - ref mut literal, .. - } => literal.push(ch), } } } } - Self::collect_segment(self.state, &mut result)?; + if matches!( + self.state, + Segment::Literal(SegmentLiteral { quoted: true, .. }) + ) { + return Err(PatternError::UnclosedLiteral); + } + + self.state.finish(&mut result)?; Ok(result) } @@ -203,7 +290,14 @@ impl<'p> Parser<'p> { while let Some(ch) = chars.next() { if !self.handle_generic_quoted_literal(ch, &mut chars)? { if ch == '{' { - Self::collect_generic_segment(self.state, &mut result)?; + mem::replace( + &mut self.state, + Segment::Literal(SegmentLiteral { + literal: String::new(), + quoted: false, + }), + ) + .finish_generic(&mut result)?; let ch = chars.next().ok_or(PatternError::UnclosedPlaceholder)?; let idx = ch @@ -215,13 +309,9 @@ impl<'p> Parser<'p> { if ch != '}' { return Err(PatternError::UnclosedPlaceholder); } - self.state = Segment::Literal { - literal: String::new(), - quoted: false, - }; - } else if let Segment::Literal { + } else if let Segment::Literal(SegmentLiteral { ref mut literal, .. - } = self.state + }) = self.state { literal.push(ch); } else { @@ -230,7 +320,14 @@ impl<'p> Parser<'p> { } } - Self::collect_generic_segment(self.state, &mut result)?; + if matches!( + self.state, + Segment::Literal(SegmentLiteral { quoted: true, .. }) + ) { + return Err(PatternError::UnclosedLiteral); + } + + self.state.finish_generic(&mut result)?; Ok(result) } @@ -293,11 +390,9 @@ mod tests { ':'.into(), (FieldSymbol::Minute, FieldLength::TwoDigit).into(), ':'.into(), - (fields::Second::Second.into(), FieldLength::TwoDigit).into(), - '.'.into(), ( - fields::Second::FractionalSecond.into(), - FieldLength::Fixed(2), + fields::DecimalSecond::SecondF2.into(), + FieldLength::TwoDigit, ) .into(), ], @@ -502,6 +597,68 @@ mod tests { (fields::DayPeriod::NoonMidnight.into(), FieldLength::One).into(), ], ), + ( + "s.SS", + vec![(fields::DecimalSecond::SecondF2.into(), FieldLength::One).into()], + ), + ( + "sSS", + vec![(fields::DecimalSecond::SecondF2.into(), FieldLength::One).into()], + ), + ( + "s.. z", + vec![ + (fields::Second::Second.into(), FieldLength::One).into(), + '.'.into(), + '.'.into(), + ' '.into(), + (fields::TimeZone::LowerZ.into(), FieldLength::One).into(), + ], + ), + ( + "s.SSz", + vec![ + (fields::DecimalSecond::SecondF2.into(), FieldLength::One).into(), + (fields::TimeZone::LowerZ.into(), FieldLength::One).into(), + ], + ), + ( + "sSSz", + vec![ + (fields::DecimalSecond::SecondF2.into(), FieldLength::One).into(), + (fields::TimeZone::LowerZ.into(), FieldLength::One).into(), + ], + ), + ( + "s.SSss", + vec![ + (fields::DecimalSecond::SecondF2.into(), FieldLength::One).into(), + (fields::Second::Second.into(), FieldLength::TwoDigit).into(), + ], + ), + ( + "sSSss", + vec![ + (fields::DecimalSecond::SecondF2.into(), FieldLength::One).into(), + (fields::Second::Second.into(), FieldLength::TwoDigit).into(), + ], + ), + ( + "s.z", + vec![ + (fields::Second::Second.into(), FieldLength::One).into(), + '.'.into(), + (fields::TimeZone::LowerZ.into(), FieldLength::One).into(), + ], + ), + ( + "s.ss", + vec![ + (fields::Second::Second.into(), FieldLength::One).into(), + '.'.into(), + (fields::Second::Second.into(), FieldLength::TwoDigit).into(), + ], + ), ( "z", vec![(fields::TimeZone::LowerZ.into(), FieldLength::One).into()], @@ -548,7 +705,7 @@ mod tests { ), ( "hh:mm:ss.SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS", - PatternError::FieldLengthInvalid(FieldSymbol::Second(fields::Second::FractionalSecond)), + PatternError::FieldLengthInvalid(FieldSymbol::Second(fields::Second::Second)), ), ]; diff --git a/components/datetime/src/skeleton/helpers.rs b/components/datetime/src/skeleton/helpers.rs index 2ea3bd2266c..a45472b48ab 100644 --- a/components/datetime/src/skeleton/helpers.rs +++ b/components/datetime/src/skeleton/helpers.rs @@ -8,6 +8,7 @@ use core::cmp::Ordering; use crate::{ fields::{self, Field, FieldLength, FieldSymbol}, + neo_skeleton::FractionalSecondDigits, options::{components, length}, pattern::{ hour_cycle, @@ -143,6 +144,7 @@ pub fn create_best_pattern_for_fields<'data>( pattern_plurals.for_each_mut(|pattern| { hour_cycle::naively_apply_preferences(pattern, &components.preferences); naively_apply_time_zone_name(pattern, &components.time_zone_name); + apply_fractional_seconds(pattern, components.fractional_second); }); return BestSkeleton::AllFieldsMatch(pattern_plurals); } @@ -159,7 +161,7 @@ pub fn create_best_pattern_for_fields<'data>( pattern_plurals.for_each_mut(|pattern| { hour_cycle::naively_apply_preferences(pattern, &components.preferences); naively_apply_time_zone_name(pattern, &components.time_zone_name); - append_fractional_seconds(pattern, &time); + apply_fractional_seconds(pattern, components.fractional_second); }); } BestSkeleton::MissingOrExtraFields(pattern_plurals) @@ -188,7 +190,7 @@ pub fn create_best_pattern_for_fields<'data>( pattern_plurals.expect_pattern("Only date patterns can contain plural variants"); hour_cycle::naively_apply_preferences(&mut pattern, &components.preferences); naively_apply_time_zone_name(&mut pattern, &components.time_zone_name); - append_fractional_seconds(&mut pattern, &time); + apply_fractional_seconds(&mut pattern, components.fractional_second); pattern }); @@ -292,7 +294,8 @@ fn group_fields_by_type(fields: &[Field]) -> FieldsByType { | FieldSymbol::Hour(_) | FieldSymbol::Minute | FieldSymbol::Second(_) - | FieldSymbol::TimeZone(_) => time.push(*field), + | FieldSymbol::TimeZone(_) + | FieldSymbol::DecimalSecond(_) => time.push(*field), // Other components // TODO(#486) // FieldSymbol::Era(_) => other.push(*field), @@ -311,7 +314,7 @@ fn adjust_pattern_field_lengths(fields: &[Field], pattern: &mut runtime::Pattern if let PatternItem::Field(pattern_field) = item { if let Some(requested_field) = fields .iter() - .find(|field| field.symbol.discriminant_cmp(&pattern_field.symbol).is_eq()) + .find(|field| field.symbol.skeleton_cmp(&pattern_field.symbol).is_eq()) { if requested_field.length != pattern_field.length && requested_field.get_length_type() == pattern_field.get_length_type() @@ -341,28 +344,33 @@ fn adjust_pattern_field_lengths(fields: &[Field], pattern: &mut runtime::Pattern /// pattern should be adjusted by appending the locale’s decimal separator, followed by the sequence /// of ‘S’ characters from the requested skeleton. /// (see ) -fn append_fractional_seconds(pattern: &mut runtime::Pattern, fields: &[Field]) { - if let Some(requested_field) = fields - .iter() - .find(|field| field.symbol == FieldSymbol::Second(fields::Second::FractionalSecond)) - { +fn apply_fractional_seconds( + pattern: &mut runtime::Pattern, + fractional_seconds: Option, +) { + use FractionalSecondDigits::*; + if let Some(fractional_seconds) = fractional_seconds { let mut items = pattern.items.to_vec(); - if let Some(pos) = items.iter().position(|&item| match item { - PatternItem::Field(field) => { - matches!(field.symbol, FieldSymbol::Second(fields::Second::Second)) - } - _ => false, - }) { - if let FieldLength::Fixed(p) = requested_field.length { - if p > 0 { - items.insert(pos + 1, PatternItem::Field(*requested_field)); - } - } + for item in items.iter_mut() { + if let PatternItem::Field( + ref mut field @ Field { + symbol: + FieldSymbol::Second(fields::Second::Second) | FieldSymbol::DecimalSecond(_), + .. + }, + ) = item + { + field.symbol = FieldSymbol::from_fractional_second_digits(fractional_seconds); + }; } *pattern = runtime::Pattern::from(items); pattern .metadata - .set_time_granularity(TimeGranularity::Nanoseconds); + .set_time_granularity(if fractional_seconds == F0 { + TimeGranularity::Seconds + } else { + TimeGranularity::Nanoseconds + }); } } @@ -407,7 +415,6 @@ pub fn get_best_available_format_pattern<'data>( let mut requested_fields = fields.iter().peekable(); let mut skeleton_fields = skeleton.0.fields_iter().peekable(); - let mut matched_seconds = false; loop { let next = (requested_fields.peek(), skeleton_fields.peek()); @@ -422,10 +429,7 @@ pub fn get_best_available_format_pattern<'data>( skeleton_field.symbol != FieldSymbol::Month(fields::Month::StandAlone) ); - match skeleton_field - .symbol - .discriminant_cmp(&requested_field.symbol) - { + match skeleton_field.symbol.skeleton_cmp(&requested_field.symbol) { Ordering::Less => { // Keep searching for a matching skeleton field. skeleton_fields.next(); @@ -433,31 +437,15 @@ pub fn get_best_available_format_pattern<'data>( continue; } Ordering::Greater => { - // https://unicode.org/reports/tr35/tr35-dates.html#Matching_Skeletons - // A requested skeleton that includes both seconds and fractional seconds (e.g. “mmssSSS”) is allowed - // to match a dateFormatItem skeleton that includes seconds but not fractional seconds (e.g. “ms”). - if !(matched_seconds - && requested_field.symbol - == FieldSymbol::Second(fields::Second::FractionalSecond)) - { - // The requested field symbol is missing from the skeleton. - distance += REQUESTED_SYMBOL_MISSING; - missing_fields += 1; - requested_fields.next(); - continue; - } + // The requested field symbol is missing from the skeleton. + distance += REQUESTED_SYMBOL_MISSING; + missing_fields += 1; + requested_fields.next(); + continue; } _ => (), } - if requested_field.symbol - == FieldSymbol::Second(fields::Second::FractionalSecond) - && skeleton_field.symbol - == FieldSymbol::Second(fields::Second::FractionalSecond) - { - matched_seconds = true; - } - distance += if requested_field == skeleton_field { NO_DISTANCE } else if requested_field.symbol != skeleton_field.symbol { diff --git a/components/datetime/src/skeleton/mod.rs b/components/datetime/src/skeleton/mod.rs index 27c87511e22..e9c2a960536 100644 --- a/components/datetime/src/skeleton/mod.rs +++ b/components/datetime/src/skeleton/mod.rs @@ -414,7 +414,6 @@ mod test { assert_pattern_to_skeleton("LLL", "MMM", "Remove standalone months."); assert_pattern_to_skeleton("s", "s", "Seconds pass through"); - assert_pattern_to_skeleton("S", "S", "Seconds pass through"); assert_pattern_to_skeleton("A", "A", "Seconds pass through"); assert_pattern_to_skeleton("z", "z", "Time zones get passed through"); diff --git a/components/datetime/src/skeleton/reference.rs b/components/datetime/src/skeleton/reference.rs index 2e9a8ff0de6..8ec150e1a18 100644 --- a/components/datetime/src/skeleton/reference.rs +++ b/components/datetime/src/skeleton/reference.rs @@ -104,6 +104,7 @@ impl From<&Pattern> for Skeleton { FieldSymbol::Minute | FieldSymbol::Second(_) | FieldSymbol::TimeZone(_) + | FieldSymbol::DecimalSecond(_) | FieldSymbol::Era | FieldSymbol::Year(_) | FieldSymbol::Week(_) diff --git a/components/datetime/tests/fixtures/tests/components.json b/components/datetime/tests/fixtures/tests/components.json index 48fc33f2add..f4ca4fd7210 100644 --- a/components/datetime/tests/fixtures/tests/components.json +++ b/components/datetime/tests/fixtures/tests/components.json @@ -145,55 +145,19 @@ "hour": "numeric", "minute": "numeric", "second": "two-digit", - "fractional_second": 8 + "fractional_second": 9 }, "semantic": { "fieldSet": ["hour", "minute", "second"], "length": "short", - "fractionalSecondDigits": 8 + "fractionalSecondDigits": 9 }, "preferences": { "hourCycle": "h23" } } }, "output": { "values": { - "en": "14:15:07.01230000" - } - } - }, - { - "input": { - "value": "2022-05-03T14:15:07.123", - "options": { - "components": { - "hour": "numeric", - "minute": "numeric", - "second": "two-digit", - "fractional_second": 127 - } - } - }, - "output": { - "values": { - "en": "14:15:07.1230000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - } - } - }, - { - "input": { - "value": "2022-05-03T14:15:07.123", - "options": { - "components": { - "hour": "numeric", - "minute": "numeric", - "second": "two-digit", - "fractional_second": 255 - } - } - }, - "output": { - "values": { - "en": "14:15:07.1230000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + "en": "14:15:07.012300000" } } } diff --git a/components/datetime/tests/fixtures/tests/patterns.bin b/components/datetime/tests/fixtures/tests/patterns.bin index f42d18eb4d8..2b2a2a80b8e 100644 Binary files a/components/datetime/tests/fixtures/tests/patterns.bin and b/components/datetime/tests/fixtures/tests/patterns.bin differ diff --git a/components/datetime/tests/fixtures/tests/patterns.json b/components/datetime/tests/fixtures/tests/patterns.json index 57d79edfee4..cab8826adb2 100644 --- a/components/datetime/tests/fixtures/tests/patterns.json +++ b/components/datetime/tests/fixtures/tests/patterns.json @@ -20,7 +20,8 @@ "b bb bbb bbbb bbbbb", "m mm", "s ss", - "S SS SSS SSSS SSSSS SSSSS", + "s.S s.SS s.SSS s.SSSS s.SSSSSSSSS", + "ss.S ss.SS ss.SSS ss.SSSS ss.SSSSSSSSS", "A AA AAA AAAA AAAAA AAAAA", "G GG GGG GGGG GGGGG GGGGG" ], diff --git a/components/datetime/tests/resolved_components.rs b/components/datetime/tests/resolved_components.rs index bf95c00cc50..5fdd5ee4d42 100644 --- a/components/datetime/tests/resolved_components.rs +++ b/components/datetime/tests/resolved_components.rs @@ -4,6 +4,7 @@ use icu_calendar::Gregorian; use icu_datetime::{ + neo_skeleton::FractionalSecondDigits, options::{components, length, preferences}, DateTimeFormatterOptions, TypedDateTimeFormatter, }; @@ -83,7 +84,7 @@ fn test_components_bag() { input_bag.hour = Some(components::Numeric::Numeric); input_bag.minute = Some(components::Numeric::TwoDigit); input_bag.second = Some(components::Numeric::TwoDigit); - input_bag.fractional_second = Some(4); + input_bag.fractional_second = Some(FractionalSecondDigits::F4); input_bag.preferences = None; let mut output_bag = input_bag; // make a copy output_bag.month = Some(components::Month::Short);