diff --git a/Cargo.lock b/Cargo.lock index c858d22303d..72d0d93b63f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1369,6 +1369,7 @@ dependencies = [ "iai", "icu", "icu_benchmark_macros", + "icu_provider", "litemap", "postcard", "potential_utf", diff --git a/components/locale_core/Cargo.toml b/components/locale_core/Cargo.toml index 596eb74d440..a1f3b9af6da 100644 --- a/components/locale_core/Cargo.toml +++ b/components/locale_core/Cargo.toml @@ -25,15 +25,16 @@ litemap = { workspace = true, features = ["alloc"] } tinystr = { workspace = true, features = ["alloc"] } writeable = { workspace = true } -databake = { workspace = true, features = ["derive"], optional = true} +databake = { workspace = true, features = ["derive"], optional = true } serde = { workspace = true, features = ["alloc", "derive"], optional = true } zerovec = { workspace = true, optional = true } [dev-dependencies] iai = { workspace = true } icu = { path = "../../components/icu", default-features = false } +icu_provider = { workspace = true } icu_benchmark_macros = { path = "../../tools/benchmark/macros" } -litemap = { path = "../../utils/litemap", features = ["testing"]} +litemap = { path = "../../utils/litemap", features = ["testing"] } postcard = { workspace = true, features = ["use-std"] } potential_utf = { workspace = true } serde = { workspace = true, features = ["derive"] } @@ -51,7 +52,7 @@ zerovec = ["dep:zerovec", "tinystr/zerovec"] bench = ["serde"] [lib] -bench = false # This option is required for Benchmark CI +bench = false # This option is required for Benchmark CI [package.metadata.cargo-all-features] # Bench feature gets tested separately and is only relevant for CI diff --git a/components/locale_core/src/preferences/mod.rs b/components/locale_core/src/preferences/mod.rs index cde4e19927e..6420e914b54 100644 --- a/components/locale_core/src/preferences/mod.rs +++ b/components/locale_core/src/preferences/mod.rs @@ -11,44 +11,6 @@ //! The crate is intended primarily to be used by components constructors to normalize the format //! of ingesting preferences across all of [`ICU4X`]. //! -//! # Examples: -//! -//! ``` -//! use icu::locale::preferences::{ -//! preferences, -//! extensions::unicode::keywords::HourCycle, -//! }; -//! use icu::locale::LanguageIdentifier; -//! -//! pub fn get_defaults(lid: &Option) -> ExampleComponentResolvedPreferences { -//! unimplemented!() -//! } -//! -//! preferences!( -//! ExampleComponentPreferences, -//! ExampleComponentResolvedPreferences, -//! { -//! hour_cycle: HourCycle -//! } -//! ); -//! -//! pub struct ExampleComponent { -//! resolved_prefs: ExampleComponentResolvedPreferences, -//! } -//! -//! impl ExampleComponent { -//! pub fn new(prefs: ExampleComponentPreferences) -> Self { -//! // Retrieve the default values for the given [`LanguageIdentifier`]. -//! let mut resolved_prefs = get_defaults(&prefs.lid); -//! -//! // Resolve them against provided preferences. -//! resolved_prefs.extend(prefs); -//! -//! Self { resolved_prefs } -//! } -//! } -//! ``` -//! //! [`ICU4X`]: ../icu/index.html //! [`Locale`]: crate::Locale @@ -88,7 +50,6 @@ macro_rules! __preferences { ( $(#[$doc:meta])* $name:ident, - $resolved_name:ident, { $( $(#[$key_doc:meta])* @@ -100,33 +61,23 @@ macro_rules! __preferences { $(#[$doc])* #[non_exhaustive] pub struct $name { - #[doc = concat!("The locale that these `", stringify!($name), "` use.")] - pub lid: Option<$crate::LanguageIdentifier>, - $( - $(#[$key_doc])* - pub $key: Option<$pref>, - )* - } - - #[non_exhaustive] - #[derive(Debug, Clone)] - #[doc = concat!("The resolved version of `", stringify!($name), "`.")] - pub struct $resolved_name { - #[doc = concat!("The locale that these `", stringify!($name), "` use.")] - pub lid: $crate::LanguageIdentifier, + pub(crate) language: $crate::subtags::Language, + pub(crate) script: Option<$crate::subtags::Script>, + pub(crate) region: Option<$crate::subtags::Region>, + pub(crate) variant: Option<$crate::subtags::Variant>, + pub(crate) subdivision: Option<$crate::subtags::Subtag>, + pub(crate) ue_region: Option<$crate::subtags::Region>, $( $(#[$key_doc])* - pub $key: $pref, + pub $key: Option<$pref>, )* } - impl From<$crate::Locale> for $name { - fn from(loc: $crate::Locale) -> Self { + impl From<&$crate::Locale> for $name { + fn from(loc: &$crate::Locale) -> Self { use $crate::preferences::PreferenceKey; - let lid = Some(loc.id); - $( let mut $key = None; )* @@ -143,8 +94,27 @@ macro_rules! __preferences { )* } + let sd = loc + .extensions + .unicode + .keywords + .get(&$crate::extensions::unicode::key!("sd")) + .and_then(|v| v.as_single_subtag().copied()); + let ue_region = loc + .extensions + .unicode + .keywords + .get(&$crate::extensions::unicode::key!("rg")) + .and_then(|v| v.as_single_subtag() + .and_then(|s| $crate::subtags::Region::try_from_str(s.as_str()).ok())); Self { - lid, + language: loc.id.language, + script: loc.id.script, + region: loc.id.region, + variant: loc.id.variants.iter().copied().next(), + subdivision: sd, + ue_region, + $( $key, )* @@ -152,23 +122,63 @@ macro_rules! __preferences { } } + impl From<&$crate::LanguageIdentifier> for $name { + fn from(lid: &$crate::LanguageIdentifier) -> Self { + Self { + language: lid.language, + script: lid.script, + region: lid.region, + variant: lid.variants.iter().copied().next(), + subdivision: None, + ue_region: None, + + $( + $key: None, + )* + } + } + } + impl $name { /// Constructs a `Locale` corresponding to these preferences. pub fn into_locale(self) -> $crate::Locale { use $crate::preferences::PreferenceKey; - let id = self.lid.unwrap_or_default(); - let mut extensions = $crate::extensions::Extensions::new(); - $( - if let Some(value) = &self.$key { - if let Some(ue) = <$pref>::unicode_extension_key() { - let val = value.unicode_extension_value().unwrap(); - extensions.unicode.keywords.set(ue, val); - } - } - )* $crate::Locale { - id, - extensions + id: $crate::LanguageIdentifier { + language: self.language, + script: self.script, + region: self.region, + variants: self + .variant + .map($crate::subtags::Variants::from_variant) + .unwrap_or_default(), + }, + extensions: { + let mut extensions = $crate::extensions::Extensions::default(); + if self.subdivision.is_some() || self.ue_region.is_some() { + if let Some(sd) = self.subdivision { + extensions.unicode.keywords.set( + $crate::extensions::unicode::key!("sd"), + $crate::extensions::unicode::Value::from_subtag(Some(sd)) + ); + } + if let Some(rg) = self.ue_region { + extensions.unicode.keywords.set( + $crate::extensions::unicode::key!("rg"), + $crate::extensions::unicode::Value::try_from_str(rg.as_str()).unwrap() + ); + } + } + $( + if let Some(value) = &self.$key { + if let Some(ue) = <$pref>::unicode_extension_key() { + let val = value.unicode_extension_value().unwrap(); + extensions.unicode.keywords.set(ue, val); + } + } + )* + extensions + }, } } @@ -193,17 +203,6 @@ macro_rules! __preferences { )* } } - - impl $resolved_name { - /// TODO - pub fn extend(&mut self, prefs: $name) { - $( - if let Some(v) = prefs.$key { - self.$key = v; - } - )* - } - } ) } #[doc(inline)] @@ -226,7 +225,6 @@ mod tests { preferences!( /// Preferences for the dummy formatter DummyPreferences, - DummyResolvedPreferences, { /// Controls how dummyly the formatter behaves dummy_keyword: DummyKeyword @@ -235,7 +233,7 @@ mod tests { let loc: Locale = "und-u-ab-default-cd-foo".parse().unwrap(); - let prefs = DummyPreferences::from(loc); + let prefs = DummyPreferences::from(&loc); assert_eq!(prefs.dummy_keyword, Some(DummyKeyword::Default)); } } diff --git a/components/locale_core/src/preferences/options.rs b/components/locale_core/src/preferences/options.rs index 9b75d1bbc6c..9058938b818 100644 --- a/components/locale_core/src/preferences/options.rs +++ b/components/locale_core/src/preferences/options.rs @@ -7,10 +7,9 @@ #[doc(hidden)] macro_rules! __options { ($name:ident, - $resolved_name:ident, {$($key:ident => $pref:ty),*} ) => ( - #[derive(Default, Debug, PartialEq)] + #[derive(Default, Debug, Clone)] #[non_exhaustive] pub struct $name { $( @@ -18,14 +17,6 @@ macro_rules! __options { )* } - #[non_exhaustive] - #[derive(Debug, PartialEq)] - pub struct $resolved_name { - $( - pub $key: $pref, - )* - } - impl $name { pub fn extend(&mut self, other: $name) { $( diff --git a/components/locale_core/tests/dtf/data_provider.rs b/components/locale_core/tests/dtf/data_provider.rs deleted file mode 100644 index 847d458a248..00000000000 --- a/components/locale_core/tests/dtf/data_provider.rs +++ /dev/null @@ -1,52 +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 ). - -use super::*; -use icu_locale_core::preferences::extensions::unicode::keywords; -use icu_locale_core::{langid, subtags::subtag, LanguageIdentifier}; -use tinystr::tinystr; - -struct DefaultPrefs { - pub und: DateTimeFormatResolvedPreferences, - pub list: &'static [DateTimeFormatResolvedPreferences], -} - -const DEFAULT_PREFS: DefaultPrefs = DefaultPrefs { - und: DateTimeFormatResolvedPreferences { - lid: LanguageIdentifier::default(), - hour_cycle: keywords::HourCycle::H23, - calendar: keywords::CalendarAlgorithm::Gregory, - numbering_system: keywords::NumberingSystem(subtag!("latn")), - date_pattern: DatePattern(tinystr!(8, "Y-m-d")), - }, - list: &[DateTimeFormatResolvedPreferences { - lid: langid!("en-US"), - hour_cycle: keywords::HourCycle::H12, - calendar: keywords::CalendarAlgorithm::Gregory, - numbering_system: keywords::NumberingSystem(subtag!("latn")), - date_pattern: DatePattern(tinystr!(8, "m/d/Y")), - }], -}; - -pub fn get_default_prefs(lid: &Option) -> DateTimeFormatResolvedPreferences { - lid.as_ref() - .and_then(|lid| { - DEFAULT_PREFS - .list - .iter() - .find(|dtfrp| dtfrp.lid.language == lid.language) - }) - .cloned() - .unwrap_or(DEFAULT_PREFS.und) -} - -pub fn resolve_options(options: &DateTimeFormatOptions) -> DateTimeFormatResolvedOptions { - DateTimeFormatResolvedOptions { - date_length: options.date_length.unwrap_or(DateLength::Short), - time_length: options.time_length.unwrap_or(TimeLength::Short), - day_period: DayPeriod::Short, - locale_matcher: LocaleMatcher::BestFit, - time_zone: options.time_zone.unwrap_or(false), - } -} diff --git a/components/locale_core/tests/dtf/mod.rs b/components/locale_core/tests/dtf/mod.rs deleted file mode 100644 index ee0ba9bd269..00000000000 --- a/components/locale_core/tests/dtf/mod.rs +++ /dev/null @@ -1,100 +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 ). - -mod data_provider; -mod options; - -use data_provider::{get_default_prefs, resolve_options}; -use icu_locale_core::extensions::unicode; -use icu_locale_core::preferences::{ - extensions::unicode::{errors::PreferencesParseError, keywords}, - options, preferences, PreferenceKey, -}; -use options::{DayPeriod, LocaleMatcher}; -use tinystr::TinyAsciiStr; - -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub struct DatePattern(pub TinyAsciiStr<8>); - -impl PreferenceKey for DatePattern {} - -impl TryFrom for DatePattern { - type Error = PreferencesParseError; - - fn try_from(_: unicode::Value) -> Result { - Err(Self::Error::UnknownKeyword) - } -} - -preferences!( - /// The locale preferences for datetime formatting. - DateTimeFormatPreferences, - DateTimeFormatResolvedPreferences, - { - /// The hour cycle - hour_cycle: keywords::HourCycle, - /// The calendar - calendar: keywords::CalendarAlgorithm, - /// The numbering system - numbering_system: keywords::NumberingSystem, - /// The date pattern - date_pattern: DatePattern - } -); - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -#[allow(dead_code)] -pub enum DateLength { - Full, - Long, - Medium, - Short, -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -#[allow(dead_code)] -pub enum TimeLength { - Full, - Long, - Medium, - Short, -} - -options!( - DateTimeFormatOptions, - DateTimeFormatResolvedOptions, - { - date_length => DateLength, - time_length => TimeLength, - day_period => DayPeriod, - locale_matcher => LocaleMatcher, - time_zone => bool - } -); - -pub struct DateTimeFormat { - prefs: DateTimeFormatResolvedPreferences, - options: DateTimeFormatResolvedOptions, -} - -impl DateTimeFormat { - pub fn new(prefs: DateTimeFormatPreferences, options: DateTimeFormatOptions) -> Self { - let mut resolved = get_default_prefs(&prefs.lid); - - resolved.extend(prefs); - - Self { - prefs: resolved, - options: resolve_options(&options), - } - } - - pub fn resolved_preferences(&self) -> &DateTimeFormatResolvedPreferences { - &self.prefs - } - - pub fn resolved_options(&self) -> &DateTimeFormatResolvedOptions { - &self.options - } -} diff --git a/components/locale_core/tests/dtf/options.rs b/components/locale_core/tests/dtf/options.rs deleted file mode 100644 index 0522cbd1195..00000000000 --- a/components/locale_core/tests/dtf/options.rs +++ /dev/null @@ -1,25 +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 ). - -#[derive(Debug, PartialEq)] -pub enum DayPeriod { - Short, -} - -impl Default for DayPeriod { - fn default() -> Self { - Self::Short - } -} - -#[derive(Debug, PartialEq)] -pub enum LocaleMatcher { - BestFit, -} - -impl Default for LocaleMatcher { - fn default() -> Self { - Self::BestFit - } -} diff --git a/components/locale_core/tests/dtf_os_prefs_tests.rs b/components/locale_core/tests/dtf_os_prefs_tests.rs deleted file mode 100644 index aa5a381fc8c..00000000000 --- a/components/locale_core/tests/dtf_os_prefs_tests.rs +++ /dev/null @@ -1,166 +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 ). - -mod dtf; - -use icu_locale_core::preferences::extensions::unicode::keywords; -use icu_locale_core::{locale, LanguageIdentifier}; - -use dtf::*; - -fn get_os_dtf_preferences(lid: &LanguageIdentifier) -> Option { - // This optionally may different - Some(dtf::DateTimeFormatPreferences { - lid: Some(lid.clone()), - hour_cycle: Some(keywords::HourCycle::H23), - date_pattern: Some(DatePattern(tinystr::tinystr!(8, "d.m.Y"))), - ..Default::default() - }) -} - -fn get_os_dtf_options() -> Option { - Some(dtf::DateTimeFormatOptions { - date_length: Some(DateLength::Long), - time_zone: Some(true), - ..Default::default() - }) -} - -// In this scenario we showcase retrieval of OS regional preferences. -// The result chain is: OS > Locale > Defaults. -#[test] -fn dtf_get_os_prefs() { - let loc = locale!("en-US"); - - let os_prefs = get_os_dtf_preferences(&loc.id); - let mut prefs = DateTimeFormatPreferences::from(loc); - if let Some(os_prefs) = os_prefs { - prefs.extend(os_prefs); - } - - let dtf = DateTimeFormat::new(prefs, Default::default()); - - assert_eq!( - dtf.resolved_preferences().hour_cycle, - keywords::HourCycle::H23 - ); -} - -// In this scenario we showcase retrieval of OS regional preferences. -// The priority is in locale unicode extension overriding OS preferences. -// The result chain is: Locale > OS > Defaults. -#[test] -fn dtf_locale_override_os_prefs() { - let loc = locale!("en-US-u-hc-h11"); - - let os_prefs = get_os_dtf_preferences(&loc.id); - let prefs = if let Some(mut os_prefs) = os_prefs { - os_prefs.extend(loc.into()); - os_prefs - } else { - loc.into() - }; - - let dtf = DateTimeFormat::new(prefs, Default::default()); - - assert_eq!( - dtf.resolved_preferences().hour_cycle, - keywords::HourCycle::H11 - ); -} - -// In this scenario we showcase retrieval of OS regional preferences. -// The priority is in OS preferences overriding locale unicode extension. -// The result chain is: OS > Locale > Defaults. -#[test] -fn dtf_os_prefs_override_locale() { - let loc = locale!("en-US-u-hc-h11"); - - let os_prefs = get_os_dtf_preferences(&loc.id); - let mut prefs = DateTimeFormatPreferences::from(loc); - if let Some(os_prefs) = os_prefs { - prefs.extend(os_prefs); - } - - let dtf = DateTimeFormat::new(prefs, Default::default()); - - assert_eq!( - dtf.resolved_preferences().hour_cycle, - keywords::HourCycle::H23 - ); -} - -// In this scenario we showcase retrieval of OS regional preferences, -// preferences bag, and locale. -// The result chain is: Bag > Locale > OS > Defaults. -#[test] -fn dtf_call_override_locale_override_os_prefs() { - let loc = locale!("en-US-u-hc-h11"); - - let os_prefs = get_os_dtf_preferences(&loc.id); - let mut prefs = if let Some(mut os_prefs) = os_prefs { - os_prefs.extend(loc.into()); - os_prefs - } else { - loc.into() - }; - - let bag = DateTimeFormatPreferences { - hour_cycle: Some(keywords::HourCycle::H24), - ..Default::default() - }; - - prefs.extend(bag); - - let dtf = DateTimeFormat::new(prefs, Default::default()); - - assert_eq!( - dtf.resolved_preferences().hour_cycle, - keywords::HourCycle::H24 - ); -} - -#[test] -fn dtf_options_override_os_options() { - let loc = locale!("en"); - - let dev_options = DateTimeFormatOptions { - date_length: Some(DateLength::Medium), - ..Default::default() - }; - - let options = if let Some(mut os_options) = get_os_dtf_options() { - os_options.extend(dev_options); - os_options - } else { - dev_options - }; - - let dtf = DateTimeFormat::new(loc.into(), options); - - // This is taken from dev options - assert_eq!(dtf.resolved_options().date_length, DateLength::Medium); - - // Dev didn't specify time zone field presence, so this is taken from os_prefs - assert!(dtf.resolved_options().time_zone); -} - -#[test] -fn dtf_prefs_non_ue_preference() { - let loc = locale!("en-US"); - - let os_prefs = get_os_dtf_preferences(&loc.id); - let prefs = if let Some(mut os_prefs) = os_prefs { - os_prefs.extend(loc.into()); - os_prefs - } else { - loc.into() - }; - - let dtf = DateTimeFormat::new(prefs, Default::default()); - assert_eq!( - dtf.resolved_preferences().date_pattern, - DatePattern(tinystr::tinystr!(8, "d.m.Y")) - ); -} diff --git a/components/locale_core/tests/dtf_tests.rs b/components/locale_core/tests/dtf_tests.rs deleted file mode 100644 index 01e048b1bbc..00000000000 --- a/components/locale_core/tests/dtf_tests.rs +++ /dev/null @@ -1,216 +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 ). - -mod dtf; - -use icu_locale_core::preferences::extensions::unicode::keywords; -use icu_locale_core::{ - locale, - subtags::{language, region}, - Locale, -}; -use tinystr::tinystr; - -use dtf::*; - -// In this scenario, the locale is the only source of preferences -// and since it's empty, the defaults for the resolved locale will be taken. -// The result chain is: Defaults. -#[test] -fn dtf_default() { - let loc = locale!("en-US"); - - let dtf = DateTimeFormat::new(loc.into(), Default::default()); - - assert_eq!( - dtf.resolved_preferences().hour_cycle, - keywords::HourCycle::H12 - ); -} - -// In this scenario, we resolve the locale, and then apply the regional -// preferences from unicode extensions of the Locale on top of it. -// The result chain is: Locale > Defaults. -#[test] -fn dtf_uext() { - let loc: Locale = "en-US-u-hc-h11".parse().unwrap(); - let dtf = DateTimeFormat::new(loc.into(), Default::default()); - - assert_eq!( - dtf.resolved_preferences().hour_cycle, - keywords::HourCycle::H11 - ); -} - -// In this scenario, we will take the preferences bag, and extend -// the preferences from the locale with it. -// The result chain is: Bag > Locale > Defaults. -#[test] -fn dtf_prefs() { - let loc: Locale = "en-US-u-hc-h11".parse().unwrap(); - - let bag = DateTimeFormatPreferences { - hour_cycle: Some(keywords::HourCycle::H24), - ..Default::default() - }; - let mut prefs = DateTimeFormatPreferences::from(loc); - prefs.extend(bag); - - let dtf = DateTimeFormat::new(prefs, Default::default()); - assert_eq!( - dtf.resolved_preferences().hour_cycle, - keywords::HourCycle::H24 - ); - assert_eq!( - dtf.resolved_preferences().calendar, - keywords::CalendarAlgorithm::Gregory, - ); -} - -// In this scenario we showcase two preferences in locale extensions, -// and one of them overridden in the preferences bag. -// The result chain is: Bag > Locale > Defaults. -#[test] -fn dtf_prefs_with_ca() { - let loc: Locale = "en-US-u-hc-h11-ca-buddhist".parse().unwrap(); - let bag = DateTimeFormatPreferences { - hour_cycle: Some(keywords::HourCycle::H24), - ..Default::default() - }; - let mut prefs = DateTimeFormatPreferences::from(loc); - prefs.extend(bag); - - let dtf = DateTimeFormat::new(prefs, Default::default()); - assert_eq!( - dtf.resolved_preferences().hour_cycle, - keywords::HourCycle::H24 - ); - assert_eq!( - dtf.resolved_preferences().calendar, - keywords::CalendarAlgorithm::Buddhist, - ); -} - -// In this scenario we pass `en` but resolve to `en-US`. -#[test] -fn dtf_prefs_default_region() { - let loc: Locale = "en-u-hc-h12".parse().unwrap(); - let dtf = DateTimeFormat::new(loc.into(), Default::default()); - assert_eq!(dtf.resolved_preferences().lid.language, language!("en")); - assert_eq!(dtf.resolved_preferences().lid.region, Some(region!("US"))); - assert_eq!( - dtf.resolved_preferences().hour_cycle, - keywords::HourCycle::H12 - ); -} - -#[test] -fn dtf_options_default() { - let loc: Locale = "en".parse().unwrap(); - - let options = DateTimeFormatOptions { - ..Default::default() - }; - let dtf = DateTimeFormat::new(loc.into(), options); - assert_eq!(dtf.resolved_options().date_length, DateLength::Short); -} - -#[test] -fn dtf_options_manual() { - let loc: Locale = "en".parse().unwrap(); - - let options = DateTimeFormatOptions { - date_length: Some(DateLength::Medium), - ..Default::default() - }; - let dtf = DateTimeFormat::new(loc.into(), options); - assert_eq!(dtf.resolved_options().date_length, DateLength::Medium); -} - -#[test] -fn dtf_prefs_unknown_ue_keu() { - let loc: Locale = "en-u-bb-h99".parse().unwrap(); - let dtf = DateTimeFormat::new(loc.into(), Default::default()); - assert_eq!(dtf.resolved_preferences().lid.language, language!("en")); - assert_eq!(dtf.resolved_preferences().lid.region, Some(region!("US"))); - assert_eq!( - dtf.resolved_preferences().hour_cycle, - keywords::HourCycle::H12 - ); -} - -#[test] -fn dtf_prefs_unknown_ue_value() { - let loc: Locale = "en-u-hc-h99".parse().unwrap(); - let dtf = DateTimeFormat::new(loc.into(), Default::default()); - assert_eq!(dtf.resolved_preferences().lid.language, language!("en")); - assert_eq!(dtf.resolved_preferences().lid.region, Some(region!("US"))); - assert_eq!( - dtf.resolved_preferences().hour_cycle, - keywords::HourCycle::H12 - ); -} - -#[test] -fn dtf_prefs_non_ue_preference() { - let loc: Locale = "en-US".parse().unwrap(); - let dtf = DateTimeFormat::new(loc.into(), Default::default()); - assert_eq!(dtf.resolved_preferences().lid.language, language!("en")); - assert_eq!(dtf.resolved_preferences().lid.region, Some(region!("US"))); - assert_eq!( - dtf.resolved_preferences().date_pattern, - DatePattern(tinystr!(8, "m/d/Y")) - ); -} - -#[test] -fn dtf_prefs_unknown_ue_value_skipped() { - let loc: Locale = "en-u-hc-h99".parse().unwrap(); - let prefs: DateTimeFormatPreferences = loc.into(); - - assert_eq!(prefs.hour_cycle, None); -} - -#[test] -fn dtf_prefs_into_locale() { - let loc: Locale = "en-u-hc-h23-ca-buddhist".parse().unwrap(); - let prefs: DateTimeFormatPreferences = loc.into(); - let loc2 = prefs.into_locale(); - - assert_eq!(loc2.to_string(), "en-u-ca-buddhist-hc-h23"); -} - -#[test] -fn dtf_prefs_ca_islamic() { - use icu_locale_core::preferences::extensions::unicode::keywords; - - let loc: Locale = "en-u-ca-islamic".parse().unwrap(); - let prefs: DateTimeFormatPreferences = loc.into(); - assert_eq!( - prefs.calendar, - Some(keywords::CalendarAlgorithm::Islamic(None)) - ); - let loc2 = prefs.into_locale(); - assert_eq!(loc2.to_string(), "en-u-ca-islamic"); - - let loc: Locale = "en-u-ca-islamic-civil".parse().unwrap(); - let prefs: DateTimeFormatPreferences = loc.into(); - assert_eq!( - prefs.calendar, - Some(keywords::CalendarAlgorithm::Islamic(Some( - keywords::IslamicCalendarAlgorithm::Civil - ))) - ); - let loc2 = prefs.into_locale(); - assert_eq!(loc2.to_string(), "en-u-ca-islamic-civil"); - - let loc: Locale = "en-u-ca-islamic-foo".parse().unwrap(); - let prefs: DateTimeFormatPreferences = loc.into(); - assert_eq!( - prefs.calendar, - Some(keywords::CalendarAlgorithm::Islamic(None)) - ); - let loc2 = prefs.into_locale(); - assert_eq!(loc2.to_string(), "en-u-ca-islamic"); -} diff --git a/components/locale_core/tests/preferences/mod.rs b/components/locale_core/tests/preferences/mod.rs new file mode 100644 index 00000000000..fc0c53bfc68 --- /dev/null +++ b/components/locale_core/tests/preferences/mod.rs @@ -0,0 +1,5 @@ +// 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 ). + +pub mod time_formatter; diff --git a/components/locale_core/tests/preferences/time_formatter/data_provider.rs b/components/locale_core/tests/preferences/time_formatter/data_provider.rs new file mode 100644 index 00000000000..235a8b802e8 --- /dev/null +++ b/components/locale_core/tests/preferences/time_formatter/data_provider.rs @@ -0,0 +1,43 @@ +// This file is part of ICU4X. For terms of use, please see the file +// called LICENSE at the top level of the ICU4X source tree +// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). + +use icu_locale_core::{preferences::extensions::unicode::keywords, subtags::language}; +use icu_provider::{DataIdentifierBorrowed, DataLocale}; + +#[derive(Clone, Debug)] +pub struct TimeLocaleData { + pub data_locale: DataLocale, + pub hour_cycle: keywords::HourCycle, + pub time_format: &'static str, + pub time_format_ampm: &'static str, +} + +struct TimeData { + pub und: TimeLocaleData, + pub list: &'static [TimeLocaleData], +} + +const TF_DATA: TimeData = TimeData { + und: TimeLocaleData { + data_locale: DataLocale::default(), + hour_cycle: keywords::HourCycle::H23, + time_format: "(und) [0]:00", + time_format_ampm: "(und) [0]:00 [1]", + }, + list: &[TimeLocaleData { + data_locale: DataLocale::from_subtags(language!("en"), None, None, None, None), + hour_cycle: keywords::HourCycle::H12, + time_format: "(en) [0]:00", + time_format_ampm: "(en) [0]:00 [1]", + }], +}; + +pub fn get_payload(di: DataIdentifierBorrowed) -> TimeLocaleData { + for item in TF_DATA.list { + if item.data_locale.language == di.locale.language { + return item.clone(); + } + } + TF_DATA.und.clone() +} diff --git a/components/locale_core/tests/preferences/time_formatter/ecma402.rs b/components/locale_core/tests/preferences/time_formatter/ecma402.rs new file mode 100644 index 00000000000..b7fb3198128 --- /dev/null +++ b/components/locale_core/tests/preferences/time_formatter/ecma402.rs @@ -0,0 +1,62 @@ +// This file is part of ICU4X. For terms of use, please see the file +// called LICENSE at the top level of the ICU4X source tree +// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). + +use icu_locale_core::{preferences::extensions::unicode::keywords::HourCycle, Locale}; + +use super::{ + data_provider::{get_payload, TimeLocaleData}, + options::{TimeFormatterOptions, TimeStyle}, + preferences::TimeFormatterPreferences, + TimeFormatter as ICU4XTimeFormatter, +}; + +pub struct TimeFormatter { + dtf: ICU4XTimeFormatter, + ecma402_resolved_options: ECMA402TimeFormatterResolvedOptions, +} + +impl TimeFormatter { + pub fn new(prefs: TimeFormatterPreferences, options: TimeFormatterOptions) -> Self { + let di = ICU4XTimeFormatter::get_data_identifier(&prefs); + let data = get_payload(di.as_borrowed()); + + let ecma402_resolved_options = + ECMA402TimeFormatterResolvedOptions::from(&data, &prefs, &options); + + let dtf = ICU4XTimeFormatter::new_with_data(data, prefs, options); + + Self { + dtf, + ecma402_resolved_options, + } + } + + pub fn format_to_string(&self, input: usize) -> String { + self.dtf.format_to_string(input) + } + + pub fn resolved_options(&self) -> &ECMA402TimeFormatterResolvedOptions { + &self.ecma402_resolved_options + } +} + +pub struct ECMA402TimeFormatterResolvedOptions { + pub locale: Locale, + pub hour_cycle: HourCycle, + pub time_style: TimeStyle, +} + +impl ECMA402TimeFormatterResolvedOptions { + pub fn from( + data: &TimeLocaleData, + prefs: &TimeFormatterPreferences, + options: &TimeFormatterOptions, + ) -> Self { + Self { + locale: data.data_locale.clone().into_locale(), + hour_cycle: prefs.hour_cycle.unwrap_or(data.hour_cycle), + time_style: options.time_style.unwrap_or_default(), + } + } +} diff --git a/components/locale_core/tests/preferences/time_formatter/mod.rs b/components/locale_core/tests/preferences/time_formatter/mod.rs new file mode 100644 index 00000000000..7aa0d6f311e --- /dev/null +++ b/components/locale_core/tests/preferences/time_formatter/mod.rs @@ -0,0 +1,71 @@ +// 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 ). + +mod data_provider; +pub mod ecma402; +pub mod options; +mod preferences; + +use data_provider::{get_payload, TimeLocaleData}; +use icu_locale_core::preferences::extensions::unicode::keywords::HourCycle; +use icu_provider::{DataIdentifierCow, DataLocale}; +pub use options::TimeFormatterOptions; +pub use preferences::TimeFormatterPreferences; + +pub struct TimeFormatter { + data: TimeLocaleData, + raw_options: TFRawOptions, +} + +impl TimeFormatter { + pub fn new(prefs: TimeFormatterPreferences, options: TimeFormatterOptions) -> Self { + let di = Self::get_data_identifier(&prefs); + let data = get_payload(di.as_borrowed()); + + Self::new_with_data(data, prefs, options) + } + + pub fn new_with_data( + data: TimeLocaleData, + prefs: TimeFormatterPreferences, + options: TimeFormatterOptions, + ) -> Self { + let raw_options = TFRawOptions::from(&prefs, &options); + Self { data, raw_options } + } + + pub fn format_to_string(&self, input: usize) -> String { + use icu_locale_core::preferences::extensions::unicode::keywords::HourCycle; + + let hour_cycle = self.raw_options.hour_cycle.unwrap_or(self.data.hour_cycle); + match hour_cycle { + HourCycle::H11 | HourCycle::H12 => self + .data + .time_format_ampm + .replace("[0]", &input.to_string()) + .replace("[1]", "am"), + _ => self.data.time_format.replace("[0]", &input.to_string()), + } + } + pub(crate) fn get_data_identifier(prefs: &TimeFormatterPreferences) -> DataIdentifierCow { + let locale = + DataLocale::from_subtags(prefs.language, prefs.script, prefs.region, None, None); + DataIdentifierCow::from_locale(locale) + } +} + +#[derive(Debug, Clone)] +pub(crate) struct TFRawOptions { + pub(crate) hour_cycle: Option, + pub(crate) time_style: Option, +} + +impl TFRawOptions { + pub fn from(prefs: &TimeFormatterPreferences, options: &TimeFormatterOptions) -> Self { + Self { + hour_cycle: prefs.hour_cycle, + time_style: options.time_style, + } + } +} diff --git a/components/locale_core/tests/preferences/time_formatter/options.rs b/components/locale_core/tests/preferences/time_formatter/options.rs new file mode 100644 index 00000000000..b103a39193e --- /dev/null +++ b/components/locale_core/tests/preferences/time_formatter/options.rs @@ -0,0 +1,19 @@ +// This file is part of ICU4X. For terms of use, please see the file +// called LICENSE at the top level of the ICU4X source tree +// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). + +use icu_locale_core::preferences::options; + +options!( + TimeFormatterOptions, + { + time_style => TimeStyle + } +); + +#[derive(Debug, PartialEq, Default, Clone, Copy)] +pub enum TimeStyle { + Short, + #[default] + Medium, +} diff --git a/components/locale_core/tests/preferences/time_formatter/preferences.rs b/components/locale_core/tests/preferences/time_formatter/preferences.rs new file mode 100644 index 00000000000..245469052ca --- /dev/null +++ b/components/locale_core/tests/preferences/time_formatter/preferences.rs @@ -0,0 +1,14 @@ +// This file is part of ICU4X. For terms of use, please see the file +// called LICENSE at the top level of the ICU4X source tree +// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). + +use icu_locale_core::preferences::{extensions::unicode::keywords, preferences}; + +preferences!( + /// The locale preferences for time formatting. + TimeFormatterPreferences, + { + /// The hour cycle + hour_cycle: keywords::HourCycle + } +); diff --git a/components/locale_core/tests/prefs.rs b/components/locale_core/tests/prefs.rs new file mode 100644 index 00000000000..bdbe19d6222 --- /dev/null +++ b/components/locale_core/tests/prefs.rs @@ -0,0 +1,94 @@ +// 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 ). + +mod preferences; + +use icu_locale_core::{langid, locale, preferences::extensions::unicode::keywords::HourCycle}; + +use preferences::time_formatter::{ + options::TimeStyle, TimeFormatter, TimeFormatterOptions, TimeFormatterPreferences, +}; + +#[test] +fn prefs_tf_default() { + let loc = locale!("en-US"); + + let tf = TimeFormatter::new((&loc).into(), Default::default()); + + assert_eq!(tf.format_to_string(11), "(en) 11:00 am"); + + // Locale with no data + let loc = locale!("fr"); + + let tf = TimeFormatter::new((&loc).into(), Default::default()); + + assert_eq!(tf.format_to_string(11), "(und) 11:00"); +} + +#[test] +fn prefs_tf_custom_options() { + let loc = locale!("en-US"); + + let tf = TimeFormatter::new( + (&loc).into(), + TimeFormatterOptions { + time_style: Some(TimeStyle::Short), + }, + ); + + assert_eq!(tf.format_to_string(11), "(en) 11:00 am"); + + // Locale with no data + let loc = locale!("fr"); + + let tf = TimeFormatter::new((&loc).into(), Default::default()); + + assert_eq!(tf.format_to_string(11), "(und) 11:00"); +} + +#[test] +fn prefs_tf_custom_preferences() { + let loc = locale!("en-US"); + let bag = TimeFormatterPreferences { + hour_cycle: Some(HourCycle::H23), + ..Default::default() + }; + let mut prefs = TimeFormatterPreferences::from(&loc); + prefs.extend(bag); + + let tf = TimeFormatter::new(prefs, Default::default()); + + assert_eq!(tf.format_to_string(11), "(en) 11:00"); +} + +#[test] +fn prefs_tf_from_lid() { + let lid = langid!("en-US"); + + let tf = TimeFormatter::new((&lid).into(), Default::default()); + + assert_eq!(tf.format_to_string(11), "(en) 11:00 am"); + + // Locale with no data + let loc = locale!("fr"); + + let tf = TimeFormatter::new((&loc).into(), Default::default()); + + assert_eq!(tf.format_to_string(11), "(und) 11:00"); +} + +#[test] +fn prefs_tf_from_lid_with_custom_preferences() { + let lid = langid!("en-US"); + let bag = TimeFormatterPreferences { + hour_cycle: Some(HourCycle::H23), + ..Default::default() + }; + let mut prefs = TimeFormatterPreferences::from(&lid); + prefs.extend(bag); + + let tf = TimeFormatter::new(prefs, Default::default()); + + assert_eq!(tf.format_to_string(11), "(en) 11:00"); +} diff --git a/components/locale_core/tests/prefs_ecma402.rs b/components/locale_core/tests/prefs_ecma402.rs new file mode 100644 index 00000000000..c65db713306 --- /dev/null +++ b/components/locale_core/tests/prefs_ecma402.rs @@ -0,0 +1,30 @@ +// 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 ). + +mod preferences; + +use icu_locale_core::locale; +use icu_locale_core::preferences::extensions::unicode::keywords; + +use preferences::time_formatter::{ecma402::TimeFormatter, options::TimeStyle}; + +#[test] +fn prefs_ecma402_tf_default() { + let loc = locale!("en-US"); + + let tf = TimeFormatter::new((&loc).into(), Default::default()); + + assert_eq!(tf.resolved_options().hour_cycle, keywords::HourCycle::H12); + assert_eq!(tf.resolved_options().time_style, TimeStyle::Medium); + assert_eq!(tf.resolved_options().locale, locale!("en")); + + // Locale with no data + let loc = locale!("fr"); + + let tf = TimeFormatter::new((&loc).into(), Default::default()); + + assert_eq!(tf.resolved_options().hour_cycle, keywords::HourCycle::H23); + assert_eq!(tf.resolved_options().time_style, TimeStyle::Medium); + assert_eq!(tf.resolved_options().locale, locale!("und")); +} diff --git a/provider/core/src/request.rs b/provider/core/src/request.rs index a954d31f0af..9a6b7816c5e 100644 --- a/provider/core/src/request.rs +++ b/provider/core/src/request.rs @@ -14,9 +14,7 @@ use core::hash::Hash; use core::ops::Deref; use core::str::FromStr; use icu_locale_core::extensions::unicode as unicode_ext; -use icu_locale_core::subtags::Subtag; -use icu_locale_core::subtags::Variant; -use icu_locale_core::subtags::{Language, Region, Script}; +use icu_locale_core::subtags::{Language, Region, Script, Subtag, Variant}; use icu_locale_core::{LanguageIdentifier, Locale, ParseError}; use writeable::Writeable; use zerovec::ule::VarULE; @@ -279,6 +277,25 @@ impl DataLocale { self.subdivision, ) } + + /// Creates a [`DataLocale`] from a list of subtags. + pub const fn from_subtags( + language: Language, + script: Option