Skip to content

Commit

Permalink
Special-case Z, Z[Etc/UTC] and Z[Etc/GMT] in IXDTF parser (#5757)
Browse files Browse the repository at this point in the history
  • Loading branch information
robertbastian authored Oct 31, 2024
1 parent fb9f30a commit ed143c9
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 38 deletions.
36 changes: 18 additions & 18 deletions components/datetime/src/time_zone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -385,11 +385,10 @@ impl FormatTimeZone for GenericNonLocationFormat {
return Ok(Err(FormatTimeZoneError::MissingZoneSymbols));
};

let Some(name) = metazone(time_zone_id, local_time, metazone_period).and_then(|mz| {
let Some(name) = names.overrides.get(&time_zone_id).or_else(|| {
names
.overrides
.get(&time_zone_id)
.or_else(|| names.defaults.get(&mz))
.defaults
.get(&metazone(time_zone_id, local_time, metazone_period)?)
}) else {
return Ok(Err(FormatTimeZoneError::Fallback));
};
Expand Down Expand Up @@ -433,12 +432,16 @@ impl FormatTimeZone for SpecificNonLocationFormat {
return Ok(Err(FormatTimeZoneError::MissingZoneSymbols));
};

let Some(name) = metazone(time_zone_id, local_time, metazone_period).and_then(|mz| {
names
.overrides
.get(&(time_zone_id, zone_variant))
.or_else(|| names.defaults.get(&(mz, zone_variant)))
}) else {
let Some(name) = names
.overrides
.get(&(time_zone_id, zone_variant))
.or_else(|| {
names.defaults.get(&(
metazone(time_zone_id, local_time, metazone_period)?,
zone_variant,
))
})
else {
return Ok(Err(FormatTimeZoneError::Fallback));
};

Expand Down Expand Up @@ -651,14 +654,11 @@ impl FormatTimeZone for GenericPartialLocationFormat {
let Some(location) = locations.locations.get(&time_zone_id) else {
return Ok(Err(FormatTimeZoneError::Fallback));
};
let Some(non_location) =
metazone(time_zone_id, local_time, metazone_period).and_then(|mz| {
non_locations
.overrides
.get(&time_zone_id)
.or_else(|| non_locations.defaults.get(&mz))
})
else {
let Some(non_location) = non_locations.overrides.get(&time_zone_id).or_else(|| {
non_locations
.defaults
.get(&metazone(time_zone_id, local_time, metazone_period)?)
}) else {
return Ok(Err(FormatTimeZoneError::Fallback));
};

Expand Down
40 changes: 40 additions & 0 deletions components/datetime/tests/patterns/tests/time_zones.json
Original file line number Diff line number Diff line change
Expand Up @@ -204,5 +204,45 @@
"Z": "+?",
"ZZZZZ": "+?"
}
},
{
"locale": "en",
"datetime": "2021-07-11T12:00:00.000Z",
"expectations": {
"z": "UTC",
"zzzz": "Coordinated Universal Time",

"v": "UTC",
"vvvv": "Coordinated Universal Time",

"VVV": "Unknown City",
"VVVV": "GMT",

"O": "GMT",
"OOOO": "GMT",

"Z": "+0000",
"ZZZZZ": "Z"
}
},
{
"locale": "en",
"datetime": "2021-07-11T12:00:00.000[Etc/GMT]",
"expectations": {
"z": "GMT",
"zzzz": "Greenwich Mean Time",

"v": "GMT",
"vvvv": "Greenwich Mean Time",

"VVV": "Unknown City",
"VVVV": "GMT",

"O": "GMT",
"OOOO": "GMT",

"Z": "+0000",
"ZZZZZ": "Z"
}
}
]
97 changes: 78 additions & 19 deletions components/timezone/src/ixdtf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,20 +141,33 @@ impl IxdtfParser {

struct Intermediate<'a> {
offset: Option<UtcOffsetRecord>,
is_z: bool,
iana_identifier: Option<&'a [u8]>,
date: DateRecord,
time: TimeRecord,
}

impl<'a> Intermediate<'a> {
fn try_from_ixdtf_record(ixdtf_record: &'a IxdtfParseRecord) -> Result<Self, ParseError> {
let (offset, iana_identifier) = match ixdtf_record {
let (offset, is_z, iana_identifier) = match ixdtf_record {
// empty
IxdtfParseRecord {
offset: None,
tz: None,
..
} => (None, false, None),
// -0800
IxdtfParseRecord {
offset: Some(UtcOffsetRecordOrZ::Offset(offset)),
tz: None,
..
} => (Some(*offset), false, None),
// Z
IxdtfParseRecord {
offset, tz: None, ..
} => (offset.map(UtcOffsetRecordOrZ::resolve_rfc_9557), None),
offset: Some(UtcOffsetRecordOrZ::Z),
tz: None,
..
} => (None, true, None),
// [-0800]
IxdtfParseRecord {
offset: None,
Expand All @@ -164,7 +177,7 @@ impl<'a> Intermediate<'a> {
..
}),
..
} => (Some(*offset), None),
} => (Some(*offset), false, None),
// -0800[-0800]
IxdtfParseRecord {
offset: Some(UtcOffsetRecordOrZ::Offset(offset)),
Expand All @@ -178,7 +191,7 @@ impl<'a> Intermediate<'a> {
if offset != offset1 {
return Err(ParseError::InconsistentTimeZoneOffsets);
}
(Some(*offset), None)
(Some(*offset), false, None)
}
// -0800[America/Los_Angeles]
IxdtfParseRecord {
Expand All @@ -189,14 +202,27 @@ impl<'a> Intermediate<'a> {
..
}),
..
} => (Some(*offset), Some(*iana_identifier)),
} => (Some(*offset), false, Some(*iana_identifier)),
// Z[-0800]
IxdtfParseRecord {
offset: Some(UtcOffsetRecordOrZ::Z),
tz:
Some(TimeZoneAnnotation {
tz: TimeZoneRecord::Offset(offset),
..
}),
..
} => (Some(*offset), true, None),
// Z[America/Los_Angeles]
IxdtfParseRecord {
offset: Some(UtcOffsetRecordOrZ::Z),
tz: Some(_),
tz:
Some(TimeZoneAnnotation {
tz: TimeZoneRecord::Name(iana_identifier),
..
}),
..
} => return Err(ParseError::RequiresCalculation),
} => (None, true, Some(*iana_identifier)),
// [America/Los_Angeles]
IxdtfParseRecord {
offset: None,
Expand All @@ -206,14 +232,14 @@ impl<'a> Intermediate<'a> {
..
}),
..
} => (None, Some(*iana_identifier)),
} => (None, false, Some(*iana_identifier)),
// non_exhaustive match: maybe something like [u-tz=uslax] in the future
IxdtfParseRecord {
tz: Some(TimeZoneAnnotation { tz, .. }),
..
} => {
debug_assert!(false, "unexpected TimeZoneRecord: {tz:?}");
(None, None)
(None, false, None)
}
};
let IxdtfParseRecord {
Expand All @@ -227,17 +253,26 @@ impl<'a> Intermediate<'a> {
};
Ok(Self {
offset,
is_z,
iana_identifier,
date,
time,
})
}

