From 95ff80e2a52b6f5f91b2fb62d3e2dab9b5631c9f Mon Sep 17 00:00:00 2001 From: "Shane F. Carr" Date: Fri, 15 Nov 2024 11:54:15 -0800 Subject: [PATCH] More datetime cleanups for 2.0 Beta (#5823) #1317 - Move docs tests from neo_marker to better places - Making Serde impls experimental --- components/datetime/Cargo.toml | 6 +- components/datetime/benches/datetime.rs | 5 +- components/datetime/benches/fixtures/mod.rs | 4 +- components/datetime/src/dynamic.rs | 9 +- .../skeleton => fields}/components.rs | 18 +- components/datetime/src/fields/mod.rs | 3 + components/datetime/src/fieldset.rs | 145 +++++++ components/datetime/src/lib.rs | 3 +- components/datetime/src/neo_marker.rs | 366 ------------------ components/datetime/src/neo_pattern.rs | 2 +- components/datetime/src/neo_serde.rs | 114 +++++- components/datetime/src/options/mod.rs | 317 ++++++++++++++- .../src/provider/pattern/reference/pattern.rs | 4 +- .../datetime/src/provider/skeleton/helpers.rs | 3 +- .../datetime/src/provider/skeleton/mod.rs | 10 +- .../datetime/src/provider/skeleton/plural.rs | 14 +- components/datetime/src/raw/neo.rs | 2 +- components/datetime/tests/datetime.rs | 3 +- components/datetime/tests/fixtures/mod.rs | 6 +- .../datetime/tests/resolved_components.rs | 2 +- provider/source/src/datetime/neo_skeleton.rs | 2 +- provider/source/src/datetime/skeletons.rs | 1 + tools/make/diplomat-coverage/src/allowlist.rs | 2 + 23 files changed, 591 insertions(+), 450 deletions(-) rename components/datetime/src/{provider/skeleton => fields}/components.rs (98%) delete mode 100644 components/datetime/src/neo_marker.rs diff --git a/components/datetime/Cargo.toml b/components/datetime/Cargo.toml index 9fc7db01455..82085f822c4 100644 --- a/components/datetime/Cargo.toml +++ b/components/datetime/Cargo.toml @@ -114,16 +114,16 @@ unexpected_cfgs = { level = "warn", check-cfg = ['cfg(icu4x_run_size_tests)'] } [[bench]] name = "datetime" harness = false -required-features = ["compiled_data"] +required-features = ["compiled_data", "serde", "experimental"] [[bench]] name = "pattern" harness = false -required-features = ["bench"] +required-features = ["bench", "serde", "experimental"] [[test]] name = "datetime" -required-features = ["experimental", "compiled_data"] +required-features = ["experimental", "compiled_data", "serde"] [[test]] name = "resolved_components" diff --git a/components/datetime/benches/datetime.rs b/components/datetime/benches/datetime.rs index 4481db47809..799356d60b5 100644 --- a/components/datetime/benches/datetime.rs +++ b/components/datetime/benches/datetime.rs @@ -5,7 +5,7 @@ mod fixtures; use criterion::{criterion_group, criterion_main, Criterion}; -use icu_datetime::FixedCalendarDateTimeFormatter; +use icu_datetime::{fieldset::dynamic::CompositeFieldSet, FixedCalendarDateTimeFormatter}; use icu_calendar::{Date, DateTime, Gregorian, Time}; use icu_locale_core::Locale; @@ -47,7 +47,8 @@ fn datetime_benches(c: &mut Criterion) { .collect(); for setup in &fx.setups { let locale: Locale = setup.locale.parse().expect("Failed to parse locale."); - let skeleton = setup.options.semantic.unwrap(); + let skeleton = + CompositeFieldSet::try_from(setup.options.semantic.unwrap()).unwrap(); let dtf = { FixedCalendarDateTimeFormatter::::try_new( diff --git a/components/datetime/benches/fixtures/mod.rs b/components/datetime/benches/fixtures/mod.rs index 11803235e19..77dc4265fb1 100644 --- a/components/datetime/benches/fixtures/mod.rs +++ b/components/datetime/benches/fixtures/mod.rs @@ -2,7 +2,7 @@ // called LICENSE at the top level of the ICU4X source tree // (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). -use icu_datetime::{options, provider::skeleton::components}; +use icu_datetime::{fields::components, fieldset::serde::CompositeFieldSetSerde, options}; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -24,7 +24,7 @@ pub struct TestInput { pub struct TestOptions { pub length: Option, pub components: Option, - pub semantic: Option, + pub semantic: Option, #[serde(rename = "hourCycle")] pub hour_cycle: Option, } diff --git a/components/datetime/src/dynamic.rs b/components/datetime/src/dynamic.rs index 56968620bf9..5d6c5aca507 100644 --- a/components/datetime/src/dynamic.rs +++ b/components/datetime/src/dynamic.rs @@ -2,8 +2,6 @@ // called LICENSE at the top level of the ICU4X source tree // (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). -#[cfg(feature = "serde")] -use crate::neo_serde::*; use crate::raw::neo::RawNeoOptions; use crate::scaffold::GetField; use crate::{fields, fieldset, NeoSkeletonLength}; @@ -211,11 +209,6 @@ impl GetField for CompositeDateTimeFieldSet { /// An enum supporting all possible field sets and options. #[derive(Debug, Copy, Clone, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr( - feature = "serde", - serde(try_from = "SemanticSkeletonSerde", into = "SemanticSkeletonSerde") -)] #[non_exhaustive] pub enum CompositeFieldSet { /// Field set for a date. @@ -365,7 +358,7 @@ macro_rules! impl_attrs { alignment, }) } - #[cfg(feature = "serde")] + #[cfg(all(feature = "serde", feature = "experimental"))] pub(crate) fn from_date_field_set_with_raw_options(date_field_set: DateFieldSet, options: RawNeoOptions) -> Self { match date_field_set { $( diff --git a/components/datetime/src/provider/skeleton/components.rs b/components/datetime/src/fields/components.rs similarity index 98% rename from components/datetime/src/provider/skeleton/components.rs rename to components/datetime/src/fields/components.rs index 7ead82db13d..66995b3f203 100644 --- a/components/datetime/src/provider/skeleton/components.rs +++ b/components/datetime/src/fields/components.rs @@ -2,7 +2,7 @@ // called LICENSE at the top level of the ICU4X source tree // (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). -//! Types for specifying fields in a classical datetime skeleton. +//! 🚧 \[Experimental\] Types for specifying fields in a classical datetime skeleton. //! //!
//! 🚧 This code is experimental; it may change at any time, in breaking or non-breaking ways, @@ -14,7 +14,7 @@ //! # Examples //! //! ``` -//! use icu::datetime::provider::skeleton::components; +//! use icu::datetime::fields::components; //! //! let mut bag = components::Bag::default(); //! bag.year = Some(components::Year::Numeric); @@ -33,7 +33,6 @@ use crate::{ fields::{self, Field, FieldLength, FieldSymbol}, options::FractionalSecondDigits, provider::pattern::{runtime::Pattern, PatternItem}, - provider::skeleton::PatternPlurals, }; use crate::neo_pattern::DateTimePattern; @@ -116,7 +115,6 @@ impl Bag { /// /// - `default_hour_cycle` specifies the hour cycle to use for the hour field if not in the Bag. /// `preferences::Bag::hour_cycle` takes precedence over this argument. - #[cfg(feature = "datagen")] pub fn to_vec_fields(&self, default_hour_cycle: HourCycle) -> alloc::vec::Vec { let mut fields = alloc::vec::Vec::new(); if let Some(era) = self.era { @@ -579,18 +577,6 @@ impl From for Field { } } -/// Get the resolved components for a FixedCalendarDateTimeFormatter, via the PatternPlurals. In the case of -/// plurals resolve off of the required `other` pattern. -impl From<&PatternPlurals<'_>> for Bag { - fn from(other: &PatternPlurals) -> Self { - let pattern = match other { - PatternPlurals::SinglePattern(pattern) => pattern, - PatternPlurals::MultipleVariants(plural_pattern) => &plural_pattern.other, - }; - Self::from(pattern) - } -} - impl From<&DateTimePattern> for Bag { fn from(value: &DateTimePattern) -> Self { Self::from(value.as_borrowed().0) diff --git a/components/datetime/src/fields/mod.rs b/components/datetime/src/fields/mod.rs index 4a660ebfc41..bda3214810f 100644 --- a/components/datetime/src/fields/mod.rs +++ b/components/datetime/src/fields/mod.rs @@ -13,6 +13,9 @@ use displaydoc::Display; pub use length::{FieldLength, FieldNumericOverrides, LengthError}; pub use symbols::*; +#[cfg(any(feature = "experimental", feature = "datagen"))] +pub mod components; + use core::{ cmp::{Ord, PartialOrd}, convert::TryFrom, diff --git a/components/datetime/src/fieldset.rs b/components/datetime/src/fieldset.rs index bf655555ac3..2068fad96e4 100644 --- a/components/datetime/src/fieldset.rs +++ b/components/datetime/src/fieldset.rs @@ -30,6 +30,19 @@ pub mod dynamic { pub use crate::dynamic::*; } +/// 🚧 \[Experimental\] Types for dealing with serialization of semantic skeletons. +/// +///
+/// 🚧 This code is experimental; it may change at any time, in breaking or non-breaking ways, +/// including in SemVer minor releases. Use with caution. +/// #5825 +///
+#[cfg(all(feature = "experimental", feature = "serde"))] +pub mod serde { + pub use crate::neo_serde::CompositeFieldSetSerde; + pub use crate::neo_serde::CompositeFieldSetSerdeError; +} + #[cfg(doc)] use icu_timezone::TimeZoneInfo; @@ -1147,6 +1160,88 @@ impl_calendar_period_marker!( ); impl_time_marker!( + /// Hours can be switched between 12-hour and 24-hour time via the `u-hc` locale keyword. + /// + /// ``` + /// use icu::calendar::Time; + /// use icu::datetime::fieldset::T; + /// use icu::datetime::FixedCalendarDateTimeFormatter; + /// use icu::locale::locale; + /// use writeable::assert_try_writeable_eq; + /// + /// // By default, en-US uses 12-hour time and fr-FR uses 24-hour time, + /// // but we can set overrides. + /// + /// let formatter = FixedCalendarDateTimeFormatter::<(), _>::try_new( + /// &locale!("en-US-u-hc-h12").into(), + /// T::short().hm(), + /// ) + /// .unwrap(); + /// assert_try_writeable_eq!( + /// formatter.format(&Time::try_new(16, 12, 20, 0).unwrap()), + /// "4:12 PM" + /// ); + /// + /// let formatter = FixedCalendarDateTimeFormatter::<(), _>::try_new( + /// &locale!("en-US-u-hc-h23").into(), + /// T::short().hm(), + /// ) + /// .unwrap(); + /// assert_try_writeable_eq!( + /// formatter.format(&Time::try_new(16, 12, 20, 0).unwrap()), + /// "16:12" + /// ); + /// + /// let formatter = FixedCalendarDateTimeFormatter::<(), _>::try_new( + /// &locale!("fr-FR-u-hc-h12").into(), + /// T::short().hm(), + /// ) + /// .unwrap(); + /// assert_try_writeable_eq!( + /// formatter.format(&Time::try_new(16, 12, 20, 0).unwrap()), + /// "4:12 PM" + /// ); + /// + /// let formatter = FixedCalendarDateTimeFormatter::<(), _>::try_new( + /// &locale!("fr-FR-u-hc-h23").into(), + /// T::short().hm(), + /// ) + /// .unwrap(); + /// assert_try_writeable_eq!( + /// formatter.format(&Time::try_new(16, 12, 20, 0).unwrap()), + /// "16:12" + /// ); + /// ``` + /// + /// Hour cycles `h11` and `h24` are supported, too: + /// + /// ``` + /// use icu::calendar::Time; + /// use icu::datetime::fieldset::T; + /// use icu::datetime::FixedCalendarDateTimeFormatter; + /// use icu::locale::locale; + /// use writeable::assert_try_writeable_eq; + /// + /// let formatter = FixedCalendarDateTimeFormatter::<(), _>::try_new( + /// &locale!("und-u-hc-h11").into(), + /// T::short().hm(), + /// ) + /// .unwrap(); + /// assert_try_writeable_eq!( + /// formatter.format(&Time::try_new(0, 0, 0, 0).unwrap()), + /// "0:00 AM" + /// ); + /// + /// let formatter = FixedCalendarDateTimeFormatter::<(), _>::try_new( + /// &locale!("und-u-hc-h24").into(), + /// T::short().hm(), + /// ) + /// .unwrap(); + /// assert_try_writeable_eq!( + /// formatter.format(&Time::try_new(0, 0, 0, 0).unwrap()), + /// "24:00" + /// ); + /// ``` T, NeoTimeComponents::Time, description = "time (locale-dependent hour cycle)", @@ -1381,6 +1476,56 @@ impl_zone_marker!( /// ); /// ``` /// + /// Can also fall back to the UTC offset: + /// + /// ``` + /// use icu::calendar::{Date, Time}; + /// use icu::timezone::{TimeZoneInfo, UtcOffset, TimeZoneIdMapper, TimeZoneBcp47Id}; + /// use icu::datetime::FixedCalendarDateTimeFormatter; + /// use icu::datetime::fieldset::V; + /// use icu::datetime::DateTimeWriteError; + /// use icu::locale::locale; + /// use tinystr::tinystr; + /// use writeable::assert_try_writeable_eq; + /// + /// // Set up the formatter + /// let mut tzf = FixedCalendarDateTimeFormatter::<(), _>::try_new( + /// &locale!("en").into(), + /// V::short(), + /// ) + /// .unwrap(); + /// + /// // "uschi" - has symbol data for short generic non-location + /// let time_zone = TimeZoneIdMapper::new() + /// .iana_to_bcp47("America/Chicago") + /// .with_offset("-05".parse().ok()) + /// .at_time((Date::try_new_iso(2022, 8, 29).unwrap(), Time::midnight())); + /// assert_try_writeable_eq!( + /// tzf.format(&time_zone), + /// "CT" + /// ); + /// + /// // "ushnl" - has time zone override symbol data for short generic non-location + /// let time_zone = TimeZoneIdMapper::new() + /// .iana_to_bcp47("Pacific/Honolulu") + /// .with_offset("-10".parse().ok()) + /// .at_time((Date::try_new_iso(2022, 8, 29).unwrap(), Time::midnight())); + /// assert_try_writeable_eq!( + /// tzf.format(&time_zone), + /// "HST" + /// ); + /// + /// // Mis-spelling of "America/Chicago" results in a fallback to offset format + /// let time_zone = TimeZoneIdMapper::new() + /// .iana_to_bcp47("America/Chigagou") + /// .with_offset("-05".parse().ok()) + /// .at_time((Date::try_new_iso(2022, 8, 29).unwrap(), Time::midnight())); + /// assert_try_writeable_eq!( + /// tzf.format(&time_zone), + /// "GMT-5" + /// ); + /// ``` + /// /// Since non-location names might change over time, /// this time zone style requires a reference time. /// diff --git a/components/datetime/src/lib.rs b/components/datetime/src/lib.rs index f4355eea9b9..a79823b71d5 100644 --- a/components/datetime/src/lib.rs +++ b/components/datetime/src/lib.rs @@ -96,9 +96,8 @@ pub mod fieldset; mod format; pub mod input; mod neo; -mod neo_marker; pub mod neo_pattern; -#[cfg(feature = "serde")] +#[cfg(all(feature = "experimental", feature = "serde"))] mod neo_serde; pub mod options; pub mod provider; diff --git a/components/datetime/src/neo_marker.rs b/components/datetime/src/neo_marker.rs deleted file mode 100644 index 20f481b679b..00000000000 --- a/components/datetime/src/neo_marker.rs +++ /dev/null @@ -1,366 +0,0 @@ -// This file is part of ICU4X. For terms of use, please see the file -// called LICENSE at the top level of the ICU4X source tree -// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). - -//! Temporary module for neo formatter markers. -//! -//! # Examples -//! -//! ## Alignment -//! -//! By default, datetimes are formatted for a variable-width context. You can -//! give a hint that the strings will be displayed in a column-like context, -//! which will coerce numerics to be padded with zeros. -//! -//! ``` -//! use icu::calendar::Date; -//! use icu::calendar::Gregorian; -//! use icu::datetime::fieldset::YMD; -//! use icu::datetime::options::Alignment; -//! use icu::datetime::FixedCalendarDateTimeFormatter; -//! use icu::locale::locale; -//! use writeable::assert_try_writeable_eq; -//! -//! let plain_formatter = -//! FixedCalendarDateTimeFormatter::::try_new( -//! &locale!("en-US").into(), -//! YMD::short(), -//! ) -//! .unwrap(); -//! -//! let column_formatter = -//! FixedCalendarDateTimeFormatter::::try_new( -//! &locale!("en-US").into(), -//! YMD::short().with_alignment(Alignment::Column), -//! ) -//! .unwrap(); -//! -//! // By default, en-US does not pad the month and day with zeros. -//! assert_try_writeable_eq!( -//! plain_formatter.format(&Date::try_new_gregorian(2025, 1, 1).unwrap()), -//! "1/1/25" -//! ); -//! -//! // The column alignment option hints that they should be padded. -//! assert_try_writeable_eq!( -//! column_formatter.format(&Date::try_new_gregorian(2025, 1, 1).unwrap()), -//! "01/01/25" -//! ); -//! ``` -//! -//! ## Year Style -//! -//! The precision of the rendered year can be adjusted using the [`YearStyle`] option. -//! -//! ``` -//! use icu::calendar::Date; -//! use icu::calendar::Gregorian; -//! use icu::datetime::fieldset::YMD; -//! use icu::datetime::options::YearStyle; -//! use icu::datetime::FixedCalendarDateTimeFormatter; -//! use icu::locale::locale; -//! use writeable::assert_try_writeable_eq; -//! -//! let formatter = FixedCalendarDateTimeFormatter::::try_new( -//! &locale!("en-US").into(), -//! YMD::short().with_year_style(YearStyle::Auto), -//! ) -//! .unwrap(); -//! -//! // Era displayed when needed for disambiguation, -//! // such as years before year 0 and small year numbers: -//! assert_try_writeable_eq!( -//! formatter.format(&Date::try_new_gregorian(-1000, 1, 1).unwrap()), -//! "1/1/1001 BC" -//! ); -//! assert_try_writeable_eq!( -//! formatter.format(&Date::try_new_gregorian(77, 1, 1).unwrap()), -//! "1/1/77 AD" -//! ); -//! // Era elided for modern years: -//! assert_try_writeable_eq!( -//! formatter.format(&Date::try_new_gregorian(1900, 1, 1).unwrap()), -//! "1/1/1900" -//! ); -//! // Era and century both elided for nearby years: -//! assert_try_writeable_eq!( -//! formatter.format(&Date::try_new_gregorian(2025, 1, 1).unwrap()), -//! "1/1/25" -//! ); -//! -//! let formatter = FixedCalendarDateTimeFormatter::::try_new( -//! &locale!("en-US").into(), -//! YMD::short().with_year_style(YearStyle::Full), -//! ) -//! .unwrap(); -//! -//! // Era still displayed in cases with ambiguity: -//! assert_try_writeable_eq!( -//! formatter.format(&Date::try_new_gregorian(-1000, 1, 1).unwrap()), -//! "1/1/1001 BC" -//! ); -//! assert_try_writeable_eq!( -//! formatter.format(&Date::try_new_gregorian(77, 1, 1).unwrap()), -//! "1/1/77 AD" -//! ); -//! // Era elided for modern years: -//! assert_try_writeable_eq!( -//! formatter.format(&Date::try_new_gregorian(1900, 1, 1).unwrap()), -//! "1/1/1900" -//! ); -//! // But now we always get a full-precision year: -//! assert_try_writeable_eq!( -//! formatter.format(&Date::try_new_gregorian(2025, 1, 1).unwrap()), -//! "1/1/2025" -//! ); -//! -//! let formatter = FixedCalendarDateTimeFormatter::::try_new( -//! &locale!("en-US").into(), -//! YMD::short().with_year_style(YearStyle::Always), -//! ) -//! .unwrap(); -//! -//! // Era still displayed in cases with ambiguity: -//! assert_try_writeable_eq!( -//! formatter.format(&Date::try_new_gregorian(-1000, 1, 1).unwrap()), -//! "1/1/1001 BC" -//! ); -//! assert_try_writeable_eq!( -//! formatter.format(&Date::try_new_gregorian(77, 1, 1).unwrap()), -//! "1/1/77 AD" -//! ); -//! // But now it is shown even on modern years: -//! assert_try_writeable_eq!( -//! formatter.format(&Date::try_new_gregorian(1900, 1, 1).unwrap()), -//! "1/1/1900 AD" -//! ); -//! assert_try_writeable_eq!( -//! formatter.format(&Date::try_new_gregorian(2025, 1, 1).unwrap()), -//! "1/1/2025 AD" -//! ); -//! ``` -//! -//! ## Hour Cycle -//! -//! Hours can be switched between 12-hour and 24-hour time via the `u-hc` locale keyword. -//! -//! ``` -//! use icu::calendar::Time; -//! use icu::datetime::fieldset::T; -//! use icu::datetime::FixedCalendarDateTimeFormatter; -//! use icu::locale::locale; -//! use writeable::assert_try_writeable_eq; -//! -//! // By default, en-US uses 12-hour time and fr-FR uses 24-hour time, -//! // but we can set overrides. -//! -//! let formatter = FixedCalendarDateTimeFormatter::<(), _>::try_new( -//! &locale!("en-US-u-hc-h12").into(), -//! T::short().hm(), -//! ) -//! .unwrap(); -//! assert_try_writeable_eq!( -//! formatter.format(&Time::try_new(16, 12, 20, 0).unwrap()), -//! "4:12 PM" -//! ); -//! -//! let formatter = FixedCalendarDateTimeFormatter::<(), _>::try_new( -//! &locale!("en-US-u-hc-h23").into(), -//! T::short().hm(), -//! ) -//! .unwrap(); -//! assert_try_writeable_eq!( -//! formatter.format(&Time::try_new(16, 12, 20, 0).unwrap()), -//! "16:12" -//! ); -//! -//! let formatter = FixedCalendarDateTimeFormatter::<(), _>::try_new( -//! &locale!("fr-FR-u-hc-h12").into(), -//! T::short().hm(), -//! ) -//! .unwrap(); -//! assert_try_writeable_eq!( -//! formatter.format(&Time::try_new(16, 12, 20, 0).unwrap()), -//! "4:12 PM" -//! ); -//! -//! let formatter = FixedCalendarDateTimeFormatter::<(), _>::try_new( -//! &locale!("fr-FR-u-hc-h23").into(), -//! T::short().hm(), -//! ) -//! .unwrap(); -//! assert_try_writeable_eq!( -//! formatter.format(&Time::try_new(16, 12, 20, 0).unwrap()), -//! "16:12" -//! ); -//! ``` -//! -//! Hour cycles `h11` and `h24` are supported, too: -//! -//! ``` -//! use icu::calendar::Time; -//! use icu::datetime::fieldset::T; -//! use icu::datetime::FixedCalendarDateTimeFormatter; -//! use icu::locale::locale; -//! use writeable::assert_try_writeable_eq; -//! -//! let formatter = FixedCalendarDateTimeFormatter::<(), _>::try_new( -//! &locale!("und-u-hc-h11").into(), -//! T::short().hm(), -//! ) -//! .unwrap(); -//! assert_try_writeable_eq!( -//! formatter.format(&Time::try_new(0, 0, 0, 0).unwrap()), -//! "0:00 AM" -//! ); -//! -//! let formatter = FixedCalendarDateTimeFormatter::<(), _>::try_new( -//! &locale!("und-u-hc-h24").into(), -//! T::short().hm(), -//! ) -//! .unwrap(); -//! assert_try_writeable_eq!( -//! formatter.format(&Time::try_new(0, 0, 0, 0).unwrap()), -//! "24:00" -//! ); -//! ``` -//! -//! ## Time Precision -//! -//! The time can be displayed with hour, minute, or second precision, and -//! zero-valued fields can be automatically hidden. -//! -//! ``` -//! use icu::calendar::Time; -//! use icu::datetime::fieldset::T; -//! use icu::datetime::options::FractionalSecondDigits; -//! use icu::datetime::options::TimePrecision; -//! use icu::datetime::FixedCalendarDateTimeFormatter; -//! use icu::locale::locale; -//! use writeable::assert_try_writeable_eq; -//! -//! let formatters = [ -//! TimePrecision::HourPlus, -//! TimePrecision::HourExact, -//! TimePrecision::MinutePlus, -//! TimePrecision::MinuteExact, -//! TimePrecision::SecondPlus, -//! TimePrecision::SecondExact(FractionalSecondDigits::F0), -//! ].map(|time_precision| { -//! FixedCalendarDateTimeFormatter::<(), _>::try_new( -//! &locale!("en-US").into(), -//! T::short().with_time_precision(time_precision), -//! ) -//! .unwrap() -//! }); -//! -//! let times = [ -//! Time::try_new(7, 0, 0, 0).unwrap(), -//! Time::try_new(7, 0, 10, 0).unwrap(), -//! Time::try_new(7, 12, 20, 5).unwrap(), -//! ]; -//! -//! // TODO(#5782): the Plus variants should render fractional digits -//! let expected_value_table = [ -//! // 7:00:00, 7:00:10, 7:12:20.5432 -//! ["7 AM", "7:00:10 AM", "7:12:20 AM"], // HourPlus -//! ["7 AM", "7 AM", "7 AM"], // HourExact -//! ["7:00 AM", "7:00:10 AM", "7:12:20 AM"], // MinutePlus -//! ["7:00 AM", "7:00 AM", "7:12 AM"], // MinuteExact -//! ["7:00:00 AM", "7:00:10 AM", "7:12:20 AM"], // SecondPlus -//! ["7:00:00 AM", "7:00:10 AM", "7:12:20 AM"], // SecondExact -//! ]; -//! -//! for (expected_value_row, formatter) in expected_value_table.iter().zip(formatters.iter()) { -//! for (expected_value, time) in expected_value_row.iter().zip(times.iter()) { -//! assert_try_writeable_eq!( -//! formatter.format(time), -//! *expected_value, -//! Ok(()), -//! "{formatter:?} @ {time:?}" -//! ); -//! } -//! } -//! ``` -//! -//! ## Fractional Second Digits -//! -//! Times can be displayed with a custom number of fractional digits from 0-9: -//! -//! ``` -//! use icu::calendar::Gregorian; -//! use icu::calendar::Time; -//! use icu::datetime::fieldset::T; -//! use icu::datetime::options::FractionalSecondDigits; -//! use icu::datetime::options::TimePrecision; -//! use icu::datetime::FixedCalendarDateTimeFormatter; -//! use icu::locale::locale; -//! use writeable::assert_try_writeable_eq; -//! -//! let formatter = FixedCalendarDateTimeFormatter::<(), _>::try_new( -//! &locale!("en-US").into(), -//! T::short().with_time_precision(TimePrecision::SecondExact(FractionalSecondDigits::F2)), -//! ) -//! .unwrap(); -//! -//! assert_try_writeable_eq!( -//! formatter.format(&Time::try_new(16, 12, 20, 543200000).unwrap()), -//! "4:12:20.54 PM" -//! ); -//! ``` -//! -//! ## Time Zone Formatting -//! -//! Here, we configure a [`DateTimeFormatter`] to format with generic non-location short, -//! which falls back to the offset when unavailable (see [`V`]). -//! -//! ``` -//! use icu::calendar::{Date, Time}; -//! use icu::timezone::{TimeZoneInfo, UtcOffset, TimeZoneIdMapper, TimeZoneBcp47Id}; -//! use icu::datetime::FixedCalendarDateTimeFormatter; -//! use icu::datetime::fieldset::V; -//! use icu::datetime::DateTimeWriteError; -//! use icu::locale::locale; -//! use tinystr::tinystr; -//! use writeable::assert_try_writeable_eq; -//! -//! // Set up the formatter -//! let mut tzf = FixedCalendarDateTimeFormatter::<(), _>::try_new( -//! &locale!("en").into(), -//! V::short(), -//! ) -//! .unwrap(); -//! -//! // "uschi" - has symbol data for short generic non-location -//! let time_zone = TimeZoneIdMapper::new() -//! .iana_to_bcp47("America/Chicago") -//! .with_offset("-05".parse().ok()) -//! .at_time((Date::try_new_iso(2022, 8, 29).unwrap(), Time::midnight())); -//! assert_try_writeable_eq!( -//! tzf.format(&time_zone), -//! "CT" -//! ); -//! -//! // "ushnl" - has time zone override symbol data for short generic non-location -//! let time_zone = TimeZoneIdMapper::new() -//! .iana_to_bcp47("Pacific/Honolulu") -//! .with_offset("-10".parse().ok()) -//! .at_time((Date::try_new_iso(2022, 8, 29).unwrap(), Time::midnight())); -//! assert_try_writeable_eq!( -//! tzf.format(&time_zone), -//! "HST" -//! ); -//! -//! // Mis-spelling of "America/Chicago" results in a fallback to offset format -//! let time_zone = TimeZoneIdMapper::new() -//! .iana_to_bcp47("America/Chigagou") -//! .with_offset("-05".parse().ok()) -//! .at_time((Date::try_new_iso(2022, 8, 29).unwrap(), Time::midnight())); -//! assert_try_writeable_eq!( -//! tzf.format(&time_zone), -//! "GMT-5" -//! ); -//! ``` - -#[cfg(doc)] -use crate::{fieldset::*, DateTimeFormatter}; diff --git a/components/datetime/src/neo_pattern.rs b/components/datetime/src/neo_pattern.rs index 274760f120c..789ed51305e 100644 --- a/components/datetime/src/neo_pattern.rs +++ b/components/datetime/src/neo_pattern.rs @@ -42,7 +42,7 @@ size_test!(DateTimePattern, date_time_pattern_size, 32); /// use icu::calendar::Gregorian; /// use icu::datetime::fieldset::YMD; /// use icu::datetime::neo_pattern::DateTimePattern; -/// use icu::datetime::provider::skeleton::components; +/// use icu::datetime::fields::components; /// use icu::datetime::FixedCalendarDateTimeFormatter; /// use icu::locale::locale; /// use writeable::assert_writeable_eq; diff --git a/components/datetime/src/neo_serde.rs b/components/datetime/src/neo_serde.rs index fdb534b4efe..b501f4db839 100644 --- a/components/datetime/src/neo_serde.rs +++ b/components/datetime/src/neo_serde.rs @@ -11,27 +11,90 @@ use serde::{Deserialize, Serialize}; // Bring `Day`, `Hour`, ... into scope in this file. They are used in multiple places use FieldSetField::*; -#[derive(displaydoc::Display)] -pub(crate) enum Error { +/// 🚧 \[Experimental\] An error when resolving a [`CompositeFieldSet`] +/// from a [`CompositeFieldSetSerde`]. +/// +///
+/// 🚧 This code is experimental; it may change at any time, in breaking or non-breaking ways, +/// including in SemVer minor releases. Use with caution. +/// #5825 +///
+#[derive(Debug, displaydoc::Display)] +#[non_exhaustive] +pub enum CompositeFieldSetSerdeError { + /// The deserialized field set contains no fields. #[displaydoc("at least one field is required")] NoFields, + /// The fields in the deserialized field set are invalid together. #[displaydoc("the given combination of fields does not create a valid semantic skeleton")] InvalidFields, } -#[derive(Serialize, Deserialize)] -pub(crate) struct SemanticSkeletonSerde { +/// 🚧 \[Experimental\] A type corresponding to [`CompositeFieldSet`] that implements +/// [`serde::Serialize`] and [`serde::Deserialize`]. +/// +/// The serialized representation is subject to change. +/// +///
+/// 🚧 This code is experimental; it may change at any time, in breaking or non-breaking ways, +/// including in SemVer minor releases. Use with caution. +/// #5825 +///
+/// +/// # Examples +/// +/// ``` +/// use icu::datetime::fieldset; +/// use icu::datetime::fieldset::dynamic::CompositeFieldSet; +/// use icu::datetime::fieldset::dynamic::DateFieldSet; +/// use icu::datetime::fieldset::serde::CompositeFieldSetSerde; +/// +/// let field_set = CompositeFieldSet::Date(DateFieldSet::YMD(fieldset::YMD::short())); +/// let serde_input = CompositeFieldSetSerde::from(field_set); +/// +/// let json_string = serde_json::to_string(&serde_input).unwrap(); +/// assert_eq!( +/// json_string, +/// r#"{"fieldSet":["year","month","day"],"length":"short"}"# +/// ); +/// +/// let serde_output = serde_json::from_str::(&json_string).unwrap(); +/// let deserialized = CompositeFieldSet::try_from(serde_output).unwrap(); +/// +/// assert_eq!(field_set, deserialized); +/// ``` +/// +/// If the field set is invalid, an error will occur: +/// +/// ``` +/// use icu::datetime::fieldset::dynamic::CompositeFieldSet; +/// use icu::datetime::fieldset::serde::CompositeFieldSetSerde; +/// use icu::datetime::fieldset::serde::CompositeFieldSetSerdeError; +/// +/// let json_string = r#"{"fieldSet":["year","time"],"length":"short"}"#; +/// let serde_output = serde_json::from_str::(&json_string).unwrap(); +/// +/// assert!(matches!( +/// CompositeFieldSet::try_from(serde_output), +/// Err(CompositeFieldSetSerdeError::InvalidFields) +/// )); +/// ``` +#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)] +pub struct CompositeFieldSetSerde { #[serde(rename = "fieldSet")] pub(crate) field_set: FieldSetSerde, pub(crate) length: NeoSkeletonLength, + #[serde(skip_serializing_if = "Option::is_none")] pub(crate) alignment: Option, #[serde(rename = "yearStyle")] + #[serde(skip_serializing_if = "Option::is_none")] pub(crate) year_style: Option, #[serde(rename = "timePrecision")] + #[serde(skip_serializing_if = "Option::is_none")] pub(crate) time_precision: Option, } -impl From for SemanticSkeletonSerde { +impl From for CompositeFieldSetSerde { fn from(value: CompositeFieldSet) -> Self { let (serde_field, options) = match value { CompositeFieldSet::Date(v) => FieldSetSerde::from_date_field_set(v), @@ -82,9 +145,9 @@ impl From for SemanticSkeletonSerde { } } -impl TryFrom for CompositeFieldSet { - type Error = Error; - fn try_from(value: SemanticSkeletonSerde) -> Result { +impl TryFrom for CompositeFieldSet { + type Error = CompositeFieldSetSerdeError; + fn try_from(value: CompositeFieldSetSerde) -> Result { let date = value.field_set.date_only(); let time = value.field_set.time_only(); let zone = value.field_set.zone_only(); @@ -102,15 +165,15 @@ impl TryFrom for CompositeFieldSet { date.to_calendar_period_field_set(options) .map(CompositeFieldSet::CalendarPeriod) }) - .ok_or(Error::InvalidFields), + .ok_or(Self::Error::InvalidFields), (false, true, false) => time .to_time_field_set(options) .map(CompositeFieldSet::Time) - .ok_or(Error::InvalidFields), + .ok_or(Self::Error::InvalidFields), (false, false, true) => zone .to_zone_field_set(options) .map(CompositeFieldSet::Zone) - .ok_or(Error::InvalidFields), + .ok_or(Self::Error::InvalidFields), (true, true, false) => date .to_date_field_set(options) .map(|date_field_set| { @@ -121,21 +184,21 @@ impl TryFrom for CompositeFieldSet { ), ) }) - .ok_or(Error::InvalidFields), + .ok_or(Self::Error::InvalidFields), (true, false, true) => date .to_date_field_set(options) .and_then(|date_field_set| { zone.to_time_zone_style() .map(|style| CompositeFieldSet::DateZone(date_field_set, style)) }) - .ok_or(Error::InvalidFields), + .ok_or(Self::Error::InvalidFields), (false, true, true) => time .to_time_field_set(options) .and_then(|time_field_set| { zone.to_time_zone_style() .map(|style| CompositeFieldSet::TimeZone(time_field_set, style)) }) - .ok_or(Error::InvalidFields), + .ok_or(Self::Error::InvalidFields), (true, true, true) => date .to_date_field_set(options) .and_then(|date_field_set| { @@ -149,8 +212,8 @@ impl TryFrom for CompositeFieldSet { ) }) }) - .ok_or(Error::InvalidFields), - (false, false, false) => Err(Error::NoFields), + .ok_or(Self::Error::InvalidFields), + (false, false, false) => Err(Self::Error::NoFields), } } } @@ -291,7 +354,7 @@ impl From for FieldSetSerde { } } -#[derive(Copy, Clone, PartialEq, Eq)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] pub(crate) struct FieldSetSerde { pub(crate) bit_fields: u64, } @@ -510,16 +573,25 @@ fn test_basic() { }), ZoneStyle::V, ); + let skeleton_serde = CompositeFieldSetSerde::from(skeleton); - let json_string = serde_json::to_string(&skeleton).unwrap(); + let json_string = serde_json::to_string(&skeleton_serde).unwrap(); assert_eq!( json_string, r#"{"fieldSet":["year","month","day","weekday","time","zoneGeneric"],"length":"medium","alignment":"column","yearStyle":"always","timePrecision":"secondF3"}"# ); - let json_skeleton = serde_json::from_str::(&json_string).unwrap(); + let json_skeleton: CompositeFieldSet = + serde_json::from_str::(&json_string) + .unwrap() + .try_into() + .unwrap(); assert_eq!(skeleton, json_skeleton); - let bincode_bytes = bincode::serialize(&skeleton).unwrap(); - let bincode_skeleton = bincode::deserialize::(&bincode_bytes).unwrap(); + let bincode_bytes = bincode::serialize(&skeleton_serde).unwrap(); + let bincode_skeleton: CompositeFieldSet = + bincode::deserialize::(&bincode_bytes) + .unwrap() + .try_into() + .unwrap(); assert_eq!(skeleton, bincode_skeleton); } diff --git a/components/datetime/src/options/mod.rs b/components/datetime/src/options/mod.rs index 616dd4c2207..2678997e55c 100644 --- a/components/datetime/src/options/mod.rs +++ b/components/datetime/src/options/mod.rs @@ -4,18 +4,72 @@ //! Options types for date/time formatting. -#[cfg(feature = "serde")] -use crate::neo_serde::*; use icu_timezone::scaffold::IntoOption; +#[cfg(all(feature = "serde", feature = "experimental"))] +use crate::neo_serde::TimePrecisionSerde; + /// The length of a formatted date/time string. /// /// Length settings are always a hint, not a guarantee. For example, certain locales and /// calendar systems do not define numeric names, so spelled-out names could occur even if a /// short length was requested, and likewise with numeric names with a medium or long length. +/// +/// # Examples +/// +/// ``` +/// use icu::calendar::Date; +/// use icu::calendar::Gregorian; +/// use icu::datetime::fieldset::YMD; +/// use icu::datetime::FixedCalendarDateTimeFormatter; +/// use icu::locale::locale; +/// use writeable::assert_try_writeable_eq; +/// +/// let short_formatter = +/// FixedCalendarDateTimeFormatter::try_new( +/// &locale!("en-US").into(), +/// YMD::short(), +/// ) +/// .unwrap(); +/// +/// let medium_formatter = +/// FixedCalendarDateTimeFormatter::try_new( +/// &locale!("en-US").into(), +/// YMD::medium(), +/// ) +/// .unwrap(); +/// +/// let long_formatter = +/// FixedCalendarDateTimeFormatter::try_new( +/// &locale!("en-US").into(), +/// YMD::long(), +/// ) +/// .unwrap(); +/// +/// assert_try_writeable_eq!( +/// short_formatter.format(&Date::try_new_gregorian(2000, 1, 1).unwrap()), +/// "1/1/00" +/// ); +/// +/// assert_try_writeable_eq!( +/// medium_formatter.format(&Date::try_new_gregorian(2000, 1, 1).unwrap()), +/// "Jan 1, 2000" +/// ); +/// +/// assert_try_writeable_eq!( +/// long_formatter.format(&Date::try_new_gregorian(2000, 1, 1).unwrap()), +/// "January 1, 2000" +/// ); +/// ``` #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))] +#[cfg_attr( + all(feature = "serde", feature = "experimental"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + all(feature = "serde", feature = "experimental"), + serde(rename_all = "lowercase") +)] #[repr(u8)] // discriminants come from symbol count in UTS 35 #[non_exhaustive] pub enum NeoSkeletonLength { @@ -35,9 +89,57 @@ impl IntoOption for NeoSkeletonLength { } /// The alignment context of the formatted string. +/// +/// By default, datetimes are formatted for a variable-width context. You can +/// give a hint that the strings will be displayed in a column-like context, +/// which will coerce numerics to be padded with zeros. +/// +/// # Examples +/// +/// ``` +/// use icu::calendar::Date; +/// use icu::calendar::Gregorian; +/// use icu::datetime::fieldset::YMD; +/// use icu::datetime::options::Alignment; +/// use icu::datetime::FixedCalendarDateTimeFormatter; +/// use icu::locale::locale; +/// use writeable::assert_try_writeable_eq; +/// +/// let plain_formatter = +/// FixedCalendarDateTimeFormatter::::try_new( +/// &locale!("en-US").into(), +/// YMD::short(), +/// ) +/// .unwrap(); +/// +/// let column_formatter = +/// FixedCalendarDateTimeFormatter::::try_new( +/// &locale!("en-US").into(), +/// YMD::short().with_alignment(Alignment::Column), +/// ) +/// .unwrap(); +/// +/// // By default, en-US does not pad the month and day with zeros. +/// assert_try_writeable_eq!( +/// plain_formatter.format(&Date::try_new_gregorian(2025, 1, 1).unwrap()), +/// "1/1/25" +/// ); +/// +/// // The column alignment option hints that they should be padded. +/// assert_try_writeable_eq!( +/// column_formatter.format(&Date::try_new_gregorian(2025, 1, 1).unwrap()), +/// "01/01/25" +/// ); +/// ``` #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))] +#[cfg_attr( + all(feature = "serde", feature = "experimental"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + all(feature = "serde", feature = "experimental"), + serde(rename_all = "lowercase") +)] #[non_exhaustive] pub enum Alignment { /// Align fields as the locale specifies them to be aligned. @@ -65,9 +167,105 @@ impl IntoOption for Alignment { } /// A specification of how to render the year and the era. +/// +/// # Examples +/// +/// ``` +/// use icu::calendar::Date; +/// use icu::calendar::Gregorian; +/// use icu::datetime::fieldset::YMD; +/// use icu::datetime::options::YearStyle; +/// use icu::datetime::FixedCalendarDateTimeFormatter; +/// use icu::locale::locale; +/// use writeable::assert_try_writeable_eq; +/// +/// let formatter = FixedCalendarDateTimeFormatter::::try_new( +/// &locale!("en-US").into(), +/// YMD::short().with_year_style(YearStyle::Auto), +/// ) +/// .unwrap(); +/// +/// // Era displayed when needed for disambiguation, +/// // such as years before year 0 and small year numbers: +/// assert_try_writeable_eq!( +/// formatter.format(&Date::try_new_gregorian(-1000, 1, 1).unwrap()), +/// "1/1/1001 BC" +/// ); +/// assert_try_writeable_eq!( +/// formatter.format(&Date::try_new_gregorian(77, 1, 1).unwrap()), +/// "1/1/77 AD" +/// ); +/// // Era elided for modern years: +/// assert_try_writeable_eq!( +/// formatter.format(&Date::try_new_gregorian(1900, 1, 1).unwrap()), +/// "1/1/1900" +/// ); +/// // Era and century both elided for nearby years: +/// assert_try_writeable_eq!( +/// formatter.format(&Date::try_new_gregorian(2025, 1, 1).unwrap()), +/// "1/1/25" +/// ); +/// +/// let formatter = FixedCalendarDateTimeFormatter::::try_new( +/// &locale!("en-US").into(), +/// YMD::short().with_year_style(YearStyle::Full), +/// ) +/// .unwrap(); +/// +/// // Era still displayed in cases with ambiguity: +/// assert_try_writeable_eq!( +/// formatter.format(&Date::try_new_gregorian(-1000, 1, 1).unwrap()), +/// "1/1/1001 BC" +/// ); +/// assert_try_writeable_eq!( +/// formatter.format(&Date::try_new_gregorian(77, 1, 1).unwrap()), +/// "1/1/77 AD" +/// ); +/// // Era elided for modern years: +/// assert_try_writeable_eq!( +/// formatter.format(&Date::try_new_gregorian(1900, 1, 1).unwrap()), +/// "1/1/1900" +/// ); +/// // But now we always get a full-precision year: +/// assert_try_writeable_eq!( +/// formatter.format(&Date::try_new_gregorian(2025, 1, 1).unwrap()), +/// "1/1/2025" +/// ); +/// +/// let formatter = FixedCalendarDateTimeFormatter::::try_new( +/// &locale!("en-US").into(), +/// YMD::short().with_year_style(YearStyle::Always), +/// ) +/// .unwrap(); +/// +/// // Era still displayed in cases with ambiguity: +/// assert_try_writeable_eq!( +/// formatter.format(&Date::try_new_gregorian(-1000, 1, 1).unwrap()), +/// "1/1/1001 BC" +/// ); +/// assert_try_writeable_eq!( +/// formatter.format(&Date::try_new_gregorian(77, 1, 1).unwrap()), +/// "1/1/77 AD" +/// ); +/// // But now it is shown even on modern years: +/// assert_try_writeable_eq!( +/// formatter.format(&Date::try_new_gregorian(1900, 1, 1).unwrap()), +/// "1/1/1900 AD" +/// ); +/// assert_try_writeable_eq!( +/// formatter.format(&Date::try_new_gregorian(2025, 1, 1).unwrap()), +/// "1/1/2025 AD" +/// ); +/// ``` #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))] +#[cfg_attr( + all(feature = "serde", feature = "experimental"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + all(feature = "serde", feature = "experimental"), + serde(rename_all = "lowercase") +)] #[non_exhaustive] pub enum YearStyle { /// Display the century and/or era when needed to disambiguate the year, @@ -113,15 +311,76 @@ impl IntoOption for YearStyle { /// A specification for how precisely to display the time of day. /// -/// The examples below are based on the following inputs and hour cycles: +/// The time can be displayed with hour, minute, or second precision, and +/// zero-valued fields can be automatically hidden. +/// +/// The examples in the discriminants are based on the following inputs and hour cycles: /// /// 1. 11 o'clock with 12-hour time /// 2. 16:20 (4:20 pm) with 24-hour time /// 3. 7:15:01.85 with 24-hour time +/// +/// # Examples +/// +/// ``` +/// use icu::calendar::Time; +/// use icu::datetime::fieldset::T; +/// use icu::datetime::options::FractionalSecondDigits; +/// use icu::datetime::options::TimePrecision; +/// use icu::datetime::FixedCalendarDateTimeFormatter; +/// use icu::locale::locale; +/// use writeable::assert_try_writeable_eq; +/// +/// let formatters = [ +/// TimePrecision::HourPlus, +/// TimePrecision::HourExact, +/// TimePrecision::MinutePlus, +/// TimePrecision::MinuteExact, +/// TimePrecision::SecondPlus, +/// TimePrecision::SecondExact(FractionalSecondDigits::F0), +/// ].map(|time_precision| { +/// FixedCalendarDateTimeFormatter::<(), _>::try_new( +/// &locale!("en-US").into(), +/// T::short().with_time_precision(time_precision), +/// ) +/// .unwrap() +/// }); +/// +/// let times = [ +/// Time::try_new(7, 0, 0, 0).unwrap(), +/// Time::try_new(7, 0, 10, 0).unwrap(), +/// Time::try_new(7, 12, 20, 5).unwrap(), +/// ]; +/// +/// // TODO(#5782): the Plus variants should render fractional digits +/// let expected_value_table = [ +/// // 7:00:00, 7:00:10, 7:12:20.5432 +/// ["7 AM", "7:00:10 AM", "7:12:20 AM"], // HourPlus +/// ["7 AM", "7 AM", "7 AM"], // HourExact +/// ["7:00 AM", "7:00:10 AM", "7:12:20 AM"], // MinutePlus +/// ["7:00 AM", "7:00 AM", "7:12 AM"], // MinuteExact +/// ["7:00:00 AM", "7:00:10 AM", "7:12:20 AM"], // SecondPlus +/// ["7:00:00 AM", "7:00:10 AM", "7:12:20 AM"], // SecondExact +/// ]; +/// +/// for (expected_value_row, formatter) in expected_value_table.iter().zip(formatters.iter()) { +/// for (expected_value, time) in expected_value_row.iter().zip(times.iter()) { +/// assert_try_writeable_eq!( +/// formatter.format(time), +/// *expected_value, +/// Ok(()), +/// "{formatter:?} @ {time:?}" +/// ); +/// } +/// } +/// ``` #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr( - feature = "serde", + all(feature = "serde", feature = "experimental"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + all(feature = "serde", feature = "experimental"), serde(from = "TimePrecisionSerde", into = "TimePrecisionSerde") )] #[non_exhaustive] @@ -192,9 +451,41 @@ impl IntoOption for TimePrecision { /// [`FractionalSecondDigits::F3`]. /// /// Lower-precision digits will be truncated. +/// +/// # Examples +/// +/// Times can be displayed with a custom number of fractional digits from 0-9: +/// +/// ``` +/// use icu::calendar::Gregorian; +/// use icu::calendar::Time; +/// use icu::datetime::fieldset::T; +/// use icu::datetime::options::FractionalSecondDigits; +/// use icu::datetime::options::TimePrecision; +/// use icu::datetime::FixedCalendarDateTimeFormatter; +/// use icu::locale::locale; +/// use writeable::assert_try_writeable_eq; +/// +/// let formatter = FixedCalendarDateTimeFormatter::<(), _>::try_new( +/// &locale!("en-US").into(), +/// T::short().with_time_precision(TimePrecision::SecondExact(FractionalSecondDigits::F2)), +/// ) +/// .unwrap(); +/// +/// assert_try_writeable_eq!( +/// formatter.format(&Time::try_new(16, 12, 20, 543200000).unwrap()), +/// "4:12:20.54 PM" +/// ); +/// ``` #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(try_from = "u8", into = "u8"))] +#[cfg_attr( + all(feature = "serde", feature = "experimental"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + all(feature = "serde", feature = "experimental"), + serde(try_from = "u8", into = "u8") +)] #[non_exhaustive] pub enum FractionalSecondDigits { /// Zero fractional digits. This is the default. diff --git a/components/datetime/src/provider/pattern/reference/pattern.rs b/components/datetime/src/provider/pattern/reference/pattern.rs index f67b4ccd537..4966b227575 100644 --- a/components/datetime/src/provider/pattern/reference/pattern.rs +++ b/components/datetime/src/provider/pattern/reference/pattern.rs @@ -2,7 +2,7 @@ // called LICENSE at the top level of the ICU4X source tree // (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). -#[cfg(feature = "serde")] +#[cfg(any(feature = "serde", test))] use super::super::runtime; use super::{ super::{PatternError, PatternItem, TimeGranularity}, @@ -35,7 +35,7 @@ impl Pattern { &mut self.items } - #[cfg(feature = "serde")] + #[cfg(any(feature = "serde", test))] pub(crate) fn to_runtime_pattern(&self) -> runtime::Pattern<'static> { runtime::Pattern::from(self) } diff --git a/components/datetime/src/provider/skeleton/helpers.rs b/components/datetime/src/provider/skeleton/helpers.rs index 5af364d817e..962d2eea112 100644 --- a/components/datetime/src/provider/skeleton/helpers.rs +++ b/components/datetime/src/provider/skeleton/helpers.rs @@ -6,10 +6,9 @@ use alloc::vec; use alloc::vec::Vec; use core::cmp::Ordering; -use super::components; use super::plural::PatternPlurals; use crate::{ - fields::{self, Field, FieldLength, FieldSymbol}, + fields::{self, components, Field, FieldLength, FieldSymbol}, options::FractionalSecondDigits, provider::{ calendar::{ diff --git a/components/datetime/src/provider/skeleton/mod.rs b/components/datetime/src/provider/skeleton/mod.rs index 0476c4eecdf..79b5f21f5ff 100644 --- a/components/datetime/src/provider/skeleton/mod.rs +++ b/components/datetime/src/provider/skeleton/mod.rs @@ -23,7 +23,7 @@ //! //! # Description //! -//! A [`components::Bag`](struct.Bag.html) is a model of encoding information on how to format date +//! A [`components::Bag`] is a model of encoding information on how to format date //! and time by specifying a list of components the user wants to be visible in the formatted string //! and how each field should be displayed. //! @@ -35,12 +35,12 @@ //! //! ## Pattern Selection //! -//! The [`components::Bag`](struct.Bag.html) is a way for the developer to describe which components +//! The [`components::Bag`] is a way for the developer to describe which components //! should be included in in a datetime, and how they should be displayed. There is not a strict //! guarantee in how the final date will be displayed to the end user. The user's preferences and //! locale information can override the developer preferences. //! -//! The fields in the [`components::Bag`](struct.Bag.html) are matched against available patterns in +//! The fields in the [`components::Bag`] are matched against available patterns in //! the `CLDR` locale data. A best fit is found, and presented to the user. This means that in //! certain situations, and component combinations, fields will not have a match, or the match will //! have a different type of presentation for a given locale. @@ -51,7 +51,9 @@ //! to be stable, their Rust representation might not be. Use with caution. //!
-pub mod components; +#[cfg(doc)] +use crate::fields::components; + mod error; mod helpers; mod plural; diff --git a/components/datetime/src/provider/skeleton/plural.rs b/components/datetime/src/provider/skeleton/plural.rs index 7d8f0353d30..9ca8fa40639 100644 --- a/components/datetime/src/provider/skeleton/plural.rs +++ b/components/datetime/src/provider/skeleton/plural.rs @@ -2,7 +2,7 @@ // called LICENSE at the top level of the ICU4X source tree // (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). -use crate::fields::{Field, FieldSymbol, Week}; +use crate::fields::{components, Field, FieldSymbol, Week}; use crate::provider::pattern::{runtime::Pattern, PatternError, PatternItem}; use either::Either; use icu_plurals::PluralCategory; @@ -118,6 +118,18 @@ pub enum PatternPlurals<'data> { SinglePattern(Pattern<'data>), } +/// Get the resolved components for a FixedCalendarDateTimeFormatter, via the PatternPlurals. In the case of +/// plurals resolve off of the required `other` pattern. +impl From<&PatternPlurals<'_>> for components::Bag { + fn from(other: &PatternPlurals) -> Self { + let pattern = match other { + PatternPlurals::SinglePattern(pattern) => pattern, + PatternPlurals::MultipleVariants(plural_pattern) => &plural_pattern.other, + }; + Self::from(pattern) + } +} + #[allow(missing_docs)] impl<'data> PatternPlurals<'data> { pub fn into_owned(self) -> PatternPlurals<'static> { diff --git a/components/datetime/src/raw/neo.rs b/components/datetime/src/raw/neo.rs index 97c3d30d4f5..caafb27f0f7 100644 --- a/components/datetime/src/raw/neo.rs +++ b/components/datetime/src/raw/neo.rs @@ -27,7 +27,7 @@ pub(crate) struct RawNeoOptions { } impl RawNeoOptions { - #[cfg(feature = "serde")] + #[cfg(all(feature = "serde", feature = "experimental"))] pub(crate) fn merge(self, other: RawNeoOptions) -> Self { Self { length: self.length, diff --git a/components/datetime/tests/datetime.rs b/components/datetime/tests/datetime.rs index 44283097c48..372fe1e18e8 100644 --- a/components/datetime/tests/datetime.rs +++ b/components/datetime/tests/datetime.rs @@ -61,6 +61,7 @@ fn test_fixture(fixture_name: &str, file: &str) { let japanext = JapaneseExtended::new(); let skeleton = match fx.input.options.semantic { Some(semantic) => { + let semantic = CompositeFieldSet::try_from(semantic).unwrap(); match CompositeDateTimeFieldSet::try_from_composite_field_set(semantic) { Some(v) => v, None => { @@ -342,7 +343,7 @@ fn test_fixture_with_time_zones(fixture_name: &str, file: &str) { .0 { let skeleton = match fx.input.options.semantic { - Some(semantic) => semantic, + Some(semantic) => CompositeFieldSet::try_from(semantic).unwrap(), None => { eprintln!("Warning: Skipping test with no semantic skeleton: {fx:?}"); continue; diff --git a/components/datetime/tests/fixtures/mod.rs b/components/datetime/tests/fixtures/mod.rs index d229531d630..85a55f59591 100644 --- a/components/datetime/tests/fixtures/mod.rs +++ b/components/datetime/tests/fixtures/mod.rs @@ -4,8 +4,8 @@ #![cfg(feature = "serde")] -use icu_datetime::options; -use icu_datetime::provider::skeleton::components; +use icu_datetime::fields::components; +use icu_datetime::{fieldset::serde::CompositeFieldSetSerde, options}; use icu_locale_core::preferences::extensions::unicode::keywords::HourCycle; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -30,7 +30,7 @@ pub struct TestInput { pub struct TestOptions { pub length: Option, pub components: Option, - pub semantic: Option, + pub semantic: Option, #[serde(rename = "hourCycle")] pub hour_cycle: Option, } diff --git a/components/datetime/tests/resolved_components.rs b/components/datetime/tests/resolved_components.rs index 8fa930a9d16..b53f6b2ef76 100644 --- a/components/datetime/tests/resolved_components.rs +++ b/components/datetime/tests/resolved_components.rs @@ -4,12 +4,12 @@ use icu_calendar::{Date, DateTime, Gregorian, Time}; use icu_datetime::{ + fields::components, fieldset::{ self, dynamic::{CompositeDateTimeFieldSet, DateAndTimeFieldSet, DateFieldSet, TimeFieldSet}, }, options::{Alignment, FractionalSecondDigits, TimePrecision, YearStyle}, - provider::skeleton::components, FixedCalendarDateTimeFormatter, }; use icu_locale_core::Locale; diff --git a/provider/source/src/datetime/neo_skeleton.rs b/provider/source/src/datetime/neo_skeleton.rs index c71b30bd572..9f76c41fd79 100644 --- a/provider/source/src/datetime/neo_skeleton.rs +++ b/provider/source/src/datetime/neo_skeleton.rs @@ -6,11 +6,11 @@ use std::collections::HashSet; use crate::{IterableDataProviderCached, SourceDataProvider}; use either::Either; +use icu::datetime::fields::components; use icu::datetime::fieldset::dynamic::*; use icu::datetime::options::NeoSkeletonLength; use icu::datetime::provider::calendar::{DateLengthsV1, DateSkeletonPatternsV1, TimeLengthsV1}; use icu::datetime::provider::pattern::runtime; -use icu::datetime::provider::skeleton::components; use icu::datetime::provider::skeleton::PatternPlurals; use icu::datetime::provider::*; use icu::locale::extensions::unicode::{value, Value}; diff --git a/provider/source/src/datetime/skeletons.rs b/provider/source/src/datetime/skeletons.rs index f755e65243d..f04ebd6e6be 100644 --- a/provider/source/src/datetime/skeletons.rs +++ b/provider/source/src/datetime/skeletons.rs @@ -80,6 +80,7 @@ mod test { use core::convert::TryFrom; use core::str::FromStr; use either::Either; + use icu::datetime::fields::components; use icu::datetime::provider::skeleton::reference::Skeleton; use icu::datetime::provider::skeleton::*; use icu::datetime::{ diff --git a/tools/make/diplomat-coverage/src/allowlist.rs b/tools/make/diplomat-coverage/src/allowlist.rs index 9bc0156f757..bd63cd84089 100644 --- a/tools/make/diplomat-coverage/src/allowlist.rs +++ b/tools/make/diplomat-coverage/src/allowlist.rs @@ -188,6 +188,8 @@ lazy_static::lazy_static! { "icu::datetime::fields", // experimental + "icu::datetime::fields::components", + "icu::datetime::fieldset::serde", "icu::datetime::neo_pattern", "icu::datetime::neo_skeleton", "icu::datetime::options::components",