Skip to content

Commit

Permalink
Add TimeZoneFallbackOverride date field length; use NeoTimeZoneSkelet…
Browse files Browse the repository at this point in the history
…on (#5024)

#1317
  • Loading branch information
sffc authored Jun 25, 2024
1 parent b888362 commit 8e7e8e7
Show file tree
Hide file tree
Showing 10 changed files with 248 additions and 138 deletions.
77 changes: 67 additions & 10 deletions components/datetime/src/fields/length.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,21 @@ pub enum FieldLength {
Fixed(u8),
/// FieldLength::One (numeric), but overridden with a different numbering system
NumericOverride(FieldNumericOverrides),
/// A time zone field with non-standard rules.
TimeZoneFallbackOverride(TimeZoneFallbackOverride),
}

/// First index used for numeric overrides in compact FieldLength representation
///
/// Currently 17 due to decision in <https://unicode-org.atlassian.net/browse/CLDR-17217>,
/// may become 16 if the `> 16` is updated to a ` >= 16`
const FIRST_NUMERIC_OVERRIDE: u8 = 17;
/// Last index used for numeric overrides
const LAST_NUMERIC_OVERRIDE: u8 = 31;
/// First index used for time zone fallback overrides
const FIRST_TIME_ZONE_FALLBACK_OVERRIDE: u8 = 32;
/// Last index used for time zone fallback overrides
const LAST_TIME_ZONE_FALLBACK_OVERRIDE: u8 = 40;
/// First index used for fixed size formats in compact FieldLength representation
const FIRST_FIXED: u8 = 128;

Expand All @@ -76,7 +84,10 @@ impl FieldLength {
FieldLength::Six => 6,
FieldLength::NumericOverride(o) => FIRST_NUMERIC_OVERRIDE
.saturating_add(*o as u8)
.min(FIRST_FIXED - 1),
.min(LAST_NUMERIC_OVERRIDE),
FieldLength::TimeZoneFallbackOverride(o) => FIRST_TIME_ZONE_FALLBACK_OVERRIDE
.saturating_add(*o as u8)
.min(LAST_TIME_ZONE_FALLBACK_OVERRIDE),
FieldLength::Fixed(p) => FIRST_FIXED.saturating_add(*p), /* truncate to at most 127 digits to avoid overflow */
}
}
Expand All @@ -90,16 +101,18 @@ impl FieldLength {
4 => Self::Wide,
5 => Self::Narrow,
6 => Self::Six,
idx => {
if idx < FIRST_NUMERIC_OVERRIDE {
return Err(LengthError::InvalidLength);
}
if idx < FIRST_FIXED {
Self::NumericOverride((idx - FIRST_NUMERIC_OVERRIDE).try_into()?)
} else {
Self::Fixed(idx - FIRST_FIXED)
}
idx if (FIRST_NUMERIC_OVERRIDE..=LAST_NUMERIC_OVERRIDE).contains(&idx) => {
Self::NumericOverride((idx - FIRST_NUMERIC_OVERRIDE).try_into()?)
}
idx if (FIRST_TIME_ZONE_FALLBACK_OVERRIDE..=LAST_TIME_ZONE_FALLBACK_OVERRIDE)
.contains(&idx) =>
{
Self::TimeZoneFallbackOverride(
(idx - FIRST_TIME_ZONE_FALLBACK_OVERRIDE).try_into()?,
)
}
idx if idx >= FIRST_FIXED => Self::Fixed(idx - FIRST_FIXED),
_ => return Err(LengthError::InvalidLength),
})
}

Expand All @@ -114,6 +127,9 @@ impl FieldLength {
FieldLength::Narrow => 5,
FieldLength::Six => 6,
FieldLength::NumericOverride(o) => FIRST_NUMERIC_OVERRIDE as usize + o as usize,
FieldLength::TimeZoneFallbackOverride(o) => {
FIRST_TIME_ZONE_FALLBACK_OVERRIDE as usize + o as usize
}
FieldLength::Fixed(p) => p as usize,
}
}
Expand Down Expand Up @@ -228,3 +244,44 @@ impl fmt::Display for FieldNumericOverrides {
self.as_str().fmt(f)
}
}

