Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 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
5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,13 @@ rustc-hash = { version = "2.0.0", features = ["std"] }
bitflags = "2.6.0"
num-traits = "0.2.19"
ixdtf = { version = "0.2.0", features = ["duration"]}
iana-time-zone = "0.1.61"

# log feature
log = { version = "0.4.0", optional = true }

# tzdb feature
tzif = { version = "0.2.3", optional = true }
iana-time-zone = "0.1.61"
jiff-tzdb = { version = "0.1.1", optional = true }
combine = { version = "4.6.7", optional = true }

Expand Down
22 changes: 11 additions & 11 deletions src/components/instant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ impl Instant {
let nanos = i128::from_f64(result).ok_or_else(|| {
TemporalError::range().with_message("Duration added to instant exceeded valid range.")
})?;
Self::new(nanos)
Self::try_new(nanos)
}

// TODO: Add test for `diff_instant`.
Expand Down Expand Up @@ -150,7 +150,7 @@ impl Instant {
impl Instant {
/// Create a new validated `Instant`.
#[inline]
pub fn new(epoch_nanoseconds: i128) -> TemporalResult<Self> {
pub fn try_new(epoch_nanoseconds: i128) -> TemporalResult<Self> {
if !is_valid_epoch_nanos(&epoch_nanoseconds) {
return Err(TemporalError::range()
.with_message("Instant nanoseconds are not within a valid epoch range."));
Expand Down Expand Up @@ -230,7 +230,7 @@ impl Instant {
let resolved_options = ResolvedRoundingOptions::from_instant_options(options)?;

let round_result = self.round_instant(resolved_options)?;
Self::new(round_result)
Self::try_new(round_result)
}

/// Returns the `epochSeconds` value for this `Instant`.
Expand Down Expand Up @@ -335,17 +335,17 @@ mod tests {
// valid, i.e., a valid instant is within the range of an f64.
let max = NS_MAX_INSTANT;
let min = NS_MIN_INSTANT;
let max_instant = Instant::new(max).unwrap();
let min_instant = Instant::new(min).unwrap();
let max_instant = Instant::try_new(max).unwrap();
let min_instant = Instant::try_new(min).unwrap();

assert_eq!(max_instant.epoch_nanoseconds(), max.to_f64().unwrap());
assert_eq!(min_instant.epoch_nanoseconds(), min.to_f64().unwrap());

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

assert!(Instant::new(max_plus_one).is_err());
assert!(Instant::new(min_minus_one).is_err());
assert!(Instant::try_new(max_plus_one).is_err());
assert!(Instant::try_new(min_minus_one).is_err());
}

#[test]
Expand Down Expand Up @@ -373,11 +373,11 @@ mod tests {
)
};

let earlier = Instant::new(
let earlier = Instant::try_new(
217_178_610_123_456_789, /* 1976-11-18T15:23:30.123456789Z */
)
.unwrap();
let later = Instant::new(
let later = Instant::try_new(
1_572_345_998_271_986_289, /* 2019-10-29T10:46:38.271986289Z */
)
.unwrap();
Expand Down Expand Up @@ -452,11 +452,11 @@ mod tests {
)
};

let earlier = Instant::new(
let earlier = Instant::try_new(
217_178_610_123_456_789, /* 1976-11-18T15:23:30.123456789Z */
)
.unwrap();
let later = Instant::new(
let later = Instant::try_new(
1_572_345_998_271_986_289, /* 2019-10-29T10:46:38.271986289Z */
)
.unwrap();
Expand Down
14 changes: 12 additions & 2 deletions src/components/now.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
//! The Temporal Now component

use crate::{sys, TemporalResult};
use alloc::string::String;

#[cfg(feature = "std")]
use num_traits::FromPrimitive;

use crate::{iso::IsoDateTime, sys, TemporalResult, TemporalUnwrap};
#[cfg(feature = "std")]
use crate::{iso::IsoDateTime, TemporalUnwrap};

#[cfg(feature = "std")]
use super::{
calendar::Calendar,
tz::{TimeZone, TzProvider},
Expand All @@ -19,7 +24,10 @@ impl Now {
pub fn time_zone_id() -> TemporalResult<String> {
sys::get_system_tz_identifier()
}
}

#[cfg(feature = "std")]
impl Now {
/// Returns the current instant
pub fn instant() -> TemporalResult<Instant> {
system_instant()
Expand All @@ -34,6 +42,7 @@ impl Now {
}
}

#[cfg(feature = "std")]
fn system_date_time(
tz: Option<TimeZone>,
provider: &mut impl TzProvider,
Expand All @@ -55,7 +64,8 @@ fn system_date_time(
)
}

#[cfg(feature = "std")]
fn system_instant() -> TemporalResult<Instant> {
let nanos = sys::get_system_nanoseconds()?;
Instant::new(i128::from_u128(nanos).temporal_unwrap()?)
Instant::try_new(i128::from_u128(nanos).temporal_unwrap()?)
}
216 changes: 211 additions & 5 deletions src/components/tz.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@

use alloc::borrow::ToOwned;
use alloc::string::String;
use alloc::vec::Vec;
use alloc::{vec, vec::Vec};
use core::{iter::Peekable, str::Chars};

use num_traits::ToPrimitive;

use crate::{components::Instant, iso::IsoDateTime, TemporalError, TemporalResult};
use crate::{
components::{duration::normalized::NormalizedTimeDuration, Instant},
iso::{IsoDate, IsoDateTime},
options::Disambiguation,
TemporalError, TemporalResult,
};

#[cfg(feature = "experimental")]
use crate::tzdb::FsTzdbProvider;
Expand All @@ -18,7 +23,7 @@ use std::sync::{LazyLock, Mutex};
pub static TZ_PROVIDER: LazyLock<Mutex<FsTzdbProvider>> =
LazyLock::new(|| Mutex::new(FsTzdbProvider::default()));

use super::ZonedDateTime;
use super::{instant::is_valid_epoch_nanos, ZonedDateTime};

pub trait TzProvider {
fn check_identifier(&self, identifier: &str) -> bool;
Expand Down Expand Up @@ -61,6 +66,12 @@ impl<'a> ParsedTimeZone<'a> {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TimeZone(pub String);

impl Default for TimeZone {
fn default() -> Self {
Self("UTC".into())
}
}

impl From<&ZonedDateTime> for TimeZone {
fn from(value: &ZonedDateTime) -> Self {
value.tz().clone()
Expand Down Expand Up @@ -109,9 +120,79 @@ impl TimeZone {
}
}

pub fn get_epoch_nanoseconds_for(
&self,
iso: IsoDateTime,
disambiguation: Disambiguation,
provider: &mut impl TzProvider,
Comment thread
nekevss marked this conversation as resolved.
Outdated
) -> TemporalResult<i128> {
// 1. Let possibleEpochNs be ? GetPossibleEpochNanoseconds(timeZone, isoDateTime).
let possible_nanos = self.get_possible_epoch_ns_for(iso, provider)?;
// 2. Return ? DisambiguatePossibleEpochNanoseconds(possibleEpochNs, timeZone, isoDateTime, disambiguation).
self.disambiguate_possible_epoch_nanos(possible_nanos, iso, disambiguation, provider)
}

/// Get the possible `Instant`s for this `TimeZoneSlot`.
pub fn get_possible_instant_for(&self) -> TemporalResult<Vec<Instant>> {
Err(TemporalError::general("Not yet implemented."))
pub fn get_possible_epoch_ns_for(
&self,
iso: IsoDateTime,
provider: &mut impl TzProvider,
) -> TemporalResult<Vec<i128>> {
// 1.Let parseResult be ! ParseTimeZoneIdentifier(timeZone).
let possible_nanoseconds = match ParsedTimeZone::from_str(&self.0, provider)? {
// 2. If parseResult.[[OffsetMinutes]] is not empty, then
ParsedTimeZone::Offset { minutes } => {
// a. Let balanced be
// BalanceISODateTime(isoDateTime.[[ISODate]].[[Year]],
// isoDateTime.[[ISODate]].[[Month]],
// isoDateTime.[[ISODate]].[[Day]],
// isoDateTime.[[Time]].[[Hour]],
// isoDateTime.[[Time]].[[Minute]] -
// parseResult.[[OffsetMinutes]],
// isoDateTime.[[Time]].[[Second]],
// isoDateTime.[[Time]].[[Millisecond]],
// isoDateTime.[[Time]].[[Microsecond]],
// isoDateTime.[[Time]].[[Nanosecond]]).
let balanced = IsoDateTime::balance(
iso.date.year,
iso.date.month.into(),
iso.date.day.into(),
iso.time.hour.into(),
(i16::from(iso.time.minute) - minutes).into(),
iso.time.second.into(),
iso.time.millisecond.into(),
iso.time.microsecond.into(),
iso.time.nanosecond.into(),
);
// b. Perform ? CheckISODaysRange(balanced.[[ISODate]]).
balanced.date.is_valid_day_range()?;
// c. Let epochNanoseconds be GetUTCEpochNanoseconds(balanced).
let epoch_ns = balanced
.as_nanoseconds()
.expect("conversion should be in a valid range. Option is result of BigInt");
// d. Let possibleEpochNanoseconds be « epochNanoseconds ».
vec![epoch_ns]
}
// 3. Else,
ParsedTimeZone::IanaIdentifier { identifier } => {
// a. Perform ? CheckISODaysRange(isoDateTime.[[ISODate]]).
iso.date.is_valid_day_range()?;
// b. Let possibleEpochNanoseconds be
// GetNamedTimeZoneEpochNanoseconds(parseResult.[[Name]],
// isoDateTime).
provider.get_named_tz_epoch_nanoseconds(identifier, iso)?
}
};
// 4. For each value epochNanoseconds in possibleEpochNanoseconds, do
for ns in &possible_nanoseconds {
// a . If IsValidEpochNanoseconds(epochNanoseconds) is false, throw a RangeError exception.
if !is_valid_epoch_nanos(ns) {
return Err(TemporalError::range()
.with_message("A possible nanosecond exceeded valid range."));
}
}
// 5. Return possibleEpochNanoseconds.
Ok(possible_nanoseconds)
}

/// Returns the current `TimeZoneSlot`'s identifier.
Expand All @@ -120,6 +201,131 @@ impl TimeZone {
}
}

impl TimeZone {
// TODO: This can be optimized by just not using a vec.
pub(crate) fn disambiguate_possible_epoch_nanos(
&self,
nanos: Vec<i128>,
iso: IsoDateTime,
disambiguation: Disambiguation,
provider: &mut impl TzProvider,
) -> TemporalResult<i128> {
// 1. Let n be possibleEpochNs's length.
let n = nanos.len();
// 2. If n = 1, then
if n == 1 {
// a. Return possibleEpochNs[0].
return Ok(nanos[0]);
// 3. If n ≠ 0, then
} else if n != 0 {
match disambiguation {
// a. If disambiguation is earlier or compatible, then
// i. Return possibleEpochNs[0].
Disambiguation::Compatible | Disambiguation::Earlier => return Ok(nanos[0]),
// b. If disambiguation is later, then
// i. Return possibleEpochNs[n - 1].
Disambiguation::Later => return Ok(nanos[n - 1]),
// c. Assert: disambiguation is reject.
// d. Throw a RangeError exception.
Disambiguation::Reject => {
return Err(
TemporalError::range().with_message("Rejecting ambiguous time zones.")
)
}
}
}
// 4. Assert: n = 0.
// 5. If disambiguation is reject, then
if disambiguation == Disambiguation::Reject {
// a. Throw a RangeError exception.
return Err(TemporalError::range().with_message("Rejecting ambiguous time zones."));
}

// NOTE: Below is rather greedy, but should in theory work.
//
// Primarily moving hour +/-3 to account Australia/Troll as
// the precision of before/after does not entirely matter as
// long is it is distinctly before / after any transition.

// 6. Let before be the latest possible ISO Date-Time Record for
// which CompareISODateTime(before, isoDateTime) = -1 and !
// GetPossibleEpochNanoseconds(timeZone, before) is not
// empty.
let mut before = iso;
before.time.hour -= 3;
// 7. Let after be the earliest possible ISO Date-Time Record
// for which CompareISODateTime(after, isoDateTime) = 1 and !
// GetPossibleEpochNanoseconds(timeZone, after) is not empty.
let mut after = iso;
after.time.hour += 3;

// 8. Let beforePossible be !
// GetPossibleEpochNanoseconds(timeZone, before).
// 9. Assert: beforePossible's length is 1.
let before_possible = self.get_possible_epoch_ns_for(before, provider)?;
debug_assert_eq!(before_possible.len(), 1);
// 10. Let afterPossible be !
// GetPossibleEpochNanoseconds(timeZone, after).
// 11. Assert: afterPossible's length is 1.
let after_possible = self.get_possible_epoch_ns_for(after, provider)?;
debug_assert_eq!(after_possible.len(), 1);
// 12. Let offsetBefore be GetOffsetNanosecondsFor(timeZone,
// beforePossible[0]).
let offset_before = self.get_offset_nanos_for(before_possible[0], provider)?;
// 13. Let offsetAfter be GetOffsetNanosecondsFor(timeZone,
// afterPossible[0]).
let offset_after = self.get_offset_nanos_for(after_possible[0], provider)?;
// 14. Let nanoseconds be offsetAfter - offsetBefore.
let nanoseconds = offset_after - offset_before;
// 15. Assert: abs(nanoseconds) ≤ nsPerDay.
// 16. If disambiguation is earlier, then
if disambiguation == Disambiguation::Earlier {
// a. Let timeDuration be TimeDurationFromComponents(0, 0, 0, 0, 0, -nanoseconds).
let time_duration = NormalizedTimeDuration(-nanoseconds);
// b. Let earlierTime be AddTime(isoDateTime.[[Time]], timeDuration).
let earlier_time = iso.time.add(time_duration);
// c. Let earlierDate be BalanceISODate(isoDateTime.[[ISODate]].[[Year]],
// isoDateTime.[[ISODate]].[[Month]],
// isoDateTime.[[ISODate]].[[Day]] + earlierTime.[[Days]]).
let earlier_date = IsoDate::balance(
iso.date.year,
iso.date.month.into(),
i32::from(iso.date.day) + earlier_time.0,
);

// d. Let earlierDateTime be
// CombineISODateAndTimeRecord(earlierDate, earlierTime).
let earlier = IsoDateTime::new_unchecked(earlier_date, earlier_time.1);
// e. Set possibleEpochNs to ? GetPossibleEpochNanoseconds(timeZone, earlierDateTime).
let possible = self.get_possible_epoch_ns_for(earlier, provider)?;
// f. Assert: possibleEpochNs is not empty.
// g. Return possibleEpochNs[0].
return Ok(possible[0]);
}
// 17. Assert: disambiguation is compatible or later.
// 18. Let timeDuration be TimeDurationFromComponents(0, 0, 0, 0, 0, nanoseconds).
let time_duration = NormalizedTimeDuration(nanoseconds);
// 19. Let laterTime be AddTime(isoDateTime.[[Time]], timeDuration).
let later_time = iso.time.add(time_duration);
// 20. Let laterDate be BalanceISODate(isoDateTime.[[ISODate]].[[Year]],
// isoDateTime.[[ISODate]].[[Month]], isoDateTime.[[ISODate]].[[Day]] + laterTime.[[Days]]).
let later_date = IsoDate::balance(
iso.date.year,
iso.date.month.into(),
i32::from(iso.date.day) + later_time.0,
);
// 21. Let laterDateTime be CombineISODateAndTimeRecord(laterDate, laterTime).
let later = IsoDateTime::new_unchecked(later_date, later_time.1);
// 22. Set possibleEpochNs to ? GetPossibleEpochNanoseconds(timeZone, laterDateTime).
let possible = self.get_possible_epoch_ns_for(later, provider)?;
// 23. Set n to possibleEpochNs's length.
let n = possible.len();
// 24. Assert: n ≠ 0.
// 25. Return possibleEpochNs[n - 1].
Ok(possible[n - 1])
}
}

#[inline]
fn parse_offset<'a>(chars: &mut Peekable<Chars<'_>>) -> TemporalResult<ParsedTimeZone<'a>> {
let sign = chars.next().map_or(1, |c| if c == '+' { 1 } else { -1 });
Expand Down
Loading