Skip to content

Commit

Permalink
Add u-hc hour cycle support to neo time formatter (#5317)
Browse files Browse the repository at this point in the history
  • Loading branch information
sffc authored Jul 30, 2024
1 parent a3f4255 commit 6d39119
Show file tree
Hide file tree
Showing 116 changed files with 1,856 additions and 141 deletions.
11 changes: 11 additions & 0 deletions components/datetime/src/neo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use crate::neo_marker::{
};
use crate::neo_pattern::DateTimePattern;
use crate::neo_skeleton::{NeoComponents, NeoSkeletonLength};
use crate::pattern::CoarseHourCycle;
use crate::provider::neo::*;
use crate::raw::neo::*;
use crate::CldrCalendar;
Expand Down Expand Up @@ -528,6 +529,10 @@ where
+ DataProvider<R::GluePatternV1Marker>,
L: FixedDecimalFormatterLoader + WeekCalculatorLoader,
{
let hour_cycle = locale
.get_unicode_ext(&icu_locale_core::extensions::unicode::key!("hc"))
.as_ref()
.and_then(CoarseHourCycle::from_locale_value);
let selection = DateTimeZonePatternSelectionData::try_new_with_skeleton(
&<R::D as TypedDateDataMarkers<C>>::DateSkeletonPatternsV1Marker::bind(provider),
&<R::T as TimeMarkers>::TimeSkeletonPatternsV1Marker::bind(provider),
Expand All @@ -536,6 +541,7 @@ where
options.length.into(),
components,
options.era_display.into(),
hour_cycle,
)
.map_err(LoadError::Data)?;
let mut names = RawDateTimeNames::new_without_fixed_decimal_formatter();
Expand Down Expand Up @@ -1229,6 +1235,10 @@ where
{
let calendar = AnyCalendarLoader::load(loader, locale).map_err(LoadError::Data)?;
let kind = calendar.kind();
let hour_cycle = locale
.get_unicode_ext(&icu_locale_core::extensions::unicode::key!("hc"))
.as_ref()
.and_then(CoarseHourCycle::from_locale_value);
let selection = DateTimeZonePatternSelectionData::try_new_with_skeleton(
&AnyCalendarProvider::<<R::D as DateDataMarkers>::Skel, _>::new(provider, kind),
&<R::T as TimeMarkers>::TimeSkeletonPatternsV1Marker::bind(provider),
Expand All @@ -1237,6 +1247,7 @@ where
options.length.into(),
components,
options.era_display.into(),
hour_cycle,
)
.map_err(LoadError::Data)?;
let mut names = RawDateTimeNames::new_without_fixed_decimal_formatter();
Expand Down
76 changes: 76 additions & 0 deletions components/datetime/src/neo_marker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,69 @@
//! );
//! ```
//!
//! ## Hour Cycle
//!
//! Hours can be switched between 12-hour and 24-hour time via the `u-hc` locale keyword.
//!
//! ```
//! use icu::calendar::Gregorian;
//! use icu::calendar::Time;
//! use icu::datetime::neo::NeoOptions;
//! use icu::datetime::neo::TypedNeoFormatter;
//! use icu::datetime::neo_marker::NeoHourMinuteMarker;
//! use icu::datetime::neo_skeleton::NeoSkeletonLength;
//! use icu::datetime::NeverCalendar;
//! 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 =
//! TypedNeoFormatter::<NeverCalendar, NeoHourMinuteMarker>::try_new(
//! &locale!("en-US-u-hc-h12").into(),
//! NeoSkeletonLength::Short.into(),
//! )
//! .unwrap();
//! assert_try_writeable_eq!(
//! formatter.format(&Time::try_new(16, 12, 20, 0).unwrap()),
//! "4:12 PM"
//! );
//!
//! let formatter =
//! TypedNeoFormatter::<NeverCalendar, NeoHourMinuteMarker>::try_new(
//! &locale!("en-US-u-hc-h23").into(),
//! NeoSkeletonLength::Short.into(),
//! )
//! .unwrap();
//! assert_try_writeable_eq!(
//! formatter.format(&Time::try_new(16, 12, 20, 0).unwrap()),
//! "16:12"
//! );
//!
//! let formatter =
//! TypedNeoFormatter::<NeverCalendar, NeoHourMinuteMarker>::try_new(
//! &locale!("fr-FR-u-hc-h12").into(),
//! NeoSkeletonLength::Short.into(),
//! )
//! .unwrap();
//! assert_try_writeable_eq!(
//! formatter.format(&Time::try_new(16, 12, 20, 0).unwrap()),
//! "4:12 PM"
//! );
//!
//! let formatter =
//! TypedNeoFormatter::<NeverCalendar, NeoHourMinuteMarker>::try_new(
//! &locale!("fr-FR-u-hc-h23").into(),
//! NeoSkeletonLength::Short.into(),
//! )
//! .unwrap();
//! assert_try_writeable_eq!(
//! formatter.format(&Time::try_new(16, 12, 20, 0).unwrap()),
//! "16:12"
//! );
//! ```
//!
//! ## Time Zone Formatting
//!
//! Here, we configure a [`NeoFormatter`] to format with generic non-location short,
Expand Down Expand Up @@ -1829,6 +1892,19 @@ impl_day_marker!(
input_any_calendar_kind = yes,
);

impl_time_marker!(
NeoHourMinuteMarker,
NeoTimeComponents::HourMinute,
description = "hour and minute (locale-dependent hour cycle)",
expectation = "3:47 PM",
dayperiods = yes,
times = yes,
input_hour = yes,
input_minute = yes,
input_second = no,
input_nanosecond = no,
);

impl_time_marker!(
NeoAutoTimeMarker,
NeoTimeComponents::Auto,
Expand Down
71 changes: 65 additions & 6 deletions components/datetime/src/neo_skeleton.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
use crate::neo_serde::*;
use crate::options::components;
use crate::options::length;
use crate::options::preferences;
#[cfg(feature = "experimental")]
use crate::pattern::CoarseHourCycle;
#[cfg(feature = "experimental")]
use crate::raw::neo::MaybeLength;
#[cfg(feature = "experimental")]
Expand Down Expand Up @@ -651,6 +654,20 @@ impl NeoTimeComponents {
}
}

#[cfg(feature = "experimental")]
pub(crate) fn with_hour_cycle(self, hour_cycle: CoarseHourCycle) -> Self {
use CoarseHourCycle::*;
match (self, hour_cycle) {
(Self::Hour, H11H12) => Self::Hour12,
(Self::HourMinute, H11H12) => Self::Hour12Minute,
(Self::HourMinuteSecond, H11H12) => Self::Hour12MinuteSecond,
(Self::Hour, H23H24) => Self::Hour24,
(Self::HourMinute, H23H24) => Self::Hour24Minute,
(Self::HourMinuteSecond, H23H24) => Self::Hour24MinuteSecond,
_ => self,
}
}

/// Converts a [`length::Time`] to its nearest [`NeoTimeComponents`].
#[doc(hidden)] // the types involved in this mapping may change
pub fn from_time_length(time_length: length::Time) -> Self {
Expand Down Expand Up @@ -680,14 +697,56 @@ impl NeoTimeComponents {
..Default::default()
}),
Self::DayPeriodHour12 => todo!(),
Self::Hour12 => todo!(),
Self::Hour12 => DateTimeFormatterOptions::Components(components::Bag {
hour: Some(length.to_components_numeric()),
preferences: Some(preferences::Bag {
hour_cycle: Some(preferences::HourCycle::H12),
}),
..Default::default()
}),
Self::DayPeriodHour12Minute => todo!(),
Self::Hour12Minute => todo!(),
Self::Hour12Minute => DateTimeFormatterOptions::Components(components::Bag {
hour: Some(length.to_components_numeric()),
minute: Some(length.to_components_numeric()),
preferences: Some(preferences::Bag {
hour_cycle: Some(preferences::HourCycle::H12),
}),
..Default::default()
}),
Self::DayPeriodHour12MinuteSecond => todo!(),
Self::Hour12MinuteSecond => todo!(),
Self::Hour24 => todo!(),
Self::Hour24Minute => todo!(),
Self::Hour24MinuteSecond => todo!(),
Self::Hour12MinuteSecond => DateTimeFormatterOptions::Components(components::Bag {
hour: Some(length.to_components_numeric()),
minute: Some(length.to_components_numeric()),
second: Some(length.to_components_numeric()),
preferences: Some(preferences::Bag {
hour_cycle: Some(preferences::HourCycle::H12),
}),
..Default::default()
}),
Self::Hour24 => DateTimeFormatterOptions::Components(components::Bag {
hour: Some(length.to_components_numeric()),
preferences: Some(preferences::Bag {
hour_cycle: Some(preferences::HourCycle::H23),
}),
..Default::default()
}),
Self::Hour24Minute => DateTimeFormatterOptions::Components(components::Bag {
hour: Some(length.to_components_numeric()),
minute: Some(length.to_components_numeric()),
preferences: Some(preferences::Bag {
hour_cycle: Some(preferences::HourCycle::H23),
}),
..Default::default()
}),
Self::Hour24MinuteSecond => DateTimeFormatterOptions::Components(components::Bag {
hour: Some(length.to_components_numeric()),
minute: Some(length.to_components_numeric()),
second: Some(length.to_components_numeric()),
preferences: Some(preferences::Bag {
hour_cycle: Some(preferences::HourCycle::H23),
}),
..Default::default()
}),
Self::Auto => match length {
// Note: For now, make "long" and "medium" both map to "medium".
// This could be improved in light of additional data.
Expand Down
15 changes: 15 additions & 0 deletions components/datetime/src/pattern/hour_cycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ use super::{reference, runtime, PatternItem};
use crate::{fields, options::preferences};
#[cfg(feature = "datagen")]
use crate::{provider, skeleton};
#[cfg(feature = "experimental")]
use icu_locale_core::{extensions::unicode::Value, subtags::Subtag};
use icu_provider::prelude::*;

/// Used to represent either H11/H12, or H23/H24. Skeletons only store these
Expand Down Expand Up @@ -53,6 +55,19 @@ impl CoarseHourCycle {
None
}

#[cfg(feature = "experimental")]
pub(crate) fn from_locale_value(value: &Value) -> Option<Self> {
const H11: Subtag = icu_locale_core::subtags::subtag!("h11");
const H12: Subtag = icu_locale_core::subtags::subtag!("h12");
const H23: Subtag = icu_locale_core::subtags::subtag!("h23");
const H24: Subtag = icu_locale_core::subtags::subtag!("h24");
match value.as_single_subtag() {
Some(&H11) | Some(&H12) => Some(Self::H11H12),
Some(&H23) | Some(&H24) => Some(Self::H23H24),
_ => None,
}
}

/// Invoke the pattern matching machinery to transform the hour cycle of a pattern. This provides
/// a safe mapping from a h11/h12 to h23/h24 for transforms.
#[doc(hidden)]
Expand Down
43 changes: 36 additions & 7 deletions components/datetime/src/raw/neo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use crate::neo_skeleton::{
NeoTimeComponents, NeoTimeSkeleton, NeoTimeZoneSkeleton,
};
use crate::pattern::runtime::PatternMetadata;
use crate::pattern::{runtime, GenericPatternItem, PatternItem};
use crate::pattern::{runtime, CoarseHourCycle, GenericPatternItem, PatternItem};
use crate::provider::neo::*;
use crate::time_zone::ResolvedNeoTimeZoneSkeleton;
use icu_calendar::AnyCalendarKind;
Expand Down Expand Up @@ -240,17 +240,40 @@ impl TimePatternSelectionData {
locale: &DataLocale,
length: MaybeLength,
components: NeoTimeComponents,
hour_cycle: Option<CoarseHourCycle>,
) -> Result<Self, DataError> {
let payload = provider
.load_bound(DataRequest {
// First try to load with the explicit hour cycle. If there is no explicit hour cycle,
// or if loading the explicit hour cycle fails, then load with the default hour cycle.
let mut maybe_payload = None;
if let Some(hour_cycle) = hour_cycle {
maybe_payload = match provider.load_bound(DataRequest {
id: DataIdentifierBorrowed::for_marker_attributes_and_locale(
components.id_str(),
components.with_hour_cycle(hour_cycle).id_str(),
locale,
),
..Default::default()
})?
.payload
.cast();
}) {
Ok(response) => Some(response.payload.cast()),
Err(DataError {
kind: DataErrorKind::IdentifierNotFound,
..
}) => None,
Err(e) => return Err(e),
};
}
let payload = match maybe_payload {
Some(payload) => payload,
None => provider
.load_bound(DataRequest {
id: DataIdentifierBorrowed::for_marker_attributes_and_locale(
components.id_str(),
locale,
),
..Default::default()
})?
.payload
.cast(),
};
Ok(Self::SkeletonTime {
skeleton: NeoTimeSkeleton {
length: length.get::<Self>(),
Expand Down Expand Up @@ -320,6 +343,7 @@ impl ZonePatternSelectionData {
}

impl DateTimeZonePatternSelectionData {
#[allow(clippy::too_many_arguments)] // private function with lots of generics
pub(crate) fn try_new_with_skeleton(
date_provider: &(impl BoundDataProvider<SkeletaV1Marker> + ?Sized),
time_provider: &(impl BoundDataProvider<SkeletaV1Marker> + ?Sized),
Expand All @@ -328,6 +352,7 @@ impl DateTimeZonePatternSelectionData {
length: Option<NeoSkeletonLength>,
components: NeoComponents,
era_display: Option<EraDisplay>,
hour_cycle: Option<CoarseHourCycle>,
) -> Result<Self, DataError> {
let length = MaybeLength(length);
match components {
Expand All @@ -347,6 +372,7 @@ impl DateTimeZonePatternSelectionData {
locale,
length,
components,
hour_cycle,
)?;
Ok(Self::Time(selection))
}
Expand All @@ -367,6 +393,7 @@ impl DateTimeZonePatternSelectionData {
locale,
length,
time_components,
hour_cycle,
)?;
let glue = Self::load_glue(glue_provider, locale, length, GlueType::DateTime)?;
Ok(Self::DateTimeGlue { date, time, glue })
Expand All @@ -389,6 +416,7 @@ impl DateTimeZonePatternSelectionData {
locale,
length,
time_components,
hour_cycle,
)?;
let zone = ZonePatternSelectionData::new_with_skeleton(length, zone_components);
let glue = Self::load_glue(glue_provider, locale, length, GlueType::TimeZone)?;
Expand All @@ -407,6 +435,7 @@ impl DateTimeZonePatternSelectionData {
locale,
length,
time_components,
hour_cycle,
)?;
let zone = ZonePatternSelectionData::new_with_skeleton(length, zone_components);
let glue = Self::load_glue(glue_provider, locale, length, GlueType::DateTimeZone)?;
Expand Down

Large diffs are not rendered by default.

Loading

0 comments on commit 6d39119

Please sign in to comment.