/// Time zone fallback overrides to support configurations not found
/// in the CLDR datetime field symbol table.
#[derive(Debug, Eq, PartialEq, Clone, Copy, Ord, PartialOrd)]
#[cfg_attr(
feature = "datagen",
derive(serde::Serialize, databake::Bake),
databake(path = icu_datetime::fields),
)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
#[non_exhaustive]
pub enum TimeZoneFallbackOverride {
/// The short form of this time zone field,
/// but fall back directly to GMT.
ShortOrGmt = 0,
}

impl TryFrom<u8> for TimeZoneFallbackOverride {
type Error = LengthError;
fn try_from(other: u8) -> Result<Self, LengthError> {
Ok(match other {
0 => Self::ShortOrGmt,
_ => return Err(LengthError::InvalidLength),
})
}
}

// impl TimeZoneFallbackOverride {
// /// Convert this to the corresponding string code
// pub fn as_str(self) -> &'static str {
// match self {
// Self::ShortOrGmt => "ShortOrGmt",
// }
// }
// }

// impl fmt::Display for TimeZoneFallbackOverride {
// fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// self.as_str().fmt(f)
// }
// }
2 changes: 1 addition & 1 deletion components/datetime/src/fields/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ mod length;
pub(crate) mod symbols;

use displaydoc::Display;
pub use length::{FieldLength, FieldNumericOverrides, LengthError};
pub use length::{FieldLength, FieldNumericOverrides, LengthError, TimeZoneFallbackOverride};
pub use symbols::*;

