Skip to content

Commit

Permalink
Implement ZonedDateTime::since and ZonedDateTime::until (#170)
Browse files Browse the repository at this point in the history
This PR implements the `since` and `until` methods for `ZonedDateTime`.

It also has a host of various bug fixes that I came across from
implementing the methods in Boa and running the test suite. As a result,
each method has a ~80% conformance rating (since 72/90 and until 70/88).
  • Loading branch information
nekevss authored Jan 19, 2025
1 parent c614682 commit cb10eec
Show file tree
Hide file tree
Showing 11 changed files with 216 additions and 80 deletions.
8 changes: 7 additions & 1 deletion src/components/calendar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use crate::{
},
iso::IsoDate,
options::{ArithmeticOverflow, TemporalUnit},
parsers::parse_allowed_calendar_formats,
TemporalError, TemporalResult,
};

Expand Down Expand Up @@ -200,8 +201,13 @@ impl Calendar {
impl FromStr for Calendar {
type Err = TemporalError;

// 13.39 ParseTemporalCalendarString ( string )
// 13.34 ParseTemporalCalendarString ( string )
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Some(s) = parse_allowed_calendar_formats(s) {
return s
.map(Calendar::from_utf8)
.unwrap_or(Ok(Calendar::default()));
}
Calendar::from_utf8(s.as_bytes())
}
}
Expand Down
13 changes: 7 additions & 6 deletions src/components/date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ use crate::{
iso::{IsoDate, IsoDateTime, IsoTime},
options::{
ArithmeticOverflow, DifferenceOperation, DifferenceSettings, DisplayCalendar,
ResolvedRoundingOptions, TemporalUnit,
ResolvedRoundingOptions, TemporalUnit, UnitGroup,
},
parsers::{parse_date_time, IxdtfStringBuilder},
primitive::FiniteF64,
Sign, TemporalError, TemporalResult, TemporalUnwrap, TimeZone,
TemporalError, TemporalResult, TemporalUnwrap, TimeZone,
};
use alloc::{format, string::String};
use core::str::FromStr;
Expand Down Expand Up @@ -260,9 +260,10 @@ impl PlainDate {

// 4. Let resolvedOptions be ? SnapshotOwnProperties(? GetOptionsObject(options), null).
// 5. Let settings be ? GetDifferenceSettings(operation, resolvedOptions, DATE, « », "day", "day").
let (sign, resolved) = ResolvedRoundingOptions::from_diff_settings(
let resolved = ResolvedRoundingOptions::from_diff_settings(
settings,
op,
UnitGroup::Date,
TemporalUnit::Day,
TemporalUnit::Day,
)?;
Expand Down Expand Up @@ -303,9 +304,9 @@ impl PlainDate {
}
let result = Duration::from_normalized(duration, TemporalUnit::Day)?;
// 13. Return ! CreateTemporalDuration(sign × duration.[[Years]], sign × duration.[[Months]], sign × duration.[[Weeks]], sign × duration.[[Days]], 0, 0, 0, 0, 0, 0).
match sign {
Sign::Positive | Sign::Zero => Ok(result),
Sign::Negative => Ok(result.negated()),
match op {
DifferenceOperation::Until => Ok(result),
DifferenceOperation::Since => Ok(result.negated()),
}
}
}
Expand Down
13 changes: 7 additions & 6 deletions src/components/datetime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ use crate::{
iso::{IsoDate, IsoDateTime, IsoTime},
options::{
ArithmeticOverflow, DifferenceOperation, DifferenceSettings, DisplayCalendar,
ResolvedRoundingOptions, RoundingOptions, TemporalUnit, ToStringRoundingOptions,
ResolvedRoundingOptions, RoundingOptions, TemporalUnit, ToStringRoundingOptions, UnitGroup,
},
parsers::{parse_date_time, IxdtfStringBuilder},
temporal_assert, Sign, TemporalError, TemporalResult, TemporalUnwrap, TimeZone,
temporal_assert, TemporalError, TemporalResult, TemporalUnwrap, TimeZone,
};
use alloc::string::String;
use core::{cmp::Ordering, str::FromStr};
Expand Down Expand Up @@ -131,9 +131,10 @@ impl PlainDateTime {
}

// 5. Let settings be ? GetDifferenceSettings(operation, resolvedOptions, datetime, « », "nanosecond", "day").
let (sign, options) = ResolvedRoundingOptions::from_diff_settings(
let options = ResolvedRoundingOptions::from_diff_settings(
settings,
op,
UnitGroup::DateTime,
TemporalUnit::Day,
TemporalUnit::Nanosecond,
)?;
Expand All @@ -149,9 +150,9 @@ impl PlainDateTime {
let result = Duration::from_normalized(norm_record, options.largest_unit)?;

// Step 12
match sign {
Sign::Positive | Sign::Zero => Ok(result),
Sign::Negative => Ok(result.negated()),
match op {
DifferenceOperation::Until => Ok(result),
DifferenceOperation::Since => Ok(result.negated()),
}
}

Expand Down
17 changes: 9 additions & 8 deletions src/components/duration/normalized.rs
Original file line number Diff line number Diff line change
Expand Up @@ -256,8 +256,9 @@ impl NormalizedDurationRecord {
/// Equivalent: `CreateNormalizedDurationRecord` & `CombineDateAndNormalizedTimeDuration`.
pub(crate) fn new(date: DateDuration, norm: NormalizedTimeDuration) -> TemporalResult<Self> {
if date.sign() != Sign::Zero && norm.sign() != Sign::Zero && date.sign() != norm.sign() {
return Err(TemporalError::range()
.with_message("DateDuration and NormalizedTimeDuration must agree."));
return Err(TemporalError::range().with_message(
"DateDuration and NormalizedTimeDuration must agree if both are not zero.",
));
}
Ok(Self { date, norm })
}
Expand Down Expand Up @@ -778,18 +779,18 @@ impl NormalizedDurationRecord {
// 4. Let largestUnitIndex be the ordinal index of the row of Table 22 whose "Singular" column contains largestUnit.
// 5. Let smallestUnitIndex be the ordinal index of the row of Table 22 whose "Singular" column contains smallestUnit.
// 6. Let unitIndex be smallestUnitIndex - 1.
let mut unit = smallest_unit + 1;
let mut smallest_unit = smallest_unit + 1;
// 7. Let done be false.
// 8. Repeat, while unitIndex ≤ largestUnitIndex and done is false,
while unit != TemporalUnit::Auto && unit <= largest_unit {
while smallest_unit != TemporalUnit::Auto && largest_unit < smallest_unit {
// a. Let unit be the value in the "Singular" column of Table 22 in the row whose ordinal index is unitIndex.
// b. If unit is not "week", or largestUnit is "week", then
if unit == TemporalUnit::Week || largest_unit != TemporalUnit::Week {
unit = unit + 1;
if smallest_unit == TemporalUnit::Week || largest_unit != TemporalUnit::Week {
smallest_unit = smallest_unit + 1;
continue;
}

let end_duration = match unit {
let end_duration = match smallest_unit {
// i. If unit is "year", then
TemporalUnit::Year => {
// 1. Let years be duration.[[Years]] + sign.
Expand Down Expand Up @@ -887,7 +888,7 @@ impl NormalizedDurationRecord {
break;
}
// c. Set unitIndex to unitIndex - 1.
unit = unit + 1;
smallest_unit = smallest_unit + 1;
}

Ok(duration)
Expand Down
48 changes: 24 additions & 24 deletions src/components/instant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ use crate::{
iso::{IsoDate, IsoDateTime, IsoTime},
options::{
ArithmeticOverflow, DifferenceOperation, DifferenceSettings, DisplayOffset,
ResolvedRoundingOptions, RoundingOptions, TemporalUnit, ToStringRoundingOptions,
ResolvedRoundingOptions, RoundingOptions, TemporalUnit, ToStringRoundingOptions, UnitGroup,
},
parsers::{parse_instant, IxdtfStringBuilder},
primitive::FiniteF64,
rounding::{IncrementRounder, Round},
Sign, TemporalError, TemporalResult, TemporalUnwrap, TimeZone, NS_MAX_INSTANT,
TemporalError, TemporalResult, TemporalUnwrap, TimeZone, NS_MAX_INSTANT,
};

use ixdtf::parsers::records::UtcOffsetRecordOrZ;
Expand All @@ -24,11 +24,12 @@ use num_traits::FromPrimitive;
use super::{
duration::normalized::{NormalizedDurationRecord, NormalizedTimeDuration},
timezone::TimeZoneProvider,
DateDuration,
};

const NANOSECONDS_PER_SECOND: f64 = 1e9;
const NANOSECONDS_PER_MINUTE: f64 = 60f64 * NANOSECONDS_PER_SECOND;
const NANOSECONDS_PER_HOUR: f64 = 60f64 * NANOSECONDS_PER_MINUTE;
const NANOSECONDS_PER_SECOND: i128 = 1_000_000_000;
const NANOSECONDS_PER_MINUTE: i128 = 60 * NANOSECONDS_PER_SECOND;
const NANOSECONDS_PER_HOUR: i128 = 60 * NANOSECONDS_PER_MINUTE;

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct EpochNanoseconds(pub(crate) i128);
Expand Down Expand Up @@ -83,19 +84,14 @@ impl Instant {
// TODO: Update to `i128`?
/// Adds a `TimeDuration` to the current `Instant`.
///
/// Temporal-Proposal equivalent: `AddDurationToOrSubtractDurationFrom`.
/// Temporal-Proposal equivalent: `AddInstant`.
pub(crate) fn add_to_instant(&self, duration: &TimeDuration) -> TemporalResult<Self> {
let current_nanos = self.epoch_nanoseconds() as f64;
let result = current_nanos
+ duration.nanoseconds.0
+ (duration.microseconds.0 * 1000f64)
+ (duration.milliseconds.0 * 1_000_000f64)
+ (duration.seconds.0 * NANOSECONDS_PER_SECOND)
+ (duration.minutes.0 * NANOSECONDS_PER_MINUTE)
+ (duration.hours.0 * NANOSECONDS_PER_HOUR);
let norm = NormalizedTimeDuration::from_time_duration(duration);
let result = self.epoch_nanoseconds() + norm.0;
Ok(Self::from(EpochNanoseconds::try_from(result)?))
}

/// `temporal_rs` equivalent of `DifferenceInstant`
pub(crate) fn diff_instant_internal(
&self,
other: &Self,
Expand All @@ -104,7 +100,10 @@ impl Instant {
let diff =
NormalizedTimeDuration::from_nanosecond_difference(other.as_i128(), self.as_i128())?;
let (round_record, _) = diff.round(FiniteF64::default(), resolved_options)?;
Ok(round_record)
NormalizedDurationRecord::new(
DateDuration::default(),
round_record.normalized_time_duration(),
)
}

// TODO: Add test for `diff_instant`.
Expand All @@ -121,9 +120,10 @@ impl Instant {
// 2. Set other to ? ToTemporalInstant(other).
// 3. Let resolvedOptions be ? SnapshotOwnProperties(? GetOptionsObject(options), null).
// 4. Let settings be ? GetDifferenceSettings(operation, resolvedOptions, time, « », "nanosecond", "second").
let (sign, resolved_options) = ResolvedRoundingOptions::from_diff_settings(
let resolved_options = ResolvedRoundingOptions::from_diff_settings(
options,
op,
UnitGroup::Time,
TemporalUnit::Second,
TemporalUnit::Nanosecond,
)?;
Expand All @@ -138,9 +138,9 @@ impl Instant {
// 6. Let norm be diffRecord.[[NormalizedTimeDuration]].
// 7. Let result be ! BalanceTimeDuration(norm, settings.[[LargestUnit]]).
// 8. Return ! CreateTemporalDuration(0, 0, 0, 0, sign × result.[[Hours]], sign × result.[[Minutes]], sign × result.[[Seconds]], sign × result.[[Milliseconds]], sign × result.[[Microseconds]], sign × result.[[Nanoseconds]]).
match sign {
Sign::Positive | Sign::Zero => Ok(result),
Sign::Negative => Ok(result.negated()),
match op {
DifferenceOperation::Until => Ok(result),
DifferenceOperation::Since => Ok(result.negated()),
}
}

Expand Down Expand Up @@ -338,12 +338,12 @@ impl FromStr for Instant {
// Find the offset
let offset = match ixdtf_record.offset {
UtcOffsetRecordOrZ::Offset(offset) => {
f64::from(offset.hour) * NANOSECONDS_PER_HOUR
+ f64::from(offset.minute) * NANOSECONDS_PER_MINUTE
+ f64::from(offset.second) * NANOSECONDS_PER_SECOND
+ f64::from(offset.nanosecond)
offset.hour as i128 * NANOSECONDS_PER_HOUR
+ i128::from(offset.minute) * NANOSECONDS_PER_MINUTE
+ i128::from(offset.second) * NANOSECONDS_PER_SECOND
+ i128::from(offset.nanosecond)
}
UtcOffsetRecordOrZ::Z => 0.0,
UtcOffsetRecordOrZ::Z => 0,
};
let nanoseconds = IsoDateTime::new_unchecked(iso_date, iso_time)
.as_nanoseconds()
Expand Down
13 changes: 7 additions & 6 deletions src/components/time.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ use crate::{
iso::IsoTime,
options::{
ArithmeticOverflow, DifferenceOperation, DifferenceSettings, ResolvedRoundingOptions,
RoundingIncrement, TemporalRoundingMode, TemporalUnit, ToStringRoundingOptions,
RoundingIncrement, TemporalRoundingMode, TemporalUnit, ToStringRoundingOptions, UnitGroup,
},
parsers::{parse_time, IxdtfStringBuilder},
primitive::FiniteF64,
Sign, TemporalError, TemporalResult,
TemporalError, TemporalResult,
};
use alloc::string::String;
use core::str::FromStr;
Expand Down Expand Up @@ -124,9 +124,10 @@ impl PlainTime {
// 2. Set other to ? ToTemporalTime(other).
// 3. Let resolvedOptions be ? SnapshotOwnProperties(? GetOptionsObject(options), null).
// 4. Let settings be ? GetDifferenceSettings(operation, resolvedOptions, TIME, « », "nanosecond", "hour").
let (sign, resolved) = ResolvedRoundingOptions::from_diff_settings(
let resolved = ResolvedRoundingOptions::from_diff_settings(
settings,
op,
UnitGroup::Time,
TemporalUnit::Hour,
TemporalUnit::Nanosecond,
)?;
Expand All @@ -151,9 +152,9 @@ impl PlainTime {
let result = TimeDuration::from_normalized(normalized_time, resolved.largest_unit)?.1;

// 8. Return ! CreateTemporalDuration(0, 0, 0, 0, sign × result.[[Hours]], sign × result.[[Minutes]], sign × result.[[Seconds]], sign × result.[[Milliseconds]], sign × result.[[Microseconds]], sign × result.[[Nanoseconds]]).
match sign {
Sign::Positive | Sign::Zero => Ok(Duration::from(result)),
Sign::Negative => Ok(Duration::from(result.negated())),
match op {
DifferenceOperation::Until => Ok(Duration::from(result)),
DifferenceOperation::Since => Ok(Duration::from(result.negated())),
}
}
}
Expand Down
Loading

0 comments on commit cb10eec

Please sign in to comment.