Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions components/datetime/tests/patterns/tests/time_zones.json
Original file line number Diff line number Diff line change
Expand Up @@ -385,5 +385,25 @@
"xxxx": "+0000",
"XXXXX": "Z"
}
},
{
"locale": "en",
"datetime": "2021-07-11T12:00:00.000[Etc/GMT+7]",
"expectations": {
"z": "GMT-7",
"zzzz": "GMT-07:00",

"v": "GMT-7",
"vvvv": "GMT-07:00",

"VVV": "Unknown Location",
"VVVV": "GMT-07:00",

"O": "GMT-7",
"OOOO": "GMT-07:00",

"xxxx": "-0700",
"XXXXX": "-07:00"
}
}
]
42 changes: 16 additions & 26 deletions components/time/src/ixdtf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@

use crate::{
zone::{iana::IanaParserBorrowed, models, InvalidOffsetError, UtcOffset},
DateTime, Time, TimeZone, TimeZoneInfo, ZonedDateTime,
DateTime, Time, TimeZoneInfo, ZonedDateTime,
};
use core::str::FromStr;
use icu_calendar::{AnyCalendarKind, AsCalendar, Date, DateError, Iso, RangeError};
use icu_locale_core::subtags::subtag;
use ixdtf::{
encoding::Utf8,
parsers::IxdtfParser,
Expand Down Expand Up @@ -281,47 +280,38 @@ impl<'a> Intermediate<'a> {
let id = iana_parser.parse_from_utf8(iana_identifier);
let date = Date::<Iso>::try_new_iso(self.date.year, self.date.month, self.date.day)?;
let time = Time::try_from_time_record(&self.time)?;
let offset = match id.as_str() {
"utc" | "gmt" => Some(UtcOffset::zero()),
_ => None,
};
Ok(id
.with_offset(offset)
.with_offset(None)
.at_date_time_iso(DateTime { date, time }))
}

fn lenient(
self,
iana_parser: IanaParserBorrowed<'_>,
) -> Result<TimeZoneInfo<models::AtTime>, ParseError> {
let id = match self.iana_identifier {
let mut zone = match self.iana_identifier {
Some(iana_identifier) => {
if self.is_z {
return Err(ParseError::RequiresCalculation);
}
iana_parser.parse_from_utf8(iana_identifier)
iana_parser
.parse_from_utf8(iana_identifier)
.with_offset(None)
}
None if self.is_z => TimeZone(subtag!("utc")),
None => TimeZone::UNKNOWN,
None if self.is_z => TimeZoneInfo::utc(),
None => TimeZoneInfo::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)?)

if let Some(offset) = self.offset {
let offset = UtcOffset::try_from_utc_offset_record(offset)?;
if zone.offset().is_some_and(|i| i != offset) {
return Err(ParseError::RequiresCalculation);
}
None => match id.as_str() {
"utc" | "gmt" => Some(UtcOffset::zero()),
_ if self.is_z => Some(UtcOffset::zero()),
_ => None,
},
};
zone = zone.id().with_offset(Some(offset));
}
let date = Date::<Iso>::try_new_iso(self.date.year, self.date.month, self.date.day)?;
let time = Time::try_from_time_record(&self.time)?;
Ok(id
.with_offset(offset)
.at_date_time_iso(DateTime { date, time }))
Ok(zone.at_date_time_iso(DateTime { date, time }))
}

#[allow(deprecated)]
Expand Down
119 changes: 109 additions & 10 deletions components/time/src/zone/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -299,35 +299,95 @@ where