use core::{
Expand Down
4 changes: 2 additions & 2 deletions components/datetime/src/fields/symbols.rs
Original file line number Diff line number Diff line change
Expand Up @@ -509,8 +509,8 @@ impl LengthType for Month {
FieldLength::Wide => TextOrNumeric::Text,
FieldLength::Narrow => TextOrNumeric::Text,
FieldLength::Six => TextOrNumeric::Text,
FieldLength::Fixed(_) => {
debug_assert!(false, "Fixed field length is only supported for seconds");
FieldLength::Fixed(_) | FieldLength::TimeZoneFallbackOverride(_) => {
debug_assert!(false, "Invalid field length for month");
TextOrNumeric::Text
}
}
Expand Down
185 changes: 109 additions & 76 deletions components/datetime/src/format/datetime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{self, Field, FieldLength, FieldSymbol, Second, TimeZone, Week, Year};
use crate::fields::{self, Field, FieldLength, FieldSymbol, Second, Week, Year};
use crate::input::{DateInput, ExtractedDateTimeInput, IsoTimeInput};
use crate::pattern::runtime::{PatternBorrowed, PatternMetadata};
use crate::pattern::{
Expand Down Expand Up @@ -159,7 +159,9 @@ where
{
if let Some(fdf) = fixed_decimal_format {
match length {
FieldLength::One | FieldLength::NumericOverride(_) => {}
FieldLength::One
| FieldLength::NumericOverride(_)
| FieldLength::TimeZoneFallbackOverride(_) => {}
FieldLength::TwoDigit => {
num.pad_start(2);
num.set_max_position(2);
Expand Down Expand Up @@ -243,6 +245,19 @@ where
while let Some(item) = iter.next() {
match item {
PatternItem::Literal(ch) => w.write_char(ch)?,
PatternItem::Field(Field {
symbol: fields::FieldSymbol::TimeZone(time_zone_field),
length,
}) => {
r = r.and(try_write_zone(
time_zone_field,
length,
datetime,
zone_symbols,
fixed_decimal_format,
w,
)?)
}
PatternItem::Field(field) => {
r = r.and(try_write_field(
field,
Expand All @@ -251,7 +266,6 @@ where
datetime,
date_symbols,
time_symbols,
zone_symbols,
week_data,
fixed_decimal_format,
w,
Expand Down Expand Up @@ -328,14 +342,13 @@ pub enum DateTimeWriteError {
// When modifying the list of fields using symbols,
// update the matching query in `analyze_pattern` function.
#[allow(clippy::too_many_arguments)]
pub(crate) fn try_write_field<'data, W, DS, TS, ZS>(
pub(crate) fn try_write_field<'data, W, DS, TS>(
field: fields::Field,
iter: &mut Peekable<impl Iterator<Item = PatternItem>>,
pattern_metadata: PatternMetadata,
datetime: &ExtractedDateTimeInput,
date_symbols: Option<&DS>,
time_symbols: Option<&TS>,
zone_symbols: Option<&ZS>,
week_data: Option<&WeekCalculator>,
fdf: Option<&FixedDecimalFormatter>,
w: &mut W,
Expand All @@ -344,7 +357,6 @@ where
W: writeable::PartsWrite + ?Sized,
DS: DateSymbols<'data>,
TS: TimeSymbols,
ZS: ZoneSymbols,
{
// Writes an error string for the given symbol
fn write_value_missing(
Expand All @@ -358,32 +370,6 @@ where
})
}

fn try_write_time_zone_gmt(
w: &mut (impl writeable::PartsWrite + ?Sized),
_gmt_offset: Option<GmtOffset>,
zone_symbols: Option<&impl ZoneSymbols>,
_graceful: bool,
) -> Result<Result<(), DateTimeWriteError>, fmt::Error> {
#[allow(clippy::bind_instead_of_map)] // TODO: Use proper formatting logic here
Ok(
match zone_symbols
.ok_or(DateTimeWriteError::MissingZoneSymbols)
.and_then(|_zs| {
// TODO: Use proper formatting logic here
Ok("{todo}")
}) {
Err(e) => Err(e),
Ok(s) => Ok(s.write_to(w)?),
},
)
}

fn write_time_zone_missing(
w: &mut (impl writeable::PartsWrite + ?Sized),
) -> Result<(), fmt::Error> {
w.with_part(Part::ERROR, |w| "GMT+?".write_to(w))
}

Ok(match (field.symbol, field.length) {
(FieldSymbol::Era, l) => match datetime.year() {
None => {
Expand Down Expand Up @@ -776,50 +762,11 @@ where
}
}
},
(FieldSymbol::TimeZone(TimeZone::LowerV), _l) => match datetime.time_zone() {
None => {
write_value_missing(w, field)?;
Err(DateTimeWriteError::MissingInputField("time_zone"))
}
Some(CustomTimeZone {
#[cfg(feature = "experimental")]
gmt_offset,
metazone_id: Some(metazone_id),
time_zone_id,
..
}) => match zone_symbols
.ok_or(GetSymbolForTimeZoneError::MissingNames(field))
.and_then(|zs| zs.get_generic_short_for_zone(metazone_id, time_zone_id))
{
Err(e) => match e {
GetSymbolForTimeZoneError::TypeTooNarrow => {
write_time_zone_missing(w)?;
Err(DateTimeWriteError::MissingNames(field))
}
#[cfg(feature = "experimental")]
GetSymbolForTimeZoneError::Missing => {
try_write_time_zone_gmt(w, gmt_offset, zone_symbols, true)?
.map_err(|_| DateTimeWriteError::MissingNames(field))
}
GetSymbolForTimeZoneError::MissingNames(f) => {
write_time_zone_missing(w)?;
Err(DateTimeWriteError::MissingNames(f))
}
},
Ok(s) => Ok(w.write_str(s)?),
},
Some(CustomTimeZone { gmt_offset, .. }) => {
// Required time zone fields not present in input
try_write_time_zone_gmt(w, gmt_offset, zone_symbols, false)?
.map_err(|_| DateTimeWriteError::MissingInputField("metazone_id"))
}
},
(
FieldSymbol::TimeZone(_)
| FieldSymbol::Day(_)
| FieldSymbol::Second(Second::Millisecond),
_,
) => {
(FieldSymbol::TimeZone(_), _) => {
debug_assert!(false, "unreachable: time zone formatted in its own fn");
Err(DateTimeWriteError::UnsupportedField(field))
}
(FieldSymbol::Day(_) | FieldSymbol::Second(Second::Millisecond), _) => {
w.with_part(Part::ERROR, |w| {
w.write_str("{unsupported:")?;
w.write_char(char::from(field.symbol))?;
Expand All @@ -830,6 +777,92 @@ where
})
}

// #[allow(clippy::too_many_arguments)]
pub(crate) fn try_write_zone<W, ZS>(
field_symbol: fields::TimeZone,
field_length: FieldLength,
datetime: &ExtractedDateTimeInput,
zone_symbols: Option<&ZS>,
_fdf: Option<&FixedDecimalFormatter>,
w: &mut W,
) -> Result<Result<(), DateTimeWriteError>, fmt::Error>
where
W: writeable::PartsWrite + ?Sized,
ZS: ZoneSymbols,
{
fn write_time_zone_missing(
w: &mut (impl writeable::PartsWrite + ?Sized),
) -> Result<(), fmt::Error> {
w.with_part(Part::ERROR, |w| "{GMT+?}".write_to(w))
}

fn try_write_time_zone_gmt(
w: &mut (impl writeable::PartsWrite + ?Sized),
_gmt_offset: Option<GmtOffset>,
zone_symbols: Option<&impl ZoneSymbols>,
_graceful: bool,
) -> Result<Result<(), DateTimeWriteError>, fmt::Error> {
#[allow(clippy::bind_instead_of_map)] // TODO: Use proper formatting logic here
Ok(
match zone_symbols
.ok_or(DateTimeWriteError::MissingZoneSymbols)
.and_then(|_zs| {
// TODO: Use proper formatting logic here
Ok("{todo}")
}) {
Err(e) => Err(e),
Ok(s) => Ok(s.write_to(w)?),
},
)
}

// for errors only:
let field = Field {
symbol: FieldSymbol::TimeZone(field_symbol),
length: field_length,
};

// TODO: Implement proper formatting logic here
Ok(match datetime.time_zone() {
None => {
write_time_zone_missing(w)?;
Err(DateTimeWriteError::MissingInputField("time_zone"))
}
Some(CustomTimeZone {
#[cfg(feature = "experimental")]
gmt_offset,
metazone_id: Some(metazone_id),
time_zone_id,
..
}) => match zone_symbols
.ok_or(GetSymbolForTimeZoneError::MissingNames(field))
.and_then(|zs| zs.get_generic_short_for_zone(metazone_id, time_zone_id))
{
Err(e) => match e {
GetSymbolForTimeZoneError::TypeTooNarrow => {
write_time_zone_missing(w)?;
Err(DateTimeWriteError::MissingNames(field))
}
#[cfg(feature = "experimental")]
GetSymbolForTimeZoneError::Missing => {
try_write_time_zone_gmt(w, gmt_offset, zone_symbols, true)?
.map_err(|_| DateTimeWriteError::MissingNames(field))
}
GetSymbolForTimeZoneError::MissingNames(f) => {
write_time_zone_missing(w)?;
Err(DateTimeWriteError::MissingNames(f))
}
},
Ok(s) => Ok(w.write_str(s)?),
},
Some(CustomTimeZone { gmt_offset, .. }) => {
// Required time zone fields not present in input
try_write_time_zone_gmt(w, gmt_offset, zone_symbols, false)?
.map_err(|_| DateTimeWriteError::MissingInputField("metazone_id"))
}
})
}

/// What data is required to format a given pattern.
#[derive(Default)]
pub struct RequiredData {
Expand Down
Loading

0 comments on commit 8e7e8e7

Please sign in to comment.