Skip to content

Commit a1f5503

Browse files
authored
Fixes for EpochNanoseconds and Offset parsing (#223)
This PR contains some fixes that was causing `IsoDateTime::from_epoch_nanoseconds` to throw incorrectly along with fixing the offset parsing that was being too forgiving and therefore not throwing errors when it should.
1 parent 098322f commit a1f5503

File tree

8 files changed

+112
-31
lines changed

8 files changed

+112
-31
lines changed

src/builtins/core/datetime.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ impl PlainDateTime {
8787
offset: i64,
8888
calendar: Calendar,
8989
) -> TemporalResult<Self> {
90-
let iso = IsoDateTime::from_epoch_nanos(&instant.as_i128(), offset)?;
90+
let iso = IsoDateTime::from_epoch_nanos(instant.epoch_nanoseconds(), offset)?;
9191
Ok(Self { iso, calendar })
9292
}
9393

src/builtins/core/instant.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ impl Instant {
5252
/// Temporal-Proposal equivalent: `AddInstant`.
5353
pub(crate) fn add_to_instant(&self, duration: &TimeDuration) -> TemporalResult<Self> {
5454
let norm = NormalizedTimeDuration::from_time_duration(duration);
55-
let result = self.epoch_nanoseconds() + norm.0;
55+
let result = self.epoch_nanoseconds().0 + norm.0;
5656
Ok(Self::from(EpochNanoseconds::try_from(result)?))
5757
}
5858

@@ -231,8 +231,8 @@ impl Instant {
231231

232232
/// Returns the `epochNanoseconds` value for this `Instant`.
233233
#[must_use]
234-
pub fn epoch_nanoseconds(&self) -> i128 {
235-
self.as_i128()
234+
pub fn epoch_nanoseconds(&self) -> &EpochNanoseconds {
235+
&self.0
236236
}
237237

238238
// TODO: May end up needing a provider API during impl
@@ -350,8 +350,8 @@ mod tests {
350350
let max_instant = Instant::try_new(max).unwrap();
351351
let min_instant = Instant::try_new(min).unwrap();
352352

353-
assert_eq!(max_instant.epoch_nanoseconds(), max);
354-
assert_eq!(min_instant.epoch_nanoseconds(), min);
353+
assert_eq!(max_instant.epoch_nanoseconds().0, max);
354+
assert_eq!(min_instant.epoch_nanoseconds().0, min);
355355

356356
let max_plus_one = NS_MAX_INSTANT + 1;
357357
let min_minus_one = NS_MIN_INSTANT - 1;

src/builtins/core/timezone.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ impl TimeZone {
141141
provider: &impl TimeZoneProvider,
142142
) -> TemporalResult<IsoDateTime> {
143143
let nanos = self.get_offset_nanos_for(instant.as_i128(), provider)?;
144-
IsoDateTime::from_epoch_nanos(&instant.as_i128(), nanos.to_i64().unwrap_or(0))
144+
IsoDateTime::from_epoch_nanos(instant.epoch_nanoseconds(), nanos.to_i64().unwrap_or(0))
145145
}
146146

147147
/// Get the offset for this current `TimeZoneSlot`.

src/builtins/core/zoneddatetime.rs

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ impl ZonedDateTime {
192192
.get_iso_datetime_for(&self.instant, provider)?;
193193
// 5. Return ? RoundRelativeDuration(difference, ns2, dateTime, timeZone, calendar, largestUnit, roundingIncrement, smallestUnit, roundingMode).
194194
diff.round_relative_duration(
195-
other.epoch_nanoseconds(),
195+
other.epoch_nanoseconds().as_i128(),
196196
&PlainDateTime::new_unchecked(iso, self.calendar().clone()),
197197
Some((self.timezone(), provider)),
198198
resolved_options,
@@ -214,7 +214,7 @@ impl ZonedDateTime {
214214
// 3. Let endDateTime be GetISODateTimeFor(timeZone, ns2).
215215
let end = self.tz.get_iso_datetime_for(&other.instant, provider)?;
216216
// 4. If ns2 - ns1 < 0, let sign be -1; else let sign be 1.
217-
let sign = if other.epoch_nanoseconds() - self.epoch_nanoseconds() < 0 {
217+
let sign = if other.epoch_nanoseconds().as_i128() - self.epoch_nanoseconds().as_i128() < 0 {
218218
Sign::Negative
219219
} else {
220220
Sign::Positive
@@ -254,7 +254,7 @@ impl ZonedDateTime {
254254
)?;
255255
// d. Set timeDuration to TimeDurationFromEpochNanosecondsDifference(ns2, intermediateNs).
256256
time_duration = NormalizedTimeDuration::from_nanosecond_difference(
257-
other.epoch_nanoseconds(),
257+
other.epoch_nanoseconds().as_i128(),
258258
intermediate_ns.0,
259259
)?;
260260
// e. Let timeSign be TimeDurationSign(timeDuration).
@@ -428,7 +428,7 @@ impl ZonedDateTime {
428428

429429
/// Returns the `epochNanoseconds` value of this `ZonedDateTime`.
430430
#[must_use]
431-
pub fn epoch_nanoseconds(&self) -> i128 {
431+
pub fn epoch_nanoseconds(&self) -> &EpochNanoseconds {
432432
self.instant.epoch_nanoseconds()
433433
}
434434

@@ -445,13 +445,21 @@ impl ZonedDateTime {
445445
/// Creates a new `ZonedDateTime` from the current `ZonedDateTime`
446446
/// combined with the provided `TimeZone`.
447447
pub fn with_timezone(&self, timezone: TimeZone) -> TemporalResult<Self> {
448-
Self::try_new(self.epoch_nanoseconds(), self.calendar.clone(), timezone)
448+
Self::try_new(
449+
self.epoch_nanoseconds().as_i128(),
450+
self.calendar.clone(),
451+
timezone,
452+
)
449453
}
450454

451455
/// Creates a new `ZonedDateTime` from the current `ZonedDateTime`
452456
/// combined with the provided `Calendar`.
453457
pub fn with_calendar(&self, calendar: Calendar) -> TemporalResult<Self> {
454-
Self::try_new(self.epoch_nanoseconds(), calendar, self.tz.clone())
458+
Self::try_new(
459+
self.epoch_nanoseconds().as_i128(),
460+
calendar,
461+
self.tz.clone(),
462+
)
455463
}
456464

457465
/// Compares one `ZonedDateTime` to another `ZonedDateTime` using their
@@ -487,8 +495,11 @@ impl ZonedDateTime {
487495
// 10. Else,
488496
// a. Assert: direction is previous.
489497
// b. Let transition be GetNamedTimeZonePreviousTransition(timeZone, zonedDateTime.[[EpochNanoseconds]]).
490-
let transition =
491-
provider.get_named_tz_transition(identifier, self.epoch_nanoseconds(), direction)?;
498+
let transition = provider.get_named_tz_transition(
499+
identifier,
500+
self.epoch_nanoseconds().as_i128(),
501+
direction,
502+
)?;
492503

493504
// 11. If transition is null, return null.
494505
// 12. Return ! CreateTemporalZonedDateTime(transition, timeZone, zonedDateTime.[[Calendar]]).
@@ -606,7 +617,7 @@ impl ZonedDateTime {
606617
pub fn offset_with_provider(&self, provider: &impl TimeZoneProvider) -> TemporalResult<String> {
607618
let offset = self
608619
.tz
609-
.get_offset_nanos_for(self.epoch_nanoseconds(), provider)?;
620+
.get_offset_nanos_for(self.epoch_nanoseconds().as_i128(), provider)?;
610621
Ok(nanoseconds_to_formattable_offset(offset).to_string())
611622
}
612623

@@ -616,7 +627,7 @@ impl ZonedDateTime {
616627
) -> TemporalResult<i64> {
617628
let offset = self
618629
.tz
619-
.get_offset_nanos_for(self.epoch_nanoseconds(), provider)?;
630+
.get_offset_nanos_for(self.epoch_nanoseconds().as_i128(), provider)?;
620631
Ok(offset as i64)
621632
}
622633
}
@@ -1139,6 +1150,7 @@ mod tests {
11391150
options::{DifferenceSettings, Disambiguation, OffsetDisambiguation, TemporalUnit},
11401151
partial::{PartialDate, PartialTime, PartialZonedDateTime},
11411152
primitive::FiniteF64,
1153+
time::EpochNanoseconds,
11421154
tzdb::FsTzdbProvider,
11431155
Calendar, MonthCode, TimeZone,
11441156
};
@@ -1245,10 +1257,13 @@ mod tests {
12451257
)
12461258
.unwrap();
12471259

1248-
assert_eq!(start_of_day.epoch_nanoseconds(), -1601753400000000000);
1260+
assert_eq!(
1261+
start_of_day.epoch_nanoseconds(),
1262+
&EpochNanoseconds(-1601753400000000000)
1263+
);
12491264
assert_eq!(
12501265
midnight_disambiguated.epoch_nanoseconds(),
1251-
-1601751600000000000
1266+
&EpochNanoseconds(-1601751600000000000)
12521267
);
12531268
let diff = start_of_day
12541269
.instant

src/epoch_nanoseconds.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@ impl TryFrom<f64> for EpochNanoseconds {
3838
}
3939
}
4040

41+
// Potential TODO: Build out primitive arthmetic methods if needed.
42+
impl EpochNanoseconds {
43+
pub fn as_i128(&self) -> i128 {
44+
self.0
45+
}
46+
}
47+
4148
/// Utility for determining if the nanos are within a valid range.
4249
#[inline]
4350
#[must_use]

src/iso.rs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ use crate::{
4444
utils, TemporalResult, TemporalUnwrap, NS_PER_DAY,
4545
};
4646
use icu_calendar::{Date as IcuDate, Iso};
47-
use num_traits::{cast::FromPrimitive, AsPrimitive, Euclid, ToPrimitive};
47+
use num_traits::{cast::FromPrimitive, AsPrimitive, Euclid};
4848

4949
/// `IsoDateTime` is the record of the `IsoDate` and `IsoTime` internal slots.
5050
#[non_exhaustive]
@@ -77,19 +77,19 @@ impl IsoDateTime {
7777
// TODO: Move away from offset use of f64
7878
/// Creates an `IsoDateTime` from a `BigInt` of epochNanoseconds.
7979
#[allow(clippy::neg_cmp_op_on_partial_ord)]
80-
pub(crate) fn from_epoch_nanos(nanos: &i128, offset: i64) -> TemporalResult<Self> {
80+
pub(crate) fn from_epoch_nanos(
81+
epoch_nanoseconds: &EpochNanoseconds,
82+
offset: i64,
83+
) -> TemporalResult<Self> {
8184
// Skip the assert as nanos should be validated by Instant.
8285
// TODO: Determine whether value needs to be validated as integral.
8386
// Get the component ISO parts
84-
let mathematical_nanos = nanos.to_i64().ok_or_else(|| {
85-
TemporalError::range().with_message("nanos was not within a valid range.")
86-
})?;
8787

8888
// 2. Let remainderNs be epochNanoseconds modulo 10^6.
89-
let remainder_nanos = mathematical_nanos.rem_euclid(1_000_000);
89+
let remainder_nanos = epoch_nanoseconds.0.rem_euclid(1_000_000);
9090

9191
// 3. Let epochMilliseconds be 𝔽((epochNanoseconds - remainderNs) / 10^6).
92-
let epoch_millis = (mathematical_nanos - remainder_nanos) / 1_000_000;
92+
let epoch_millis = (epoch_nanoseconds.0 - remainder_nanos).div_euclid(1_000_000) as i64;
9393

9494
let (year, month, day) = utils::ymd_from_epoch_milliseconds(epoch_millis);
9595

@@ -103,11 +103,11 @@ impl IsoDateTime {
103103
let millis = epoch_millis.rem_euclid(1000);
104104

105105
// 11. Let microsecond be floor(remainderNs / 1000).
106-
let micros = remainder_nanos.div_euclid(1000);
106+
let micros = remainder_nanos.div_euclid(1_000) as i64;
107107
// 12. Assert: microsecond < 1000.
108108
temporal_assert!(micros < 1000);
109109
// 13. Let nanosecond be remainderNs modulo 1000.
110-
let nanos = remainder_nanos.rem_euclid(1000);
110+
let nanos = remainder_nanos.rem_euclid(1000) as i64;
111111

112112
Ok(Self::balance(
113113
year,

src/parsers/timezone.rs

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ pub(crate) fn parse_offset(chars: &mut Peekable<Chars<'_>>) -> TemporalResult<Op
6666
// First offset portion
6767
let hours = parse_digit_pair(chars)?;
6868

69+
if !(0..24).contains(&hours) {
70+
return Err(TemporalError::range().with_message("Invalid offset hour value."));
71+
}
72+
6973
let sep = chars.peek().is_some_and(|ch| *ch == ':');
7074
if sep {
7175
let _ = chars.next();
@@ -79,7 +83,54 @@ pub(crate) fn parse_offset(chars: &mut Peekable<Chars<'_>>) -> TemporalResult<Op
7983
None => 0,
8084
};
8185

82-
Ok(Some((hours * 60 + minutes) * sign))
86+
if !(0..60).contains(&minutes) {
87+
return Err(TemporalError::range().with_message("Invalid offset hour value."));
88+
}
89+
90+
let result = Some((hours * 60 + minutes) * sign);
91+
92+
// We continue parsing for correctness, but we only care about
93+
// minute precision
94+
95+
let next_peek = chars.peek();
96+
match next_peek {
97+
Some(&':') if sep => _ = chars.next(),
98+
Some(&':') => {
99+
return Err(TemporalError::range().with_message("offset separators do not align."))
100+
}
101+
Some(_) => _ = parse_digit_pair(chars),
102+
None => return Ok(result),
103+
}
104+
105+
let potential_fraction = chars.next();
106+
match potential_fraction {
107+
Some(ch) if ch == '.' || ch == ',' => {
108+
if !chars.peek().is_some_and(|ch| ch.is_ascii_digit()) {
109+
return Err(
110+
TemporalError::range().with_message("fraction separator must have digit after")
111+
);
112+
}
113+
}
114+
Some(_) => return Err(TemporalError::range().with_message("Invalid offset")),
115+
None => return Ok(result),
116+
}
117+
118+
for _ in 0..9 {
119+
let digit_or_end = chars.next().map(|ch| ch.is_ascii_digit());
120+
match digit_or_end {
121+
Some(true) => {}
122+
Some(false) => {
123+
return Err(TemporalError::range().with_message("Not a valid fractional second"))
124+
}
125+
None => break,
126+
}
127+
}
128+
129+
if chars.peek().is_some() {
130+
return Err(TemporalError::range().with_message("Invalid offset"));
131+
}
132+
133+
Ok(result)
83134
}
84135

85136
fn parse_digit_pair(chars: &mut Peekable<Chars<'_>>) -> TemporalResult<i16> {
@@ -103,7 +154,15 @@ fn parse_digit_pair(chars: &mut Peekable<Chars<'_>>) -> TemporalResult<i16> {
103154
let tens = (first.to_digit(10).expect("validated") * 10) as i16;
104155
let ones = second.to_digit(10).expect("validated") as i16;
105156

106-
Ok(tens + ones)
157+
let result = tens + ones;
158+
159+
if !(0..=59).contains(&result) {
160+
return Err(
161+
TemporalError::range().with_message("digit pair not in a valid range of [0..59]")
162+
);
163+
}
164+
165+
Ok(result)
107166
}
108167

109168
fn parse_iana_component(chars: &mut Peekable<Chars<'_>>) -> bool {

temporal_capi/src/instant.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ pub mod ffi {
102102
}
103103

104104
pub fn epoch_nanoseconds(&self) -> I128Nanoseconds {
105-
let ns = self.0.epoch_nanoseconds();
105+
let ns = self.0.epoch_nanoseconds().as_i128();
106106
let is_neg = ns < 0;
107107
let ns = ns.unsigned_abs();
108108

0 commit comments

Comments
 (0)