fn offset_only(self) -> Result<UtcOffset, ParseError> {
let Some(offset) = self.offset else {
let None = self.iana_identifier else {
return Err(ParseError::MismatchedTimeZoneFields);
};
let None = self.iana_identifier else {
if self.is_z {
if let Some(offset) = self.offset {
if offset != UtcOffsetRecord::zero() {
return Err(ParseError::RequiresCalculation);
}
}
return Ok(UtcOffset::zero());
}
let Some(offset) = self.offset else {
return Err(ParseError::MismatchedTimeZoneFields);
};
UtcOffset::try_from_utc_offset_record(offset)
Expand All @@ -251,6 +286,9 @@ impl<'a> Intermediate<'a> {
return Err(ParseError::MismatchedTimeZoneFields);
};
let Some(iana_identifier) = self.iana_identifier else {
if self.is_z {
return Err(ParseError::RequiresCalculation);
}
return Err(ParseError::MismatchedTimeZoneFields);
};
let time_zone_id = mapper.iana_bytes_to_bcp47(iana_identifier);
Expand All @@ -262,21 +300,42 @@ impl<'a> Intermediate<'a> {
self.time.minute,
self.time.second,
)?;
Ok(time_zone_id.with_offset(None).at_time((iso.date, iso.time)))
let offset = match time_zone_id.as_str() {
"utc" | "gmt" => Some(UtcOffset::zero()),
_ => None,
};
Ok(time_zone_id
.with_offset(offset)
.at_time((iso.date, iso.time)))
}

fn loose(
self,
mapper: TimeZoneIdMapperBorrowed<'_>,
) -> Result<TimeZoneInfo<models::AtTime>, ParseError> {
let offset = match self.offset {
Some(offset) => Some(UtcOffset::try_from_utc_offset_record(offset)?),
None => None,
};
let time_zone_id = match self.iana_identifier {
Some(iana_identifier) => mapper.iana_bytes_to_bcp47(iana_identifier),
Some(iana_identifier) => {
if self.is_z {
return Err(ParseError::RequiresCalculation);
}
mapper.iana_bytes_to_bcp47(iana_identifier)
}
None if self.is_z => TimeZoneBcp47Id(tinystr::tinystr!(8, "utc")),
None => TimeZoneBcp47Id::unknown(),
};
let offset = match self.offset {
Some(offset) => {
if self.is_z && offset != UtcOffsetRecord::zero() {
return Err(ParseError::RequiresCalculation);
}
Some(UtcOffset::try_from_utc_offset_record(offset)?)
}
None => match time_zone_id.as_str() {
"utc" | "gmt" => Some(UtcOffset::zero()),
_ if self.is_z => Some(UtcOffset::zero()),
_ => None,
},
};
let iso = DateTime::<Iso>::try_new_iso(
self.date.year,
self.date.month,
Expand Down Expand Up @@ -757,7 +816,7 @@ mod test {
IxdtfParser::new()
.try_offset_only_iso_from_str("2024-08-08T12:08:19Z[Europe/Zurich]")
.unwrap_err(),
ParseError::RequiresCalculation
ParseError::MismatchedTimeZoneFields
);
}

Expand Down
15 changes: 14 additions & 1 deletion utils/ixdtf/src/parsers/records.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,21 @@ pub struct UtcOffsetRecord {
pub nanosecond: u32,
}

#[non_exhaustive]
impl UtcOffsetRecord {
/// +0000
pub const fn zero() -> Self {
Self {
sign: Sign::Positive,
hour: 0,
minute: 0,
second: 0,
nanosecond: 0,
}
}
}

#[derive(Debug, Clone, Copy, PartialEq)]
#[allow(clippy::exhaustive_enums)] // explicitly A or B
pub enum UtcOffsetRecordOrZ {
Offset(UtcOffsetRecord),
Z,
Expand Down

0 comments on commit ed143c9

Please sign in to comment.