Skip to content

Commit b3eebc3

Browse files
committed
Add UtcOffset struct
1 parent 7484b25 commit b3eebc3

File tree

5 files changed

+102
-96
lines changed

5 files changed

+102
-96
lines changed

src/builtins/core/timezone.rs

Lines changed: 74 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
use alloc::string::String;
44
use alloc::{vec, vec::Vec};
55

6+
use ixdtf::parsers::records::{TimeZoneRecord, UtcOffsetRecord};
67
use num_traits::ToPrimitive;
78

89
use crate::builtins::core::duration::DateDuration;
910
use crate::parsers::{
10-
parse_allowed_timezone_formats, parse_identifier, FormattableOffset, FormattableTime, Precision,
11+
parse_allowed_timezone_formats, parse_identifier, parse_offset, FormattableOffset,
12+
FormattableTime, Precision,
1113
};
1214
use crate::provider::{TimeZoneOffset, TimeZoneProvider};
1315
use crate::{
@@ -21,20 +23,85 @@ use crate::{Calendar, Sign};
2123

2224
const NS_IN_HOUR: i128 = 60 * 60 * 1000 * 1000 * 1000;
2325

26+
/// A UTC time zone offset stored in minutes
27+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28+
pub struct UtcOffset(pub(crate) i16);
29+
30+
impl UtcOffset {
31+
pub(crate) fn from_ixdtf_record(record: UtcOffsetRecord) -> Self {
32+
// NOTE: ixdtf parser restricts minute/second to 0..=60
33+
debug_assert!(record.hour <= 60);
34+
debug_assert!(record.minute <= 60);
35+
let minutes = i16::from(record.hour) * 60 + record.minute as i16;
36+
Self(minutes * i16::from(record.sign as i8))
37+
}
38+
39+
pub fn to_string(&self) -> TemporalResult<String> {
40+
let sign = if self.0 < 0 {
41+
Sign::Negative
42+
} else {
43+
Sign::Positive
44+
};
45+
let hour = (self.0.abs() / 60) as u8;
46+
let minute = (self.0.abs() % 60) as u8;
47+
let formattable_offset = FormattableOffset {
48+
sign,
49+
time: FormattableTime {
50+
hour,
51+
minute,
52+
second: 0,
53+
nanosecond: 0,
54+
precision: Precision::Minute,
55+
include_sep: true,
56+
},
57+
};
58+
Ok(formattable_offset.to_string())
59+
}
60+
}
61+
62+
impl core::str::FromStr for UtcOffset {
63+
type Err = TemporalError;
64+
fn from_str(s: &str) -> Result<Self, Self::Err> {
65+
let mut cursor = s.chars().peekable();
66+
match parse_offset(&mut cursor)? {
67+
Some(offset) => Ok(Self(offset)),
68+
None => Err(TemporalError::range().with_message("Invalid offset")),
69+
}
70+
}
71+
}
72+
2473
// TODO: Potentially migrate to Cow<'a, str>
2574
// TODO: There may be an argument to have Offset minutes be a (Cow<'a, str>,, i16) to
2675
// prevent allocations / writing, TBD
27-
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
76+
#[derive(Debug, Clone, PartialEq, Eq)]
2877
pub enum TimeZone {
2978
IanaIdentifier(String),
30-
OffsetMinutes(i16),
79+
UtcOffset(UtcOffset),
3180
}
3281

3382
impl TimeZone {
83+
// Create a `TimeZone` from an ixdtf `TimeZoneRecord`.
84+
#[inline]
85+
pub(crate) fn from_time_zone_record(record: TimeZoneRecord) -> TemporalResult<Self> {
86+
let timezone = match record {
87+
TimeZoneRecord::Name(s) => {
88+
TimeZone::IanaIdentifier(String::from_utf8_lossy(s).into_owned())
89+
}
90+
TimeZoneRecord::Offset(offset_record) => {
91+
let offset = UtcOffset::from_ixdtf_record(offset_record);
92+
TimeZone::UtcOffset(offset)
93+
}
94+
// TimeZoneRecord is non_exhaustive, but all current branches are matching.
95+
_ => return Err(TemporalError::assert()),
96+
};
97+
98+
Ok(timezone)
99+
}
100+
34101
/// Parses a `TimeZone` from a provided `&str`.
35102
pub fn try_from_identifier_str(identifier: &str) -> TemporalResult<Self> {
36103
if identifier == "Z" {
37-
return Ok(TimeZone::OffsetMinutes(0));
104+
return Ok(TimeZone::UtcOffset(UtcOffset(0)));
38105
}
39106
parse_identifier(identifier)
40107
}
@@ -52,27 +119,7 @@ impl TimeZone {
52119
pub fn identifier(&self) -> TemporalResult<String> {
53120
match self {
54121
TimeZone::IanaIdentifier(s) => Ok(s.clone()),
55-
TimeZone::OffsetMinutes(m) => {
56-
let sign = if *m < 0 {
57-
Sign::Negative
58-
} else {
59-
Sign::Positive
60-
};
61-
let hour = (m.abs() / 60) as u8;
62-
let minute = (m.abs() % 60) as u8;
63-
let formattable_offset = FormattableOffset {
64-
sign,
65-
time: FormattableTime {
66-
hour,
67-
minute,
68-
second: 0,
69-
nanosecond: 0,
70-
precision: Precision::Minute,
71-
include_sep: true,
72-
},
73-
};
74-
Ok(formattable_offset.to_string())
75-
}
122+
TimeZone::UtcOffset(offset) => offset.to_string(),
76123
}
77124
}
78125
}
@@ -108,7 +155,7 @@ impl TimeZone {
108155
// 1. Let parseResult be ! ParseTimeZoneIdentifier(timeZone).
109156
match self {
110157
// 2. If parseResult.[[OffsetMinutes]] is not empty, return parseResult.[[OffsetMinutes]] × (60 × 10**9).
111-
Self::OffsetMinutes(minutes) => Ok(i128::from(*minutes) * 60_000_000_000i128),
158+
Self::UtcOffset(offset) => Ok(i128::from(offset.0) * 60_000_000_000i128),
112159
// 3. Return GetNamedTimeZoneOffsetNanoseconds(parseResult.[[Name]], epochNs).
113160
Self::IanaIdentifier(identifier) => provider
114161
.get_named_tz_offset_nanoseconds(identifier, utc_epoch)
@@ -137,7 +184,7 @@ impl TimeZone {
137184
// 1.Let parseResult be ! ParseTimeZoneIdentifier(timeZone).
138185
let possible_nanoseconds = match self {
139186
// 2. If parseResult.[[OffsetMinutes]] is not empty, then
140-
Self::OffsetMinutes(minutes) => {
187+
Self::UtcOffset(UtcOffset(minutes)) => {
141188
// a. Let balanced be
142189
// BalanceISODateTime(isoDateTime.[[ISODate]].[[Year]],
143190
// isoDateTime.[[ISODate]].[[Month]],

src/builtins/core/zoneddatetime.rs

Lines changed: 7 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22
33
use alloc::string::String;
44
use core::{cmp::Ordering, num::NonZeroU128};
5-
use ixdtf::parsers::records::{TimeZoneRecord, UtcOffsetRecordOrZ};
5+
use ixdtf::parsers::records::UtcOffsetRecordOrZ;
66
use tinystr::TinyAsciiStr;
77

88
use crate::{
99
builtins::core::{
1010
calendar::Calendar,
1111
duration::normalized::{NormalizedDurationRecord, NormalizedTimeDuration},
12-
timezone::TimeZone,
12+
timezone::{TimeZone, UtcOffset},
1313
Duration, Instant, PlainDate, PlainDateTime, PlainTime,
1414
},
1515
iso::{IsoDate, IsoDateTime, IsoTime},
@@ -19,9 +19,7 @@ use crate::{
1919
ResolvedRoundingOptions, RoundingIncrement, TemporalRoundingMode, TemporalUnit,
2020
ToStringRoundingOptions, UnitGroup,
2121
},
22-
parsers::{
23-
self, parse_offset, FormattableOffset, FormattableTime, IxdtfStringBuilder, Precision,
24-
},
22+
parsers::{self, FormattableOffset, FormattableTime, IxdtfStringBuilder, Precision},
2523
partial::{PartialDate, PartialTime},
2624
provider::TimeZoneProvider,
2725
rounding::{IncrementRounder, Round},
@@ -38,7 +36,7 @@ pub struct PartialZonedDateTime {
3836
/// The `PartialTime` portion of a `PartialZonedDateTime`
3937
pub time: PartialTime,
4038
/// An optional offset string
41-
pub offset: Option<String>,
39+
pub offset: Option<UtcOffset>,
4240
/// The time zone value of a partial time zone.
4341
pub timezone: Option<TimeZone>,
4442
}
@@ -359,15 +357,9 @@ impl ZonedDateTime {
359357
};
360358

361359
// Handle time zones
362-
let offset = partial
360+
let offset_nanos = partial
363361
.offset
364-
.map(|offset| {
365-
let mut cursor = offset.chars().peekable();
366-
parse_offset(&mut cursor)
367-
})
368-
.transpose()?;
369-
370-
let offset_nanos = offset.map(|minutes| i64::from(minutes) * 60_000_000_000);
362+
.map(|offset| i64::from(offset.0) * 60_000_000_000);
371363

372364
let timezone = partial.timezone.unwrap_or_default();
373365

@@ -882,18 +874,7 @@ impl ZonedDateTime {
882874
// NOTE (nekevss): `parse_zoned_date_time` guarantees that this value exists.
883875
let annotation = parse_result.tz.temporal_unwrap()?;
884876

885-
let timezone = match annotation.tz {
886-
TimeZoneRecord::Name(s) => {
887-
TimeZone::IanaIdentifier(String::from_utf8_lossy(s).into_owned())
888-
}
889-
TimeZoneRecord::Offset(offset_record) => {
890-
// NOTE: ixdtf parser restricts minute/second to 0..=60
891-
let minutes = i16::from(offset_record.hour) * 60 + offset_record.minute as i16;
892-
TimeZone::OffsetMinutes(minutes * i16::from(offset_record.sign as i8))
893-
}
894-
// TimeZoneRecord is non_exhaustive, but all current branches are matching.
895-
_ => return Err(TemporalError::assert()),
896-
};
877+
let timezone = TimeZone::from_time_zone_record(annotation.tz)?;
897878

898879
let (offset_nanos, is_exact) = parse_result
899880
.offset

src/lib.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,10 @@ pub mod time {
9696
}
9797

9898
pub use crate::builtins::{
99-
calendar::Calendar, core::timezone::TimeZone, DateDuration, Duration, Instant, Now, PlainDate,
100-
PlainDateTime, PlainMonthDay, PlainTime, PlainYearMonth, TimeDuration, ZonedDateTime,
99+
calendar::Calendar,
100+
core::timezone::{TimeZone, UtcOffset},
101+
DateDuration, Duration, Instant, Now, PlainDate, PlainDateTime, PlainMonthDay, PlainTime,
102+
PlainYearMonth, TimeDuration, ZonedDateTime,
101103
};
102104

103105
/// A library specific trait for unwrapping assertions.

src/options/relative_to.rs

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
//! RelativeTo rounding option
22
3-
use alloc::string::String;
4-
53
use crate::builtins::core::zoneddatetime::interpret_isodatetime_offset;
64
use crate::builtins::core::{calendar::Calendar, timezone::TimeZone, PlainDate, ZonedDateTime};
75
use crate::iso::{IsoDate, IsoTime};
86
use crate::options::{ArithmeticOverflow, Disambiguation, OffsetDisambiguation};
97
use crate::parsers::parse_date_time;
108
use crate::provider::TimeZoneProvider;
11-
use crate::{TemporalError, TemporalResult, TemporalUnwrap};
9+
use crate::{TemporalResult, TemporalUnwrap};
1210

13-
use ixdtf::parsers::records::{TimeZoneRecord, UtcOffsetRecordOrZ};
11+
use ixdtf::parsers::records::UtcOffsetRecordOrZ;
1412

1513
// ==== RelativeTo Object ====
1614

@@ -62,18 +60,7 @@ impl RelativeTo {
6260
.into());
6361
};
6462

65-
let timezone = match annotation.tz {
66-
TimeZoneRecord::Name(s) => {
67-
TimeZone::IanaIdentifier(String::from_utf8_lossy(s).into_owned())
68-
}
69-
TimeZoneRecord::Offset(offset_record) => {
70-
// NOTE: ixdtf parser restricts minute/second to 0..=60
71-
let minutes = i16::from(offset_record.hour) * 60 + offset_record.minute as i16;
72-
TimeZone::OffsetMinutes(minutes * i16::from(offset_record.sign as i8))
73-
}
74-
// TimeZoneRecord is non_exhaustive, but all current branches are matching.
75-
_ => return Err(TemporalError::assert()),
76-
};
63+
let timezone = TimeZone::from_time_zone_record(annotation.tz)?;
7764

7865
let (offset_nanos, is_exact) = result
7966
.offset

src/parsers/timezone.rs

Lines changed: 14 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
use alloc::borrow::ToOwned;
2-
use alloc::string::String;
32
use core::{iter::Peekable, str::Chars};
4-
use ixdtf::parsers::{
5-
records::{TimeZoneRecord, UtcOffsetRecord, UtcOffsetRecordOrZ},
6-
IxdtfParser,
7-
};
3+
use ixdtf::parsers::{records::UtcOffsetRecordOrZ, IxdtfParser};
84

9-
use crate::{TemporalError, TemporalResult, TimeZone};
5+
use crate::{builtins::timezone::UtcOffset, TemporalError, TemporalResult, TimeZone};
106

117
use super::{parse_ixdtf, ParseVariant};
128

@@ -34,45 +30,38 @@ pub(crate) fn parse_allowed_timezone_formats(s: &str) -> Option<TimeZone> {
3430
};
3531

3632
if let Some(annotation) = annotation {
37-
match annotation.tz {
38-
TimeZoneRecord::Name(s) => {
39-
let identifier = String::from_utf8_lossy(s).into_owned();
40-
return Some(TimeZone::IanaIdentifier(identifier));
41-
}
42-
TimeZoneRecord::Offset(offset) => return Some(timezone_from_offset_record(offset)),
43-
_ => {}
44-
}
33+
return TimeZone::from_time_zone_record(annotation.tz).ok();
4534
};
4635

4736
if let Some(offset) = offset {
4837
match offset {
4938
UtcOffsetRecordOrZ::Z => return Some(TimeZone::default()),
50-
UtcOffsetRecordOrZ::Offset(offset) => return Some(timezone_from_offset_record(offset)),
39+
UtcOffsetRecordOrZ::Offset(offset) => {
40+
return Some(TimeZone::UtcOffset(UtcOffset::from_ixdtf_record(offset)))
41+
}
5142
}
5243
}
5344

5445
None
5546
}
5647

57-
fn timezone_from_offset_record(record: UtcOffsetRecord) -> TimeZone {
58-
let minutes = (record.hour as i16 * 60) + record.minute as i16 + (record.second as i16 / 60);
59-
TimeZone::OffsetMinutes(minutes * record.sign as i16)
60-
}
61-
6248
#[inline]
6349
pub(crate) fn parse_identifier(source: &str) -> TemporalResult<TimeZone> {
6450
let mut cursor = source.chars().peekable();
65-
if cursor.peek().is_some_and(is_ascii_sign) {
66-
let offset_minutes = parse_offset(&mut cursor)?;
67-
return Ok(TimeZone::OffsetMinutes(offset_minutes));
51+
if let Some(offset) = parse_offset(&mut cursor)? {
52+
return Ok(TimeZone::UtcOffset(UtcOffset(offset)));
6853
} else if parse_iana_component(&mut cursor) {
6954
return Ok(TimeZone::IanaIdentifier(source.to_owned()));
7055
}
7156
Err(TemporalError::range().with_message("Invalid TimeZone Identifier"))
7257
}
7358

7459
#[inline]
75-
pub(crate) fn parse_offset(chars: &mut Peekable<Chars<'_>>) -> TemporalResult<i16> {
60+
pub(crate) fn parse_offset(chars: &mut Peekable<Chars<'_>>) -> TemporalResult<Option<i16>> {
61+
if chars.peek().is_none() || !chars.peek().is_some_and(is_ascii_sign) {
62+
return Ok(None);
63+
}
64+
7665
let sign = chars.next().map_or(1, |c| if c == '+' { 1 } else { -1 });
7766
// First offset portion
7867
let hours = parse_digit_pair(chars)?;
@@ -90,7 +79,7 @@ pub(crate) fn parse_offset(chars: &mut Peekable<Chars<'_>>) -> TemporalResult<i1
9079
None => 0,
9180
};
9281

93-
Ok((hours * 60 + minutes) * sign)
82+
Ok(Some((hours * 60 + minutes) * sign))
9483
}
9584

9685
fn parse_digit_pair(chars: &mut Peekable<Chars<'_>>) -> TemporalResult<i16> {

0 commit comments

Comments
 (0)