Skip to content

Commit 3925c87

Browse files
authored
Add an EpochNanosecond new type (#116)
This PR adds an `EpochNanosecond` new type for the internal value of `Instant`. The primary goal of this PR is to clean up the external API around dealing with `Instant` nanoseconds while guaranteeing that the `EpochNanoseconds` value is valid.
1 parent 1fc7c16 commit 3925c87

File tree

7 files changed

+99
-87
lines changed

7 files changed

+99
-87
lines changed

src/components/datetime.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ impl PlainDateTime {
7777
offset: f64,
7878
calendar: Calendar,
7979
) -> TemporalResult<Self> {
80-
let iso = IsoDateTime::from_epoch_nanos(&instant.epoch_nanos, offset)?;
80+
let iso = IsoDateTime::from_epoch_nanos(&instant.as_i128(), offset)?;
8181
Ok(Self { iso, calendar })
8282
}
8383

src/components/instant.rs

Lines changed: 66 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,62 @@ use crate::{
1212
parsers::parse_instant,
1313
primitive::FiniteF64,
1414
rounding::{IncrementRounder, Round},
15-
Sign, TemporalError, TemporalResult, TemporalUnwrap,
15+
Sign, TemporalError, TemporalResult, TemporalUnwrap, NS_MAX_INSTANT,
1616
};
1717

18-
use num_traits::{Euclid, FromPrimitive, ToPrimitive};
18+
use num_traits::{Euclid, FromPrimitive};
1919

2020
use super::duration::normalized::NormalizedTimeDuration;
2121

2222
const NANOSECONDS_PER_SECOND: f64 = 1e9;
2323
const NANOSECONDS_PER_MINUTE: f64 = 60f64 * NANOSECONDS_PER_SECOND;
2424
const NANOSECONDS_PER_HOUR: f64 = 60f64 * NANOSECONDS_PER_MINUTE;
2525

26+
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
27+
pub struct EpochNanoseconds(i128);
28+
29+
impl TryFrom<i128> for EpochNanoseconds {
30+
type Error = TemporalError;
31+
fn try_from(value: i128) -> Result<Self, Self::Error> {
32+
if !is_valid_epoch_nanos(&value) {
33+
return Err(TemporalError::range()
34+
.with_message("Instant nanoseconds are not within a valid epoch range."));
35+
}
36+
Ok(Self(value))
37+
}
38+
}
39+
40+
impl TryFrom<u128> for EpochNanoseconds {
41+
type Error = TemporalError;
42+
fn try_from(value: u128) -> Result<Self, Self::Error> {
43+
if (NS_MAX_INSTANT as u128) < value {
44+
return Err(TemporalError::range()
45+
.with_message("Instant nanoseconds are not within a valid epoch range."));
46+
}
47+
Ok(Self(value as i128))
48+
}
49+
}
50+
51+
impl TryFrom<f64> for EpochNanoseconds {
52+
type Error = TemporalError;
53+
fn try_from(value: f64) -> Result<Self, Self::Error> {
54+
let Some(value) = i128::from_f64(value) else {
55+
return Err(TemporalError::range()
56+
.with_message("Instant nanoseconds are not within a valid epoch range."));
57+
};
58+
Self::try_from(value)
59+
}
60+
}
61+
2662
/// The native Rust implementation of `Temporal.Instant`
2763
#[non_exhaustive]
2864
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
29-
pub struct Instant {
30-
pub(crate) epoch_nanos: i128,
65+
pub struct Instant(EpochNanoseconds);
66+
67+
impl From<EpochNanoseconds> for Instant {
68+
fn from(value: EpochNanoseconds) -> Self {
69+
Self(value)
70+
}
3171
}
3272

3373
// ==== Private API ====
@@ -38,17 +78,15 @@ impl Instant {
3878
///
3979
/// Temporal-Proposal equivalent: `AddDurationToOrSubtractDurationFrom`.
4080
pub(crate) fn add_to_instant(&self, duration: &TimeDuration) -> TemporalResult<Self> {
41-
let result = self.epoch_nanoseconds()
81+
let current_nanos = self.epoch_nanoseconds() as f64;
82+
let result = current_nanos
4283
+ duration.nanoseconds.0
4384
+ (duration.microseconds.0 * 1000f64)
4485
+ (duration.milliseconds.0 * 1_000_000f64)
4586
+ (duration.seconds.0 * NANOSECONDS_PER_SECOND)
4687
+ (duration.minutes.0 * NANOSECONDS_PER_MINUTE)
4788
+ (duration.hours.0 * NANOSECONDS_PER_HOUR);
48-
let nanos = i128::from_f64(result).ok_or_else(|| {
49-
TemporalError::range().with_message("Duration added to instant exceeded valid range.")
50-
})?;
51-
Self::try_new(nanos)
89+
Ok(Self::from(EpochNanoseconds::try_from(result)?))
5290
}
5391

5492
// TODO: Add test for `diff_instant`.
@@ -76,10 +114,8 @@ impl Instant {
76114
// Below are the steps from Difference Instant.
77115
// 5. Let diffRecord be DifferenceInstant(instant.[[Nanoseconds]], other.[[Nanoseconds]],
78116
// settings.[[RoundingIncrement]], settings.[[SmallestUnit]], settings.[[RoundingMode]]).
79-
let diff = NormalizedTimeDuration::from_nanosecond_difference(
80-
other.epoch_nanos,
81-
self.epoch_nanos,
82-
)?;
117+
let diff =
118+
NormalizedTimeDuration::from_nanosecond_difference(other.as_i128(), self.as_i128())?;
83119
let (round_record, _) = diff.round(FiniteF64::default(), resolved_options)?;
84120

85121
// 6. Let norm be diffRecord.[[NormalizedTimeDuration]].
@@ -127,21 +163,15 @@ impl Instant {
127163
return Err(TemporalError::range().with_message("Increment exceeded a valid range."));
128164
};
129165

130-
let rounded = IncrementRounder::<i128>::from_positive_parts(self.epoch_nanos, increment)?
166+
let rounded = IncrementRounder::<i128>::from_positive_parts(self.as_i128(), increment)?
131167
.round_as_positive(resolved_options.rounding_mode);
132168

133169
Ok(rounded.into())
134170
}
135171

136-
/// Utility for converting `Instant` to f64.
137-
///
138-
/// # Panics
139-
///
140-
/// This function will panic if called on an invalid `Instant`.
141-
pub(crate) fn to_f64(&self) -> f64 {
142-
self.epoch_nanos
143-
.to_f64()
144-
.expect("A valid instant is representable by f64.")
172+
// Utility for converting `Instant` to `i128`.
173+
pub fn as_i128(&self) -> i128 {
174+
self.0 .0
145175
}
146176
}
147177

@@ -150,25 +180,15 @@ impl Instant {
150180
impl Instant {
151181
/// Create a new validated `Instant`.
152182
#[inline]
153-
pub fn try_new(epoch_nanoseconds: i128) -> TemporalResult<Self> {
154-
if !is_valid_epoch_nanos(&epoch_nanoseconds) {
155-
return Err(TemporalError::range()
156-
.with_message("Instant nanoseconds are not within a valid epoch range."));
157-
}
158-
Ok(Self {
159-
epoch_nanos: epoch_nanoseconds,
160-
})
183+
pub fn try_new(nanoseconds: i128) -> TemporalResult<Self> {
184+
Ok(Self::from(EpochNanoseconds::try_from(nanoseconds)?))
161185
}
162186

163187
pub fn from_epoch_milliseconds(epoch_milliseconds: i128) -> TemporalResult<Self> {
164188
let epoch_nanos = epoch_milliseconds
165189
.checked_mul(1_000_000)
166190
.unwrap_or(i128::MAX);
167-
if !is_valid_epoch_nanos(&epoch_nanos) {
168-
return Err(TemporalError::range()
169-
.with_message("Instant nanoseconds are not within a valid epoch range."));
170-
}
171-
Ok(Self { epoch_nanos })
191+
Self::try_new(epoch_nanos)
172192
}
173193

174194
/// Adds a `Duration` to the current `Instant`, returning an error if the `Duration`
@@ -235,35 +255,26 @@ impl Instant {
235255

236256
/// Returns the `epochSeconds` value for this `Instant`.
237257
#[must_use]
238-
pub fn epoch_seconds(&self) -> f64 {
239-
(&self.epoch_nanos / 1_000_000_000)
240-
.to_f64()
241-
.expect("A validated Instant should be within a valid f64")
242-
.floor()
258+
pub fn epoch_seconds(&self) -> i128 {
259+
self.as_i128() / 1_000_000_000
243260
}
244261

245262
/// Returns the `epochMilliseconds` value for this `Instant`.
246263
#[must_use]
247-
pub fn epoch_milliseconds(&self) -> f64 {
248-
(&self.epoch_nanos / 1_000_000)
249-
.to_f64()
250-
.expect("A validated Instant should be within a valid f64")
251-
.floor()
264+
pub fn epoch_milliseconds(&self) -> i128 {
265+
self.as_i128() / 1_000_000
252266
}
253267

254268
/// Returns the `epochMicroseconds` value for this `Instant`.
255269
#[must_use]
256-
pub fn epoch_microseconds(&self) -> f64 {
257-
(&self.epoch_nanos / 1_000)
258-
.to_f64()
259-
.expect("A validated Instant should be within a valid f64")
260-
.floor()
270+
pub fn epoch_microseconds(&self) -> i128 {
271+
self.as_i128() / 1_000
261272
}
262273

263274
/// Returns the `epochNanoseconds` value for this `Instant`.
264275
#[must_use]
265-
pub fn epoch_nanoseconds(&self) -> f64 {
266-
self.to_f64()
276+
pub fn epoch_nanoseconds(&self) -> i128 {
277+
self.as_i128()
267278
}
268279
}
269280

@@ -326,7 +337,6 @@ mod tests {
326337
primitive::FiniteF64,
327338
NS_MAX_INSTANT, NS_MIN_INSTANT,
328339
};
329-
use num_traits::ToPrimitive;
330340

331341
#[test]
332342
#[allow(clippy::float_cmp)]
@@ -338,8 +348,8 @@ mod tests {
338348
let max_instant = Instant::try_new(max).unwrap();
339349
let min_instant = Instant::try_new(min).unwrap();
340350

341-
assert_eq!(max_instant.epoch_nanoseconds(), max.to_f64().unwrap());
342-
assert_eq!(min_instant.epoch_nanoseconds(), min.to_f64().unwrap());
351+
assert_eq!(max_instant.epoch_nanoseconds(), max);
352+
assert_eq!(min_instant.epoch_nanoseconds(), min);
343353

344354
let max_plus_one = NS_MAX_INSTANT + 1;
345355
let min_minus_one = NS_MIN_INSTANT - 1;

src/components/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ pub use datetime::{PartialDateTime, PlainDateTime};
3232
#[doc(inline)]
3333
pub use duration::Duration;
3434
#[doc(inline)]
35-
pub use instant::Instant;
35+
pub use instant::{EpochNanoseconds, Instant};
3636
#[doc(inline)]
3737
pub use month_day::PlainMonthDay;
3838
#[doc(inline)]

src/components/now.rs

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use crate::{iso::IsoDateTime, TemporalUnwrap};
1313
use super::{
1414
calendar::Calendar,
1515
tz::{TimeZone, TzProvider},
16-
Instant, PlainDateTime,
16+
EpochNanoseconds, Instant, PlainDateTime,
1717
};
1818

1919
/// The Temporal Now object.
@@ -54,14 +54,9 @@ fn system_date_time(
5454
let tz = tz.unwrap_or(sys::get_system_tz_identifier()?.into());
5555
// 3. Let epochNs be SystemUTCEpochNanoseconds().
5656
// TODO: Handle u128 -> i128 better for system nanoseconds
57-
let epoch_ns = sys::get_system_nanoseconds()?;
57+
let epoch_ns = EpochNanoseconds::try_from(sys::get_system_nanoseconds()?)?;
5858
// 4. Return GetISODateTimeFor(timeZone, epochNs).
59-
tz.get_iso_datetime_for(
60-
&Instant {
61-
epoch_nanos: epoch_ns as i128,
62-
},
63-
provider,
64-
)
59+
tz.get_iso_datetime_for(&Instant::from(epoch_ns), provider)
6560
}
6661

6762
#[cfg(feature = "std")]

src/components/tz.rs

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use core::{iter::Peekable, str::Chars};
77

88
use num_traits::ToPrimitive;
99

10+
use crate::components::instant::EpochNanoseconds;
1011
use crate::{
1112
components::{duration::normalized::NormalizedTimeDuration, Instant},
1213
iso::{IsoDate, IsoDateTime},
@@ -96,8 +97,8 @@ impl TimeZone {
9697
instant: &Instant,
9798
provider: &impl TzProvider,
9899
) -> TemporalResult<IsoDateTime> {
99-
let nanos = self.get_offset_nanos_for(instant.epoch_nanos, provider)?;
100-
IsoDateTime::from_epoch_nanos(&instant.epoch_nanos, nanos.to_f64().unwrap_or(0.0))
100+
let nanos = self.get_offset_nanos_for(instant.as_i128(), provider)?;
101+
IsoDateTime::from_epoch_nanos(&instant.as_i128(), nanos.to_f64().unwrap_or(0.0))
101102
}
102103
}
103104

@@ -125,7 +126,7 @@ impl TimeZone {
125126
iso: IsoDateTime,
126127
disambiguation: Disambiguation,
127128
provider: &impl TzProvider,
128-
) -> TemporalResult<i128> {
129+
) -> TemporalResult<EpochNanoseconds> {
129130
// 1. Let possibleEpochNs be ? GetPossibleEpochNanoseconds(timeZone, isoDateTime).
130131
let possible_nanos = self.get_possible_epoch_ns_for(iso, provider)?;
131132
// 2. Return ? DisambiguatePossibleEpochNanoseconds(possibleEpochNs, timeZone, isoDateTime, disambiguation).
@@ -209,22 +210,24 @@ impl TimeZone {
209210
iso: IsoDateTime,
210211
disambiguation: Disambiguation,
211212
provider: &impl TzProvider,
212-
) -> TemporalResult<i128> {
213+
) -> TemporalResult<EpochNanoseconds> {
213214
// 1. Let n be possibleEpochNs's length.
214215
let n = nanos.len();
215216
// 2. If n = 1, then
216217
if n == 1 {
217218
// a. Return possibleEpochNs[0].
218-
return Ok(nanos[0]);
219+
return EpochNanoseconds::try_from(nanos[0]);
219220
// 3. If n ≠ 0, then
220221
} else if n != 0 {
221222
match disambiguation {
222223
// a. If disambiguation is earlier or compatible, then
223224
// i. Return possibleEpochNs[0].
224-
Disambiguation::Compatible | Disambiguation::Earlier => return Ok(nanos[0]),
225+
Disambiguation::Compatible | Disambiguation::Earlier => {
226+
return EpochNanoseconds::try_from(nanos[0])
227+
}
225228
// b. If disambiguation is later, then
226229
// i. Return possibleEpochNs[n - 1].
227-
Disambiguation::Later => return Ok(nanos[n - 1]),
230+
Disambiguation::Later => return EpochNanoseconds::try_from(nanos[n - 1]),
228231
// c. Assert: disambiguation is reject.
229232
// d. Throw a RangeError exception.
230233
Disambiguation::Reject => {
@@ -300,7 +303,7 @@ impl TimeZone {
300303
let possible = self.get_possible_epoch_ns_for(earlier, provider)?;
301304
// f. Assert: possibleEpochNs is not empty.
302305
// g. Return possibleEpochNs[0].
303-
return Ok(possible[0]);
306+
return EpochNanoseconds::try_from(possible[0]);
304307
}
305308
// 17. Assert: disambiguation is compatible or later.
306309
// 18. Let timeDuration be TimeDurationFromComponents(0, 0, 0, 0, 0, nanoseconds).
@@ -322,7 +325,7 @@ impl TimeZone {
322325
let n = possible.len();
323326
// 24. Assert: n ≠ 0.
324327
// 25. Return possibleEpochNs[n - 1].
325-
Ok(possible[n - 1])
328+
EpochNanoseconds::try_from(possible[n - 1])
326329
}
327330
}
328331

src/components/zoneddatetime.rs

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,7 @@ impl ZonedDateTime {
8585
)?;
8686

8787
// 7. Return ? AddInstant(intermediateNs, duration.[[Time]]).
88-
Instant {
89-
epoch_nanos: intermediate_ns,
90-
}
91-
.add_to_instant(duration.time())
88+
Instant::from(intermediate_ns).add_to_instant(duration.time())
9289
}
9390

9491
#[inline]
@@ -109,11 +106,13 @@ impl ZonedDateTime {
109106
// 6. Let timeZone be zonedDateTime.[[TimeZone]].
110107
// 7. Let internalDuration be ToInternalDurationRecord(duration).
111108
// 8. Let epochNanoseconds be ? AddZonedDateTime(zonedDateTime.[[EpochNanoseconds]], timeZone, calendar, internalDuration, overflow).
112-
let epoch_ns = self
113-
.add_as_instant(duration, overflow, provider)?
114-
.epoch_nanos;
109+
let epoch_ns = self.add_as_instant(duration, overflow, provider)?;
115110
// 9. Return ! CreateTemporalZonedDateTime(epochNanoseconds, timeZone, calendar).
116-
Self::try_new(epoch_ns, self.calendar().clone(), self.tz().clone())
111+
Ok(Self::new_unchecked(
112+
epoch_ns,
113+
self.calendar().clone(),
114+
self.tz().clone(),
115+
))
117116
}
118117
}
119118

@@ -143,25 +142,25 @@ impl ZonedDateTime {
143142

144143
/// Returns the `epochSeconds` value of this `ZonedDateTime`.
145144
#[must_use]
146-
pub fn epoch_seconds(&self) -> f64 {
145+
pub fn epoch_seconds(&self) -> i128 {
147146
self.instant.epoch_seconds()
148147
}
149148

150149
/// Returns the `epochMilliseconds` value of this `ZonedDateTime`.
151150
#[must_use]
152-
pub fn epoch_milliseconds(&self) -> f64 {
151+
pub fn epoch_milliseconds(&self) -> i128 {
153152
self.instant.epoch_milliseconds()
154153
}
155154

156155
/// Returns the `epochMicroseconds` value of this `ZonedDateTime`.
157156
#[must_use]
158-
pub fn epoch_microseconds(&self) -> f64 {
157+
pub fn epoch_microseconds(&self) -> i128 {
159158
self.instant.epoch_microseconds()
160159
}
161160

162161
/// Returns the `epochNanoseconds` value of this `ZonedDateTime`.
163162
#[must_use]
164-
pub fn epoch_nanoseconds(&self) -> f64 {
163+
pub fn epoch_nanoseconds(&self) -> i128 {
165164
self.instant.epoch_nanoseconds()
166165
}
167166
}

0 commit comments

Comments
 (0)