impl TimeZone {
/// Associates this [`TimeZone`] with a UTC offset, returning a [`TimeZoneInfo`].
pub const fn with_offset(self, offset: Option<UtcOffset>) -> TimeZoneInfo<models::Base> {
pub const fn with_offset(self, mut offset: Option<UtcOffset>) -> TimeZoneInfo<models::Base> {
let mut id = self;

#[allow(clippy::identity_op, clippy::neg_multiply)]
let correct_offset = match self.0.as_str().as_bytes() {
b"utc" | b"gmt" => Some(UtcOffset::zero()),
b"utce01" => Some(UtcOffset::from_seconds_unchecked(1 * 60 * 60)),
b"utce02" => Some(UtcOffset::from_seconds_unchecked(2 * 60 * 60)),
b"utce03" => Some(UtcOffset::from_seconds_unchecked(3 * 60 * 60)),
b"utce04" => Some(UtcOffset::from_seconds_unchecked(4 * 60 * 60)),
b"utce05" => Some(UtcOffset::from_seconds_unchecked(5 * 60 * 60)),
b"utce06" => Some(UtcOffset::from_seconds_unchecked(6 * 60 * 60)),
b"utce07" => Some(UtcOffset::from_seconds_unchecked(7 * 60 * 60)),
b"utce08" => Some(UtcOffset::from_seconds_unchecked(8 * 60 * 60)),
b"utce09" => Some(UtcOffset::from_seconds_unchecked(9 * 60 * 60)),
b"utce10" => Some(UtcOffset::from_seconds_unchecked(10 * 60 * 60)),
b"utce11" => Some(UtcOffset::from_seconds_unchecked(11 * 60 * 60)),
b"utce12" => Some(UtcOffset::from_seconds_unchecked(12 * 60 * 60)),
b"utce13" => Some(UtcOffset::from_seconds_unchecked(13 * 60 * 60)),
b"utce14" => Some(UtcOffset::from_seconds_unchecked(14 * 60 * 60)),
b"utcw01" => Some(UtcOffset::from_seconds_unchecked(-1 * 60 * 60)),
b"utcw02" => Some(UtcOffset::from_seconds_unchecked(-2 * 60 * 60)),
b"utcw03" => Some(UtcOffset::from_seconds_unchecked(-3 * 60 * 60)),
b"utcw04" => Some(UtcOffset::from_seconds_unchecked(-4 * 60 * 60)),
b"utcw05" => Some(UtcOffset::from_seconds_unchecked(-5 * 60 * 60)),
b"utcw06" => Some(UtcOffset::from_seconds_unchecked(-6 * 60 * 60)),
b"utcw07" => Some(UtcOffset::from_seconds_unchecked(-7 * 60 * 60)),
b"utcw08" => Some(UtcOffset::from_seconds_unchecked(-8 * 60 * 60)),
b"utcw09" => Some(UtcOffset::from_seconds_unchecked(-9 * 60 * 60)),
b"utcw10" => Some(UtcOffset::from_seconds_unchecked(-10 * 60 * 60)),
b"utcw11" => Some(UtcOffset::from_seconds_unchecked(-11 * 60 * 60)),
b"utcw12" => Some(UtcOffset::from_seconds_unchecked(-12 * 60 * 60)),
_ => None,
};

match (correct_offset, offset) {
// The Etc/* zones have fixed defined offsets. By setting them here,
// they won't format as UTC+?.
(Some(c), None) => {
offset = Some(c);

// The Etc/GMT+X zones do not have display names, so they format
// exactly like UNKNOWN with the same offset. For the sake of
// equality, set the ID to UNKNOWN as well.
if id.0.as_str().len() > 3 {
id = Self::UNKNOWN;
}
}
// Garbage offset for a fixed zone, now we know nothing
(Some(c), Some(o)) if c.to_seconds() != o.to_seconds() => {
offset = None;
id = Self::UNKNOWN;
}
_ => {}
}

TimeZoneInfo {
id,
offset,
id: self,
zone_name_timestamp: (),
variant: (),
}
}

/// Converts this [`TimeZone`] into a [`TimeZoneInfo`] without an offset.
pub const fn without_offset(self) -> TimeZoneInfo<models::Base> {
TimeZoneInfo {
offset: None,
id: self,
zone_name_timestamp: (),
variant: (),
}
self.with_offset(None)
}
}

impl TimeZoneInfo<models::Base> {
/// Creates a time zone info with no information.
pub const fn unknown() -> Self {
TimeZone::UNKNOWN.with_offset(None)
Self {
id: TimeZone::UNKNOWN,
offset: None,
zone_name_timestamp: (),
variant: (),
}
}

/// Creates a new [`TimeZoneInfo`] for the UTC time zone.
pub const fn utc() -> Self {
TimeZone(subtag!("utc")).with_offset(Some(UtcOffset::zero()))
TimeZoneInfo {
id: TimeZone(subtag!("utc")),
offset: Some(UtcOffset::zero()),
zone_name_timestamp: (),
variant: (),
}
}

/// Sets the [`ZoneNameTimestamp`] field.
Expand Down Expand Up @@ -500,3 +560,42 @@ impl TimeZoneVariant {
}
}
}

#[test]
fn test_zone_info_equality() {
// offset inferred
assert_eq!(
IanaParser::new().parse("Etc/GMT-8").with_offset(None),
TimeZone::UNKNOWN.with_offset(Some(UtcOffset::from_seconds_unchecked(8 * 60 * 60)))
);
assert_eq!(
IanaParser::new().parse("Etc/UTC").with_offset(None),
TimeZoneInfo::utc()
);
assert_eq!(
IanaParser::new().parse("Etc/GMT").with_offset(None),
IanaParser::new()
.parse("Etc/GMT")
.with_offset(Some(UtcOffset::zero()))
);

// bogus offset removed
assert_eq!(
IanaParser::new()
.parse("Etc/GMT-8")
.with_offset(Some(UtcOffset::from_seconds_unchecked(123))),
TimeZoneInfo::unknown()
);
assert_eq!(
IanaParser::new()
.parse("Etc/UTC")
.with_offset(Some(UtcOffset::from_seconds_unchecked(123))),
TimeZoneInfo::unknown(),
);
assert_eq!(
IanaParser::new()
.parse("Etc/GMT")
.with_offset(Some(UtcOffset::from_seconds_unchecked(123))),
TimeZoneInfo::unknown()
);
}
4 changes: 2 additions & 2 deletions components/time/src/zone/offset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,12 @@ impl UtcOffset {

/// Create a [`UtcOffset`] from a seconds input without checking bounds.
#[inline]
pub fn from_seconds_unchecked(seconds: i32) -> Self {
pub const fn from_seconds_unchecked(seconds: i32) -> Self {
Self(seconds)
}

/// Returns the raw offset value in seconds.
pub fn to_seconds(self) -> i32 {
pub const fn to_seconds(self) -> i32 {
self.0
}

Expand Down