From b98c33707ca3fe3276358978e0599ea8a60d627d Mon Sep 17 00:00:00 2001 From: Kart Date: Sat, 6 Jul 2024 07:44:00 +0530 Subject: [PATCH] Validation Duration Options and Duration Formatter Stub (#5112) --- .../src/duration/formatter/mod.rs | 25 ++ .../duration/formatter/validated_options.rs | 362 ++++++++++++++++++ components/experimental/src/duration/mod.rs | 2 + .../experimental/src/duration/options.rs | 155 +++++--- 4 files changed, 497 insertions(+), 47 deletions(-) create mode 100644 components/experimental/src/duration/formatter/mod.rs create mode 100644 components/experimental/src/duration/formatter/validated_options.rs diff --git a/components/experimental/src/duration/formatter/mod.rs b/components/experimental/src/duration/formatter/mod.rs new file mode 100644 index 00000000000..44f175ca3bb --- /dev/null +++ b/components/experimental/src/duration/formatter/mod.rs @@ -0,0 +1,25 @@ +// 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::options::*; + +mod validated_options; +pub use validated_options::{DurationFormatterOptionsError, ValidatedDurationFormatterOptions}; + +/// A formatter for [`Duration`](crate::duration::Duration)s. +#[derive(Clone)] +pub struct DurationFormatter { + #[allow(dead_code)] + /// Options for configuring the formatter. + pub(crate) options: ValidatedDurationFormatterOptions, +} + +impl DurationFormatter { + /// Create a new [`DurationFormatter`] with the given options. + pub fn new(options: DurationFormatterOptions) -> Result { + Ok(DurationFormatter { + options: ValidatedDurationFormatterOptions::validate(options)?, + }) + } +} diff --git a/components/experimental/src/duration/formatter/validated_options.rs b/components/experimental/src/duration/formatter/validated_options.rs new file mode 100644 index 00000000000..91ae5dcc147 --- /dev/null +++ b/components/experimental/src/duration/formatter/validated_options.rs @@ -0,0 +1,362 @@ +// 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 crate::duration::options::*; + +/// Validated options for [DurationFormatter](DurationFormatter). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ValidatedDurationFormatterOptions { + /// The style that will be applied to units + /// unless overridden by a specific style. + base: BaseStyle, + + /// Style for year + year: FieldStyle, + /// Visibility control for year + year_visibility: FieldDisplay, + /// Style for month + month: FieldStyle, + /// Visibility control for month + month_visibility: FieldDisplay, + /// Style for week + week: FieldStyle, + /// Visibility control for week + week_visibility: FieldDisplay, + /// Style for day + day: FieldStyle, + /// Visibility control for day + day_visibility: FieldDisplay, + /// Style for hour + hour: FieldStyle, + /// Visibility control for hour + hour_visibility: FieldDisplay, + /// Style for minute + minute: FieldStyle, + /// Visibility control for minute + minute_visibility: FieldDisplay, + /// Style for second + second: FieldStyle, + /// Visibility control for second + second_visibility: FieldDisplay, + /// Style for millisecond + millisecond: FieldStyle, + /// Visibility control for millisecond + millisecond_visibility: FieldDisplay, + /// Style for microsecond + microsecond: FieldStyle, + /// Visibility control for microsecond + microsecond_visibility: FieldDisplay, + /// Style for nanosecond + nanosecond: FieldStyle, + /// Visibility control for nanosecond + nanosecond_visibility: FieldDisplay, + + /// Number of fractional digits to use when formatting sub-second units (milliseconds, microseconds, nanoseconds). + /// ### Note: + /// - Only takes effect when the subsecond units are styled as `Numeric`. + /// - Zero means no fractional digits. + fractional_digits: FractionalDigits, +} + +/// Error type for [DurationFormatterOptions] validation. +#[non_exhaustive] +pub enum DurationFormatterOptionsError { + InvalidFractionalDigits, +} + +impl ValidatedDurationFormatterOptions { + pub(crate) fn validate( + value: DurationFormatterOptions, + ) -> Result { + let mut builder: ValidatedDurationFormatterOptionsBuilder = value.into(); + + let units = builder.iter_units(); + + // section 1.1.6 + let mut prev_style = None; + for (unit, style, visibility) in units.into_iter() { + // 2. Let displayDefault be "always". + let mut default_visibility = FieldDisplay::Always; + + // 3. If style is undefined, then + if style.is_none() { + // a. If baseStyle is "digital", then + if value.base == BaseStyle::Digital { + // i. If unit is not one of "hours", "minutes", or "seconds", then + if unit != Unit::Hour || unit != Unit::Minute || unit != Unit::Second { + // 1. Set displayDefault to "auto". + default_visibility = FieldDisplay::Auto; + } + // ii. Set style to digitalBase. + *style = Some(unit.digital_default()); + } + // b. Else, + else { + // i. If prevStyle is "fractional", "numeric" or "2-digit", then + if matches!( + prev_style, + Some(FieldStyle::Fractional | FieldStyle::Numeric | FieldStyle::TwoDigit) + ) { + // 1. If unit is not one of "minutes" or "seconds", then + if unit != Unit::Minute || unit != Unit::Second { + // a. Set displayDefault to "auto". + default_visibility = FieldDisplay::Auto; + } + // 2. Set style to "numeric". + *style = Some(FieldStyle::Numeric); + } + // ii. Else, + else { + // 1. Set displayDefault to "auto". + default_visibility = FieldDisplay::Auto; + // 2. Set style to baseStyle. + *style = Some(value.base.into()); + } + } + } + + // 4. If style is "numeric", then + if *style == Some(FieldStyle::Numeric) { + // a. If unit is one of "milliseconds", "microseconds", or "nanoseconds", then + if unit == Unit::Millisecond + || unit == Unit::Microsecond + || unit == Unit::Nanosecond + { + // i. Set style to "fractional". + *style = Some(FieldStyle::Fractional); + // ii. Set displayDefault to "auto". + default_visibility = FieldDisplay::Auto; + } + } + + // 5. Let displayField be the string-concatenation of unit and "Display". + // 6. Let display be ? GetOption(options, displayField, string, « "auto", "always" », displayDefault). + if visibility.is_none() { + *visibility = Some(default_visibility); + } + + // 7. If display is "always" and style is "fractional", then + if *visibility == Some(FieldDisplay::Always) && *style == Some(FieldStyle::Fractional) { + // a. Throw a RangeError exception. + return Err(DurationFormatterOptionsError::InvalidFractionalDigits); + } + + // 8. If prevStyle is "fractional", then + if prev_style == Some(FieldStyle::Fractional) { + // a. If style is not "fractional", then + if *style != Some(FieldStyle::Fractional) { + // i. Throw a RangeError exception. + return Err(DurationFormatterOptionsError::InvalidFractionalDigits); + } + } + + // 9. If prevStyle is "numeric" or "2-digit", then + if prev_style == Some(FieldStyle::Numeric) || prev_style == Some(FieldStyle::TwoDigit) { + // a. If style is not "fractional", "numeric" or "2-digit", then + if !matches!( + *style, + Some(FieldStyle::Fractional | FieldStyle::Numeric | FieldStyle::TwoDigit) + ) { + // i. Throw a RangeError exception. + return Err(DurationFormatterOptionsError::InvalidFractionalDigits); + } + // b. If unit is "minutes" or "seconds", then + if unit == Unit::Minute || unit == Unit::Second { + // i. Set style to "2-digit". + *style = Some(FieldStyle::TwoDigit); + } + } + + // 10. If unit is "hours" and twoDigitHours is true, then + if unit == Unit::Hour && todo!("twoDigitHours") { + // a. Set style to "2-digit". + *style = Some(FieldStyle::TwoDigit); + } + + prev_style = *style; + } + + Ok(builder.try_into().unwrap()) + } + + /// Iterates over all unit fields of the struct, returning a tuple of the unit, + /// and mutable references to its style and the visibility. + #[allow(dead_code)] + pub(crate) fn iter_units(&mut self) -> [(Unit, &mut FieldStyle, &mut FieldDisplay); 10] { + [ + (Unit::Year, &mut self.year, &mut self.year_visibility), + (Unit::Month, &mut self.month, &mut self.month_visibility), + (Unit::Week, &mut self.week, &mut self.week_visibility), + (Unit::Day, &mut self.day, &mut self.day_visibility), + (Unit::Hour, &mut self.hour, &mut self.hour_visibility), + (Unit::Minute, &mut self.minute, &mut self.minute_visibility), + (Unit::Second, &mut self.second, &mut self.second_visibility), + ( + Unit::Millisecond, + &mut self.millisecond, + &mut self.millisecond_visibility, + ), + ( + Unit::Microsecond, + &mut self.microsecond, + &mut self.microsecond_visibility, + ), + ( + Unit::Nanosecond, + &mut self.nanosecond, + &mut self.nanosecond_visibility, + ), + ] + } +} + +/// Validated options builder for [DurationFormatter](DurationFormatter). +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +struct ValidatedDurationFormatterOptionsBuilder { + base: BaseStyle, + + year: Option, + year_visibility: Option, + month: Option, + month_visibility: Option, + week: Option, + week_visibility: Option, + day: Option, + day_visibility: Option, + hour: Option, + hour_visibility: Option, + minute: Option, + minute_visibility: Option, + second: Option, + second_visibility: Option, + millisecond: Option, + millisecond_visibility: Option, + microsecond: Option, + microsecond_visibility: Option, + nanosecond: Option, + nanosecond_visibility: Option, + fractional_digits: FractionalDigits, +} + +impl ValidatedDurationFormatterOptionsBuilder { + fn iter_units(&mut self) -> [(Unit, &mut Option, &mut Option); 10] { + [ + (Unit::Year, &mut self.year, &mut self.year_visibility), + (Unit::Month, &mut self.month, &mut self.month_visibility), + (Unit::Week, &mut self.week, &mut self.week_visibility), + (Unit::Day, &mut self.day, &mut self.day_visibility), + (Unit::Hour, &mut self.hour, &mut self.hour_visibility), + (Unit::Minute, &mut self.minute, &mut self.minute_visibility), + (Unit::Second, &mut self.second, &mut self.second_visibility), + ( + Unit::Millisecond, + &mut self.millisecond, + &mut self.millisecond_visibility, + ), + ( + Unit::Microsecond, + &mut self.microsecond, + &mut self.microsecond_visibility, + ), + ( + Unit::Nanosecond, + &mut self.nanosecond, + &mut self.nanosecond_visibility, + ), + ] + } +} + +impl From for ValidatedDurationFormatterOptionsBuilder { + fn from(value: DurationFormatterOptions) -> Self { + ValidatedDurationFormatterOptionsBuilder { + base: value.base, + year: value.year.map(FieldStyle::from), + year_visibility: value.year_visibility, + month: value.month.map(FieldStyle::from), + month_visibility: value.month_visibility, + week: value.week.map(FieldStyle::from), + week_visibility: value.week_visibility, + day: value.day.map(FieldStyle::from), + day_visibility: value.day_visibility, + hour: value.hour.map(FieldStyle::from), + hour_visibility: value.hour_visibility, + minute: value.minute.map(FieldStyle::from), + minute_visibility: value.minute_visibility, + second: value.second.map(FieldStyle::from), + second_visibility: value.second_visibility, + millisecond: value.millisecond.map(FieldStyle::from), + millisecond_visibility: value.millisecond_visibility, + microsecond: value.microsecond.map(FieldStyle::from), + microsecond_visibility: value.microsecond_visibility, + nanosecond: value.nanosecond.map(FieldStyle::from), + nanosecond_visibility: value.nanosecond_visibility, + fractional_digits: value.fractional_digits, + } + } +} + +impl TryFrom for ValidatedDurationFormatterOptions { + type Error = (); + + fn try_from(value: ValidatedDurationFormatterOptionsBuilder) -> Result { + Ok(ValidatedDurationFormatterOptions { + base: value.base, + year: value.year.ok_or(())?, + year_visibility: value.year_visibility.ok_or(())?, + month: value.month.ok_or(())?, + month_visibility: value.month_visibility.ok_or(())?, + week: value.week.ok_or(())?, + week_visibility: value.week_visibility.ok_or(())?, + day: value.day.ok_or(())?, + day_visibility: value.day_visibility.ok_or(())?, + hour: value.hour.ok_or(())?, + hour_visibility: value.hour_visibility.ok_or(())?, + minute: value.minute.ok_or(())?, + minute_visibility: value.minute_visibility.ok_or(())?, + second: value.second.ok_or(())?, + second_visibility: value.second_visibility.ok_or(())?, + millisecond: value.millisecond.ok_or(())?, + millisecond_visibility: value.millisecond_visibility.ok_or(())?, + microsecond: value.microsecond.ok_or(())?, + microsecond_visibility: value.microsecond_visibility.ok_or(())?, + nanosecond: value.nanosecond.ok_or(())?, + nanosecond_visibility: value.nanosecond_visibility.ok_or(())?, + fractional_digits: value.fractional_digits, + }) + } +} + +/// An enum to specify the unit being used. Used with FieldStyle and FieldDisplay to indicate the field unit. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum Unit { + Year, + Month, + Week, + Day, + Hour, + Minute, + Second, + Millisecond, + Microsecond, + Nanosecond, +} + +impl Unit { + /// Returns the default digital style for the unit. + pub(crate) fn digital_default(&self) -> FieldStyle { + match self { + Unit::Year => YearStyle::Short.into(), + Unit::Month => MonthStyle::Short.into(), + Unit::Week => WeekStyle::Short.into(), + Unit::Day => DayStyle::Short.into(), + Unit::Hour => HourStyle::Short.into(), + Unit::Minute => MinuteStyle::Numeric.into(), + Unit::Second => SecondStyle::Numeric.into(), + Unit::Millisecond => MilliSecondStyle::Numeric.into(), + Unit::Microsecond => MicroSecondStyle::Numeric.into(), + Unit::Nanosecond => NanoSecondStyle::Numeric.into(), + } + } +} diff --git a/components/experimental/src/duration/mod.rs b/components/experimental/src/duration/mod.rs index 272e790df22..57ec74e6192 100644 --- a/components/experimental/src/duration/mod.rs +++ b/components/experimental/src/duration/mod.rs @@ -7,6 +7,8 @@ #![warn(missing_docs)] mod duration; +mod formatter; pub mod options; pub use duration::{Duration, DurationSign}; +pub use formatter::DurationFormatter; diff --git a/components/experimental/src/duration/options.rs b/components/experimental/src/duration/options.rs index e72f295696f..530a21c1576 100644 --- a/components/experimental/src/duration/options.rs +++ b/components/experimental/src/duration/options.rs @@ -2,9 +2,9 @@ // called LICENSE at the top level of the ICU4X source tree // (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). -//! Options for configuring `DurationFormatter`. +//! Options for configuring [`DurationFormatter`](crate::duration::DurationFormatter). -/// A bag of options for defining how to format duration using `DurationFormatter`. +/// A bag of options for defining how to format duration using [`DurationFormatter`](crate::duration::DurationFormatter). #[non_exhaustive] #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub struct DurationFormatterOptions { @@ -13,49 +13,50 @@ pub struct DurationFormatterOptions { pub base: BaseStyle, /// Style for year - pub year: YearStyle, + pub year: Option, /// Visibility control for year - pub year_visibility: UnitVisibility, + pub year_visibility: Option, /// Style for month - pub month: MonthStyle, + pub month: Option, /// Visibility control for month - pub month_visibility: UnitVisibility, + pub month_visibility: Option, /// Style for week - pub week: WeekStyle, + pub week: Option, /// Visibility control for week - pub week_visibility: UnitVisibility, + pub week_visibility: Option, /// Style for day - pub day: DayStyle, + pub day: Option, /// Visibility control for day - pub day_visibility: UnitVisibility, + pub day_visibility: Option, /// Style for hour - pub hour: HourStyle, + pub hour: Option, /// Visibility control for hour - pub hour_visibility: UnitVisibility, + pub hour_visibility: Option, /// Style for minute - pub minute: MinuteStyle, + pub minute: Option, /// Visibility control for minute - pub minute_visibility: UnitVisibility, + pub minute_visibility: Option, /// Style for second - pub second: SecondStyle, + pub second: Option, /// Visibility control for second - pub second_visibility: UnitVisibility, + pub second_visibility: Option, /// Style for millisecond - pub millisecond: MilliSecondStyle, + pub millisecond: Option, /// Visibility control for millisecond - pub millisecond_visibility: UnitVisibility, + pub millisecond_visibility: Option, /// Style for microsecond - pub microsecond: MicroSecondStyle, + pub microsecond: Option, /// Visibility control for microsecond - pub microsecond_visibility: UnitVisibility, + pub microsecond_visibility: Option, /// Style for nanosecond - pub nanosecond: NanoSecondStyle, + pub nanosecond: Option, /// Visibility control for nanosecond - pub nanosecond_visibility: UnitVisibility, + pub nanosecond_visibility: Option, /// Number of fractional digits to use when formatting sub-second units (milliseconds, microseconds, nanoseconds). - /// Only takes effect when the subsecond units are styled as `Numeric`. - /// Zero means no fractional digits. + /// ### Note: + /// - Only takes effect when the subsecond units are styled as `Numeric`. + /// - Zero means no fractional digits. pub fractional_digits: FractionalDigits, } @@ -73,15 +74,84 @@ pub enum FractionalDigits { /// Configures visibility of fields in the formatted string. #[non_exhaustive] -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] -pub enum UnitVisibility { - #[default] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FieldDisplay { /// Only display this field if it is non-zero. Auto, /// Always display this field. Always, } +/// Enum used to process different unit styles in a generic way. +/// Implements `From` and `TryFrom` for all unit enums. +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum FieldStyle { + /// Narrow style (most compact) + Narrow, + /// Short style (default) + Short, + /// Long style (most verbose) + Long, + /// Ensure formatted value is at least two digits long (by appending leading zeroes, if necessary) + TwoDigit, + /// Numeric style + Numeric, + /// Fractional style + Fractional, + /// Digital style + Digital, +} + +macro_rules! derive_style { + ( + $( + $(#[$enum_meta:meta])* + pub enum $enum_name: ident { + $( + $(#[$variant_meta:meta])* + $variant: ident + ),* $(,)? + } + )+ + ) => { + $( + $(#[$enum_meta])* + pub enum $enum_name { + $( + $(#[$variant_meta])* + $variant, + )* + } + + impl From<$enum_name> for FieldStyle { + fn from(style: $enum_name) -> Self { + #[allow(unreachable_patterns)] + match style { + $( + $enum_name::$variant => FieldStyle::$variant, + )* + } + } + } + + impl TryFrom for $enum_name { + type Error = FieldStyle; + + fn try_from(style: FieldStyle) -> Result { + match style { + $( + FieldStyle::$variant => Ok($enum_name::$variant), + )* + rest => Err(rest), + } + } + } + )+ + }; +} + +derive_style! { /// Configures the style of the duration output. #[non_exhaustive] #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] @@ -99,11 +169,10 @@ pub enum BaseStyle { /// Configures the style of the year field. #[non_exhaustive] -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum YearStyle { /// Narrow style (most compact) Narrow, - #[default] /// Short style (default) Short, /// Long style (most verbose) @@ -112,11 +181,10 @@ pub enum YearStyle { /// Configures the style of the month field. #[non_exhaustive] -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum MonthStyle { /// Narrow style (most compact) Narrow, - #[default] /// Short style (default) Short, /// Long style (most verbose) @@ -125,11 +193,10 @@ pub enum MonthStyle { /// Configures the style of the week field. #[non_exhaustive] -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum WeekStyle { /// Narrow style (most compact) Narrow, - #[default] /// Short style (default) Short, /// Long style (most verbose) @@ -138,11 +205,10 @@ pub enum WeekStyle { /// Configures the style of the day field. #[non_exhaustive] -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DayStyle { /// Narrow style (most compact) Narrow, - #[default] /// Short style (default) Short, /// Long style (most verbose) @@ -151,11 +217,10 @@ pub enum DayStyle { /// Configures the style of the hour field. #[non_exhaustive] -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum HourStyle { /// Narrow style (most compact) Narrow, - #[default] /// Short style (default) Short, /// Long style (most verbose) @@ -168,11 +233,10 @@ pub enum HourStyle { /// Configures the style of the minute field. #[non_exhaustive] -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum MinuteStyle { /// Narrow style (most compact) Narrow, - #[default] /// Short style (default) Short, /// Long style (most verbose) @@ -185,11 +249,10 @@ pub enum MinuteStyle { /// Configures the style of the second field. #[non_exhaustive] -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SecondStyle { /// Narrow style (most compact) Narrow, - #[default] /// Short style (default) Short, /// Long style (most verbose) @@ -202,11 +265,10 @@ pub enum SecondStyle { /// Configures the style of the milliseconds field. #[non_exhaustive] -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum MilliSecondStyle { /// Narrow style (most compact) Narrow, - #[default] /// Short style (default) Short, /// Long style (most verbose) @@ -217,11 +279,10 @@ pub enum MilliSecondStyle { /// Configures the style of the microsecond field. #[non_exhaustive] -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum MicroSecondStyle { /// Narrow style (most compact) Narrow, - #[default] /// Short style (default) Short, /// Long style (most verbose) @@ -232,11 +293,10 @@ pub enum MicroSecondStyle { /// Configures the style of the nanosecond field. #[non_exhaustive] -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum NanoSecondStyle { /// Narrow style (most compact) Narrow, - #[default] /// Short style (default) Short, /// Long style (most verbose) @@ -244,3 +304,4 @@ pub enum NanoSecondStyle { /// Numeric style Numeric, } +}