diff --git a/src/components/calendar.rs b/src/components/calendar.rs index 9543cb80..04dfac61 100644 --- a/src/components/calendar.rs +++ b/src/components/calendar.rs @@ -11,7 +11,10 @@ use std::str::FromStr; use crate::{ - components::{Date, DateTime, Duration, MonthDay, YearMonth}, + components::{ + duration::{DateDuration, TimeDuration}, + Date, DateTime, Duration, MonthDay, YearMonth, + }, iso::{IsoDate, IsoDateSlots}, options::{ArithmeticOverflow, TemporalUnit}, TemporalError, TemporalFields, TemporalResult, @@ -489,6 +492,32 @@ impl CalendarSlot { context: &mut C::Context, ) -> TemporalResult> { match self { + CalendarSlot::Builtin(AnyCalendar::Iso(_)) => { + // 8. Let norm be NormalizeTimeDuration(duration.[[Hours]], duration.[[Minutes]], duration.[[Seconds]], duration.[[Milliseconds]], duration.[[Microseconds]], duration.[[Nanoseconds]]). + // 9. Let balanceResult be BalanceTimeDuration(norm, "day"). + let (balance_days, _) = TimeDuration::from_normalized( + duration.time().to_normalized(), + TemporalUnit::Day, + )?; + // 10. Let result be ? AddISODate(date.[[ISOYear]], date.[[ISOMonth]], date.[[ISODay]], duration.[[Years]], duration.[[Months]], duration.[[Weeks]], duration.[[Days]] + balanceResult.[[Days]], overflow). + let result = date.iso().add_iso_date( + &DateDuration::new_unchecked( + duration.days(), + duration.months(), + duration.weeks(), + duration.days() + balance_days, + ), + overflow, + )?; + // 11. Return ? CreateTemporalDate(result.[[Year]], result.[[Month]], result.[[Day]], "iso8601"). + Date::new( + result.year, + result.month.into(), + result.day.into(), + date.calendar().clone(), + ArithmeticOverflow::Reject, + ) + } CalendarSlot::Builtin(_) => { Err(TemporalError::range().with_message("Not yet implemented.")) } diff --git a/src/components/date.rs b/src/components/date.rs index d6c80eb2..25da35bc 100644 --- a/src/components/date.rs +++ b/src/components/date.rs @@ -358,15 +358,8 @@ impl Date { // 3. Let overflow be ? ToTemporalOverflow(options). // 4. Let days be ? BalanceTimeDuration(duration.[[Days]], duration.[[Hours]], duration.[[Minutes]], duration.[[Seconds]], duration.[[Milliseconds]], duration.[[Microseconds]], duration.[[Nanoseconds]], "day").[[Days]]. - let (days, _) = TimeDuration::new_unchecked( - duration.hours(), - duration.minutes(), - duration.seconds(), - duration.milliseconds(), - duration.microseconds(), - duration.nanoseconds(), - ) - .balance(duration.days(), TemporalUnit::Day)?; + let (days, _) = + TimeDuration::from_normalized(duration.time().to_normalized(), TemporalUnit::Day)?; // 5. Let result be ? AddISODate(plainDate.[[ISOYear]], plainDate.[[ISOMonth]], plainDate.[[ISODay]], 0, 0, 0, days, overflow). let result = self diff --git a/src/components/duration.rs b/src/components/duration.rs index 6ac921c2..2d9661d0 100644 --- a/src/components/duration.rs +++ b/src/components/duration.rs @@ -11,6 +11,7 @@ use std::str::FromStr; use super::{calendar::CalendarProtocol, tz::TzProtocol}; mod date; +pub(crate) mod normalized; mod time; #[doc(inline)] @@ -801,7 +802,8 @@ impl Duration { Ok(result) } - // TODO: Refactor relative_to's into a RelativeTo struct? + // TODO (nekevss): Refactor relative_to's into a RelativeTo struct? + // TODO (nekevss): Update to `Duration` normalization. /// Abstract Operation 7.5.26 `RoundDuration ( years, months, weeks, days, hours, minutes, /// seconds, milliseconds, microseconds, nanoseconds, increment, unit, /// roundingMode [ , plainRelativeTo [, zonedRelativeTo [, precalculatedDateTime]]] )` @@ -922,15 +924,7 @@ impl Duration { /// Calls `TimeDuration`'s balance method on the current `Duration`. #[inline] pub fn balance_time_duration(&self, unit: TemporalUnit) -> TemporalResult<(f64, TimeDuration)> { - TimeDuration::new_unchecked( - self.hours(), - self.minutes(), - self.seconds(), - self.milliseconds(), - self.microseconds(), - self.nanoseconds(), - ) - .balance(self.days(), unit) + TimeDuration::from_normalized(self.time().to_normalized(), unit) } } diff --git a/src/components/duration/normalized.rs b/src/components/duration/normalized.rs new file mode 100644 index 00000000..27c281cd --- /dev/null +++ b/src/components/duration/normalized.rs @@ -0,0 +1,59 @@ +//! This module implements the normalized `Duration` records. + +use crate::{TemporalError, TemporalResult, NS_PER_DAY}; + +use super::TimeDuration; + +const MAX_TIME_DURATION: f64 = 2e53 * 10e9 - 1.0; + +#[derive(Debug, Clone, Copy, Default, PartialEq, PartialOrd)] +pub(crate) struct NormalizedTimeDuration(pub(crate) f64); + +impl NormalizedTimeDuration { + /// Equivalent: 7.5.20 NormalizeTimeDuration ( hours, minutes, seconds, milliseconds, microseconds, nanoseconds ) + pub(crate) fn from_time_duration(time: &TimeDuration) -> Self { + let minutes = time.minutes + time.hours * 60.0; + let seconds = time.seconds + minutes * 60.0; + let milliseconds = time.milliseconds + seconds * 1000.0; + let microseconds = time.microseconds + milliseconds * 1000.0; + let nanoseconds = time.nanoseconds + microseconds * 1000.0; + // NOTE(nekevss): Is it worth returning a `RangeError` below. + debug_assert!(nanoseconds.abs() <= MAX_TIME_DURATION); + Self(nanoseconds) + } + + /// Equivalent: 7.5.22 AddNormalizedTimeDuration ( one, two ) + #[allow(unused)] + pub(crate) fn add(&self, other: &Self) -> TemporalResult { + let result = self.0 + other.0; + if result.abs() > MAX_TIME_DURATION { + return Err(TemporalError::range() + .with_message("normalizedTimeDuration exceeds maxTimeDuration.")); + } + Ok(Self(result)) + } + + /// Equivalent: 7.5.23 Add24HourDaysToNormalizedTimeDuration ( d, days ) + #[allow(unused)] + pub(crate) fn add_days(&self, days: f64) -> TemporalResult { + let result = self.0 + days * NS_PER_DAY as f64; + if result.abs() > MAX_TIME_DURATION { + return Err(TemporalError::range() + .with_message("normalizedTimeDuration exceeds maxTimeDuration.")); + } + Ok(Self(result)) + } + + // NOTE: DivideNormalizedTimeDuration probably requires `__float128` support as `NormalizedTimeDuration` is not `safe integer`. + // Tracking issue: https://github.com/rust-lang/rfcs/pull/3453 + + /// Equivalent: 7.5.31 NormalizedTimeDurationSign ( d ) + pub(crate) fn sign(&self) -> f64 { + if self.0 < 0.0 { + return -1.0; + } else if self.0 > 0.0 { + return 1.0; + } + 0.0 + } +} diff --git a/src/components/duration/time.rs b/src/components/duration/time.rs index 8bf26d68..746e4fe2 100644 --- a/src/components/duration/time.rs +++ b/src/components/duration/time.rs @@ -5,7 +5,7 @@ use crate::{ utils, TemporalError, TemporalResult, }; -use super::is_valid_duration; +use super::{is_valid_duration, normalized::NormalizedTimeDuration}; /// `TimeDuration` represents the [Time Duration record][spec] of the `Duration.` /// @@ -55,36 +55,98 @@ impl TimeDuration { .mul_add(1_000_f64, self.microseconds) .mul_add(1_000_f64, self.nanoseconds) } +} - /// Abstract Operation 7.5.18 `BalancePossiblyInfiniteDuration ( days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, largestUnit )` - /// - /// This function will balance the current `TimeDuration`. It returns the balanced `day` and `TimeDuration` value. - #[allow(clippy::too_many_arguments)] - pub(crate) fn balance_possibly_infinite_time_duration( - days: f64, +// ==== TimeDuration's public API ==== + +impl TimeDuration { + /// Creates a new validated `TimeDuration`. + pub fn new( hours: f64, minutes: f64, seconds: f64, milliseconds: f64, microseconds: f64, nanoseconds: f64, - largest_unit: TemporalUnit, - ) -> TemporalResult<(f64, Option)> { - // 1. Set hours to hours + days × 24. - let hours = hours + (days * 24f64); - - // 2. Set nanoseconds to TotalDurationNanoseconds(hours, minutes, seconds, milliseconds, microseconds, nanoseconds). - let mut nanoseconds = Self::new_unchecked( + ) -> TemporalResult { + let result = Self::new_unchecked( hours, minutes, seconds, milliseconds, microseconds, nanoseconds, - ) - .as_nanos(); + ); + if !is_valid_duration(&result.into_iter().collect()) { + return Err( + TemporalError::range().with_message("Attempted to create an invalid TimeDuration.") + ); + } + Ok(result) + } + + /// Creates a partial `TimeDuration` with all values set to `NaN`. + #[must_use] + pub const fn partial() -> Self { + Self { + hours: f64::NAN, + minutes: f64::NAN, + seconds: f64::NAN, + milliseconds: f64::NAN, + microseconds: f64::NAN, + nanoseconds: f64::NAN, + } + } + + /// Creates a `TimeDuration` from a provided partial `TimeDuration`. + #[must_use] + pub fn from_partial(partial: &TimeDuration) -> Self { + Self { + hours: if partial.hours.is_nan() { + 0.0 + } else { + partial.hours + }, + minutes: if partial.minutes.is_nan() { + 0.0 + } else { + partial.minutes + }, + seconds: if partial.seconds.is_nan() { + 0.0 + } else { + partial.seconds + }, + milliseconds: if partial.milliseconds.is_nan() { + 0.0 + } else { + partial.milliseconds + }, + microseconds: if partial.microseconds.is_nan() { + 0.0 + } else { + partial.microseconds + }, + nanoseconds: if partial.nanoseconds.is_nan() { + 0.0 + } else { + partial.nanoseconds + }, + } + } - // 3. Set days, hours, minutes, seconds, milliseconds, and microseconds to 0. + /// Balances and creates `TimeDuration` from a `NormalizedTimeDuration`. This method will return + /// a tuple (f64, TimeDuration) where f64 is the overflow day value from balancing. + /// + /// Equivalent: `BalanceTimeDuration` + /// + /// # Errors: + /// - Will error if provided duration is invalid + pub(crate) fn from_normalized( + norm: NormalizedTimeDuration, + largest_unit: TemporalUnit, + ) -> TemporalResult<(f64, Self)> { + // 1. Let days, hours, minutes, seconds, milliseconds, and microseconds be 0. let mut days = 0f64; let mut hours = 0f64; let mut minutes = 0f64; @@ -92,18 +154,14 @@ impl TimeDuration { let mut milliseconds = 0f64; let mut microseconds = 0f64; - // 4. If nanoseconds < 0, let sign be -1; else, let sign be 1. - let sign = if nanoseconds < 0f64 { -1 } else { 1 }; - // 5. Set nanoseconds to abs(nanoseconds). - nanoseconds = nanoseconds.abs(); + // 2. Let sign be NormalizedTimeDurationSign(norm). + let sign = norm.sign(); + // 3. Let nanoseconds be NormalizedTimeDurationAbs(norm).[[TotalNanoseconds]]. + let mut nanoseconds = norm.0.abs(); match largest_unit { - // 9. If largestUnit is "year", "month", "week", "day", or "hour", then - TemporalUnit::Year - | TemporalUnit::Month - | TemporalUnit::Week - | TemporalUnit::Day - | TemporalUnit::Hour => { + // 4. If largestUnit is "year", "month", "week", or "day", then + TemporalUnit::Year | TemporalUnit::Month | TemporalUnit::Week | TemporalUnit::Day => { // a. Set microseconds to floor(nanoseconds / 1000). microseconds = (nanoseconds / 1000f64).floor(); // b. Set nanoseconds to nanoseconds modulo 1000. @@ -134,7 +192,34 @@ impl TimeDuration { // l. Set hours to hours modulo 24. hours %= 24f64; } - // 10. Else if largestUnit is "minute", then + // 5. Else if largestUnit is "hour", then + TemporalUnit::Hour => { + // a. Set microseconds to floor(nanoseconds / 1000). + microseconds = (nanoseconds / 1000f64).floor(); + // b. Set nanoseconds to nanoseconds modulo 1000. + nanoseconds %= 1000f64; + + // c. Set milliseconds to floor(microseconds / 1000). + milliseconds = (microseconds / 1000f64).floor(); + // d. Set microseconds to microseconds modulo 1000. + microseconds %= 1000f64; + + // e. Set seconds to floor(milliseconds / 1000). + seconds = (milliseconds / 1000f64).floor(); + // f. Set milliseconds to milliseconds modulo 1000. + milliseconds %= 1000f64; + + // g. Set minutes to floor(seconds / 60). + minutes = (seconds / 60f64).floor(); + // h. Set seconds to seconds modulo 60. + seconds %= 60f64; + + // i. Set hours to floor(minutes / 60). + hours = (minutes / 60f64).floor(); + // j. Set minutes to minutes modulo 60. + minutes %= 60f64; + } + // 6. Else if largestUnit is "minute", then TemporalUnit::Minute => { // a. Set microseconds to floor(nanoseconds / 1000). // b. Set nanoseconds to nanoseconds modulo 1000. @@ -156,7 +241,7 @@ impl TimeDuration { minutes = (seconds / 60f64).floor(); seconds %= 60f64; } - // 11. Else if largestUnit is "second", then + // 7. Else if largestUnit is "second", then TemporalUnit::Second => { // a. Set microseconds to floor(nanoseconds / 1000). // b. Set nanoseconds to nanoseconds modulo 1000. @@ -173,7 +258,7 @@ impl TimeDuration { seconds = (milliseconds / 1000f64).floor(); milliseconds %= 1000f64; } - // 12. Else if largestUnit is "millisecond", then + // 8. Else if largestUnit is "millisecond", then TemporalUnit::Millisecond => { // a. Set microseconds to floor(nanoseconds / 1000). // b. Set nanoseconds to nanoseconds modulo 1000. @@ -185,134 +270,50 @@ impl TimeDuration { milliseconds = (microseconds / 1000f64).floor(); microseconds %= 1000f64; } - // 13. Else if largestUnit is "microsecond", then + // 9. Else if largestUnit is "microsecond", then TemporalUnit::Microsecond => { // a. Set microseconds to floor(nanoseconds / 1000). // b. Set nanoseconds to nanoseconds modulo 1000. microseconds = (nanoseconds / 1000f64).floor(); nanoseconds %= 1000f64; } - // 14. Else, + // 10. Else, // a. Assert: largestUnit is "nanosecond". _ => debug_assert!(largest_unit == TemporalUnit::Nanosecond), } - let result_values = Vec::from(&[ - days, - hours, - minutes, - seconds, - milliseconds, - microseconds, - nanoseconds, - ]); - // 15. For each value v of « days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds », do - for value in result_values { - // a. If 𝔽(v) is not finite, then - if !value.is_finite() { - // i. If sign = 1, then - if sign == 1 { - // 1. Return positive overflow. - return Ok((f64::INFINITY, None)); - } - // ii. Else if sign = -1, then - // 1. Return negative overflow. - return Ok((f64::NEG_INFINITY, None)); - } - } - - let sign = f64::from(sign); + // NOTE(nekevss): `mul_add` is essentially the Rust's implementation of `std::fma()`, so that's handy, but + // this should be tested much further. + // 11. NOTE: When largestUnit is "millisecond", "microsecond", or "nanosecond", milliseconds, microseconds, or + // nanoseconds may be an unsafe integer. In this case, care must be taken when implementing the calculation + // using floating point arithmetic. It can be implemented in C++ using std::fma(). String manipulation will also + // give an exact result, since the multiplication is by a power of 10. - // 16. Return ? CreateTimeDurationRecord(days, hours × sign, minutes × sign, seconds × sign, milliseconds × sign, microseconds × sign, nanoseconds × sign). - let result = Self::new( - hours * sign, - minutes * sign, - seconds * sign, - milliseconds * sign, - microseconds * sign, - nanoseconds * sign, - )?; - - Ok((days, Some(result))) - } -} - -// ==== TimeDuration's public API ==== - -impl TimeDuration { - /// Creates a new validated `TimeDuration`. - pub fn new( - hours: f64, - minutes: f64, - seconds: f64, - milliseconds: f64, - microseconds: f64, - nanoseconds: f64, - ) -> TemporalResult { + // 12. Return ! CreateTimeDurationRecord(days × sign, hours × sign, minutes × sign, seconds × sign, milliseconds × sign, microseconds × sign, nanoseconds × sign). + let days = days.mul_add(sign, 0.0); let result = Self::new_unchecked( - hours, - minutes, - seconds, - milliseconds, - microseconds, - nanoseconds, + hours.mul_add(sign, 0.0), + minutes.mul_add(sign, 0.0), + seconds.mul_add(sign, 0.0), + milliseconds.mul_add(sign, 0.0), + microseconds.mul_add(sign, 0.0), + nanoseconds.mul_add(sign, 0.0), ); - if !is_valid_duration(&result.into_iter().collect()) { - return Err( - TemporalError::range().with_message("Attempted to create an invalid TimeDuration.") - ); - } - Ok(result) - } - /// Creates a partial `TimeDuration` with all values set to `NaN`. - #[must_use] - pub const fn partial() -> Self { - Self { - hours: f64::NAN, - minutes: f64::NAN, - seconds: f64::NAN, - milliseconds: f64::NAN, - microseconds: f64::NAN, - nanoseconds: f64::NAN, + let td = Vec::from(&[ + days, + result.hours, + result.minutes, + result.seconds, + result.milliseconds, + result.microseconds, + result.nanoseconds, + ]); + if !is_valid_duration(&td) { + return Err(TemporalError::range().with_message("Invalid balance TimeDuration.")); } - } - /// Creates a `TimeDuration` from a provided partial `TimeDuration`. - #[must_use] - pub fn from_partial(partial: &TimeDuration) -> Self { - Self { - hours: if partial.hours.is_nan() { - 0.0 - } else { - partial.hours - }, - minutes: if partial.minutes.is_nan() { - 0.0 - } else { - partial.minutes - }, - seconds: if partial.seconds.is_nan() { - 0.0 - } else { - partial.seconds - }, - milliseconds: if partial.milliseconds.is_nan() { - 0.0 - } else { - partial.milliseconds - }, - microseconds: if partial.microseconds.is_nan() { - 0.0 - } else { - partial.microseconds - }, - nanoseconds: if partial.nanoseconds.is_nan() { - 0.0 - } else { - partial.nanoseconds - }, - } + Ok((days, result)) } /// Returns a new `TimeDuration` representing the absolute value of the current. @@ -343,28 +344,6 @@ impl TimeDuration { } } - /// Balances a `TimeDuration` given a day value and the largest unit. `balance` will return - /// the balanced `day` and `TimeDuration`. - /// - /// # Errors: - /// - Will error if provided duration is invalid - pub fn balance(&self, days: f64, largest_unit: TemporalUnit) -> TemporalResult<(f64, Self)> { - let result = Self::balance_possibly_infinite_time_duration( - days, - self.hours, - self.minutes, - self.seconds, - self.milliseconds, - self.microseconds, - self.nanoseconds, - largest_unit, - )?; - let Some(time_duration) = result.1 else { - return Err(TemporalError::range().with_message("Invalid balance TimeDuration.")); - }; - Ok((result.0, time_duration)) - } - /// Utility function for returning if values in a valid range. #[inline] #[must_use] @@ -418,11 +397,17 @@ impl TimeDuration { pub fn iter(&self) -> TimeIter<'_> { <&Self as IntoIterator>::into_iter(self) } + + /// Returns this `TimeDuration` as a `NormalizedTimeDuration`. + pub(crate) fn to_normalized(self) -> NormalizedTimeDuration { + NormalizedTimeDuration::from_time_duration(&self) + } } // ==== TimeDuration method impls ==== impl TimeDuration { + // TODO: Update round to accomodate `Normalization`. /// Rounds the current `TimeDuration` given a rounding increment, unit and rounding mode. `round` will return a tuple of the rounded `TimeDuration` and /// the `total` value of the smallest unit prior to rounding. #[inline] diff --git a/src/components/instant.rs b/src/components/instant.rs index c0f92110..1d2ec849 100644 --- a/src/components/instant.rs +++ b/src/components/instant.rs @@ -77,8 +77,11 @@ impl Instant { // Steps 11-13 of 13.47 GetDifferenceSettings if smallest_unit == TemporalUnit::Nanosecond { - let (_, result) = TimeDuration::new_unchecked(0f64, 0f64, secs, millis, micros, nanos) - .balance(0f64, largest_unit)?; + let (_, result) = TimeDuration::from_normalized( + TimeDuration::new_unchecked(0f64, 0f64, secs, millis, micros, nanos) + .to_normalized(), + largest_unit, + )?; return Ok(result); } @@ -87,7 +90,8 @@ impl Instant { smallest_unit, rounding_mode, )?; - let (_, result) = round_result.balance(0f64, largest_unit)?; + let (_, result) = + TimeDuration::from_normalized(round_result.to_normalized(), largest_unit)?; Ok(result) }