From 529834b4c2b7b45832761ed2aec90422aad793ce Mon Sep 17 00:00:00 2001 From: nekevss Date: Fri, 9 Feb 2024 21:31:46 -0500 Subject: [PATCH 1/3] Initial work on duration-normalization --- src/components/date.rs | 2 +- src/components/duration.rs | 14 +- src/components/duration/normalized.rs | 59 +++++ src/components/duration/time.rs | 348 ++++++++++++-------------- src/components/instant.rs | 4 +- 5 files changed, 220 insertions(+), 207 deletions(-) create mode 100644 src/components/duration/normalized.rs diff --git a/src/components/date.rs b/src/components/date.rs index d6c80eb26..e11fe12b7 100644 --- a/src/components/date.rs +++ b/src/components/date.rs @@ -366,7 +366,7 @@ impl Date { duration.microseconds(), duration.nanoseconds(), ) - .balance(duration.days(), TemporalUnit::Day)?; + .balance(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 6ac921c2d..2ad972b77 100644 --- a/src/components/duration.rs +++ b/src/components/duration.rs @@ -12,6 +12,7 @@ use super::{calendar::CalendarProtocol, tz::TzProtocol}; mod date; mod time; +mod normalized; #[doc(inline)] pub use date::DateDuration; @@ -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) + self.time().balance(unit) } } diff --git a/src/components/duration/normalized.rs b/src/components/duration/normalized.rs new file mode 100644 index 000000000..fa89125cd --- /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 8bf26d68c..fa88abda4 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,123 @@ 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 + }, + } + } + + /// Returns a new `TimeDuration` representing the absolute value of the current. + #[inline] + #[must_use] + pub fn abs(&self) -> Self { + Self { + hours: self.hours.abs(), + minutes: self.minutes.abs(), + seconds: self.seconds.abs(), + milliseconds: self.milliseconds.abs(), + microseconds: self.microseconds.abs(), + nanoseconds: self.nanoseconds.abs(), + } + } + + /// Returns a negated `TimeDuration`. + #[inline] + #[must_use] + pub fn negated(&self) -> Self { + Self { + hours: self.hours * -1f64, + minutes: self.minutes * -1f64, + seconds: self.seconds * -1f64, + milliseconds: self.milliseconds * -1f64, + microseconds: self.microseconds * -1f64, + nanoseconds: self.nanoseconds * -1f64, + } + } - // 3. Set days, hours, minutes, seconds, milliseconds, and microseconds to 0. + /// 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, largest_unit: TemporalUnit) -> TemporalResult<(f64, Self)> { + let norm = NormalizedTimeDuration::from_time_duration(&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 +179,17 @@ 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 + // 4. If largestUnit is "year", "month", "week", or "day", then TemporalUnit::Year | TemporalUnit::Month | TemporalUnit::Week - | TemporalUnit::Day - | TemporalUnit::Hour => { + | TemporalUnit::Day => { // a. Set microseconds to floor(nanoseconds / 1000). microseconds = (nanoseconds / 1000f64).floor(); // b. Set nanoseconds to nanoseconds modulo 1000. @@ -134,7 +220,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 +269,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 +286,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,184 +298,30 @@ 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); - - // 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 { - let result = Self::new_unchecked( - hours, - minutes, - seconds, - milliseconds, - microseconds, - nanoseconds, - ); - 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 - }, - } - } - - /// Returns a new `TimeDuration` representing the absolute value of the current. - #[inline] - #[must_use] - pub fn abs(&self) -> Self { - Self { - hours: self.hours.abs(), - minutes: self.minutes.abs(), - seconds: self.seconds.abs(), - milliseconds: self.milliseconds.abs(), - microseconds: self.microseconds.abs(), - nanoseconds: self.nanoseconds.abs(), - } - } - - /// Returns a negated `TimeDuration`. - #[inline] - #[must_use] - pub fn negated(&self) -> Self { - Self { - hours: self.hours * -1f64, - minutes: self.minutes * -1f64, - seconds: self.seconds * -1f64, - milliseconds: self.milliseconds * -1f64, - microseconds: self.microseconds * -1f64, - nanoseconds: self.nanoseconds * -1f64, + // 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. + // 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.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)); + + 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.")); } - } - /// 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)) + Ok((days, result)) } /// Utility function for returning if values in a valid range. @@ -423,6 +382,7 @@ impl TimeDuration { // ==== 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 c0f921109..fc1bc46d2 100644 --- a/src/components/instant.rs +++ b/src/components/instant.rs @@ -78,7 +78,7 @@ impl Instant { if smallest_unit == TemporalUnit::Nanosecond { let (_, result) = TimeDuration::new_unchecked(0f64, 0f64, secs, millis, micros, nanos) - .balance(0f64, largest_unit)?; + .balance(largest_unit)?; return Ok(result); } @@ -87,7 +87,7 @@ impl Instant { smallest_unit, rounding_mode, )?; - let (_, result) = round_result.balance(0f64, largest_unit)?; + let (_, result) = round_result.balance(largest_unit)?; Ok(result) } From bf3f4830ff23995eaf7889eef0f010dc593d00f7 Mon Sep 17 00:00:00 2001 From: nekevss Date: Fri, 9 Feb 2024 22:37:33 -0500 Subject: [PATCH 2/3] Implement CalendarDateAdd for ISO and API changes --- src/components/calendar.rs | 29 ++++++- src/components/date.rs | 11 +-- src/components/duration.rs | 4 +- src/components/duration/normalized.rs | 12 +-- src/components/duration/time.rs | 107 ++++++++++++++++---------- src/components/instant.rs | 8 +- 6 files changed, 109 insertions(+), 62 deletions(-) diff --git a/src/components/calendar.rs b/src/components/calendar.rs index 9543cb806..89f9bdf97 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,30 @@ 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().as_norm(), 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 e11fe12b7..abad227ff 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(TemporalUnit::Day)?; + let (days, _) = + TimeDuration::from_normalized(duration.time().as_norm(), 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 2ad972b77..3c3d7ed87 100644 --- a/src/components/duration.rs +++ b/src/components/duration.rs @@ -11,8 +11,8 @@ use std::str::FromStr; use super::{calendar::CalendarProtocol, tz::TzProtocol}; mod date; +pub(crate) mod normalized; mod time; -mod normalized; #[doc(inline)] pub use date::DateDuration; @@ -924,7 +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)> { - self.time().balance(unit) + TimeDuration::from_normalized(self.time().as_norm(), unit) } } diff --git a/src/components/duration/normalized.rs b/src/components/duration/normalized.rs index fa89125cd..27c281cd7 100644 --- a/src/components/duration/normalized.rs +++ b/src/components/duration/normalized.rs @@ -9,7 +9,6 @@ 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 { @@ -28,7 +27,8 @@ impl NormalizedTimeDuration { 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.")) + return Err(TemporalError::range() + .with_message("normalizedTimeDuration exceeds maxTimeDuration.")); } Ok(Self(result)) } @@ -38,7 +38,8 @@ impl NormalizedTimeDuration { 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.")) + return Err(TemporalError::range() + .with_message("normalizedTimeDuration exceeds maxTimeDuration.")); } Ok(Self(result)) } @@ -49,11 +50,10 @@ impl NormalizedTimeDuration { /// Equivalent: 7.5.31 NormalizedTimeDurationSign ( d ) pub(crate) fn sign(&self) -> f64 { if self.0 < 0.0 { - return -1.0 + return -1.0; } else if self.0 > 0.0 { - return 1.0 + return 1.0; } 0.0 } } - diff --git a/src/components/duration/time.rs b/src/components/duration/time.rs index fa88abda4..04b65f60f 100644 --- a/src/components/duration/time.rs +++ b/src/components/duration/time.rs @@ -135,42 +135,17 @@ impl TimeDuration { } } - /// Returns a new `TimeDuration` representing the absolute value of the current. - #[inline] - #[must_use] - pub fn abs(&self) -> Self { - Self { - hours: self.hours.abs(), - minutes: self.minutes.abs(), - seconds: self.seconds.abs(), - milliseconds: self.milliseconds.abs(), - microseconds: self.microseconds.abs(), - nanoseconds: self.nanoseconds.abs(), - } - } - - /// Returns a negated `TimeDuration`. - #[inline] - #[must_use] - pub fn negated(&self) -> Self { - Self { - hours: self.hours * -1f64, - minutes: self.minutes * -1f64, - seconds: self.seconds * -1f64, - milliseconds: self.milliseconds * -1f64, - microseconds: self.microseconds * -1f64, - nanoseconds: self.nanoseconds * -1f64, - } - } - - /// Balances a `TimeDuration` given a day value and the largest unit. `balance` will return - /// the balanced `day` and `TimeDuration`. + /// 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 fn balance(&self, largest_unit: TemporalUnit) -> TemporalResult<(f64, Self)> { - let norm = NormalizedTimeDuration::from_time_duration(&self); - + 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; @@ -186,10 +161,7 @@ impl TimeDuration { match largest_unit { // 4. If largestUnit is "year", "month", "week", or "day", then - TemporalUnit::Year - | TemporalUnit::Month - | TemporalUnit::Week - | TemporalUnit::Day => { + 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. @@ -309,14 +281,34 @@ impl TimeDuration { // a. Assert: largestUnit is "nanosecond". _ => debug_assert!(largest_unit == TemporalUnit::Nanosecond), } - // 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 + + // 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. + // 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.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)); + let result = Self::new_unchecked( + 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), + ); - let td = Vec::from(&[days, result.hours, result.minutes, result.seconds, result.milliseconds, result.microseconds, result.nanoseconds]); + 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.")); } @@ -324,6 +316,34 @@ impl TimeDuration { Ok((days, result)) } + /// Returns a new `TimeDuration` representing the absolute value of the current. + #[inline] + #[must_use] + pub fn abs(&self) -> Self { + Self { + hours: self.hours.abs(), + minutes: self.minutes.abs(), + seconds: self.seconds.abs(), + milliseconds: self.milliseconds.abs(), + microseconds: self.microseconds.abs(), + nanoseconds: self.nanoseconds.abs(), + } + } + + /// Returns a negated `TimeDuration`. + #[inline] + #[must_use] + pub fn negated(&self) -> Self { + Self { + hours: self.hours * -1f64, + minutes: self.minutes * -1f64, + seconds: self.seconds * -1f64, + milliseconds: self.milliseconds * -1f64, + microseconds: self.microseconds * -1f64, + nanoseconds: self.nanoseconds * -1f64, + } + } + /// Utility function for returning if values in a valid range. #[inline] #[must_use] @@ -377,6 +397,11 @@ impl TimeDuration { pub fn iter(&self) -> TimeIter<'_> { <&Self as IntoIterator>::into_iter(self) } + + /// Returns this `TimeDuration` as a `NormalizedTimeDuration`. + pub(crate) fn as_norm(&self) -> NormalizedTimeDuration { + NormalizedTimeDuration::from_time_duration(self) + } } // ==== TimeDuration method impls ==== diff --git a/src/components/instant.rs b/src/components/instant.rs index fc1bc46d2..2b72d594d 100644 --- a/src/components/instant.rs +++ b/src/components/instant.rs @@ -77,8 +77,10 @@ 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(largest_unit)?; + let (_, result) = TimeDuration::from_normalized( + TimeDuration::new_unchecked(0f64, 0f64, secs, millis, micros, nanos).as_norm(), + largest_unit, + )?; return Ok(result); } @@ -87,7 +89,7 @@ impl Instant { smallest_unit, rounding_mode, )?; - let (_, result) = round_result.balance(largest_unit)?; + let (_, result) = TimeDuration::from_normalized(round_result.as_norm(), largest_unit)?; Ok(result) } From daaae7d3a536b9d5b94efea4600088ee2dde7d85 Mon Sep 17 00:00:00 2001 From: nekevss Date: Wed, 14 Feb 2024 18:17:43 -0500 Subject: [PATCH 3/3] Review feedback --- src/components/calendar.rs | 6 ++++-- src/components/date.rs | 2 +- src/components/duration.rs | 2 +- src/components/duration/time.rs | 4 ++-- src/components/instant.rs | 6 ++++-- 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/components/calendar.rs b/src/components/calendar.rs index 89f9bdf97..04dfac610 100644 --- a/src/components/calendar.rs +++ b/src/components/calendar.rs @@ -495,8 +495,10 @@ impl CalendarSlot { 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().as_norm(), TemporalUnit::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( diff --git a/src/components/date.rs b/src/components/date.rs index abad227ff..25da35bce 100644 --- a/src/components/date.rs +++ b/src/components/date.rs @@ -359,7 +359,7 @@ 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::from_normalized(duration.time().as_norm(), TemporalUnit::Day)?; + 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 3c3d7ed87..2d9661d0a 100644 --- a/src/components/duration.rs +++ b/src/components/duration.rs @@ -924,7 +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::from_normalized(self.time().as_norm(), unit) + TimeDuration::from_normalized(self.time().to_normalized(), unit) } } diff --git a/src/components/duration/time.rs b/src/components/duration/time.rs index 04b65f60f..746e4fe2a 100644 --- a/src/components/duration/time.rs +++ b/src/components/duration/time.rs @@ -399,8 +399,8 @@ impl TimeDuration { } /// Returns this `TimeDuration` as a `NormalizedTimeDuration`. - pub(crate) fn as_norm(&self) -> NormalizedTimeDuration { - NormalizedTimeDuration::from_time_duration(self) + pub(crate) fn to_normalized(self) -> NormalizedTimeDuration { + NormalizedTimeDuration::from_time_duration(&self) } } diff --git a/src/components/instant.rs b/src/components/instant.rs index 2b72d594d..1d2ec849a 100644 --- a/src/components/instant.rs +++ b/src/components/instant.rs @@ -78,7 +78,8 @@ impl Instant { if smallest_unit == TemporalUnit::Nanosecond { let (_, result) = TimeDuration::from_normalized( - TimeDuration::new_unchecked(0f64, 0f64, secs, millis, micros, nanos).as_norm(), + TimeDuration::new_unchecked(0f64, 0f64, secs, millis, micros, nanos) + .to_normalized(), largest_unit, )?; return Ok(result); @@ -89,7 +90,8 @@ impl Instant { smallest_unit, rounding_mode, )?; - let (_, result) = TimeDuration::from_normalized(round_result.as_norm(), largest_unit)?; + let (_, result) = + TimeDuration::from_normalized(round_result.to_normalized(), largest_unit)?; Ok(result) }