From a2d6518afa1de89cfa3f4d8de47dbabd4d6e62ee Mon Sep 17 00:00:00 2001 From: Kevin Ness <46825870+nekevss@users.noreply.github.com> Date: Mon, 20 May 2024 18:12:24 -0400 Subject: [PATCH] Implement add & subtract methods for `DateTime` component (#45) * Add isodatetime methods * Build out add and subtract datetime functionality --- src/components/calendar.rs | 2 +- src/components/date.rs | 19 +-- src/components/datetime.rs | 226 +++++++++++++++++++++++--- src/components/duration.rs | 10 ++ src/components/duration/date.rs | 29 +--- src/components/duration/normalized.rs | 9 +- src/error.rs | 4 +- src/iso.rs | 71 +++++++- 8 files changed, 295 insertions(+), 75 deletions(-) diff --git a/src/components/calendar.rs b/src/components/calendar.rs index 2f4bd13c..57920f68 100644 --- a/src/components/calendar.rs +++ b/src/components/calendar.rs @@ -503,7 +503,7 @@ impl CalendarSlot { // 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( + let result = date.iso.add_date_duration( &DateDuration::new_unchecked( duration.years(), duration.months(), diff --git a/src/components/date.rs b/src/components/date.rs index 96441b4a..93dffd78 100644 --- a/src/components/date.rs +++ b/src/components/date.rs @@ -45,7 +45,7 @@ impl Date { duration: &Duration, context: &mut C::Context, ) -> TemporalResult<(Self, f64)> { - let new_date = self.add_date(duration, ArithmeticOverflow::Constrain, context)?; + let new_date = self.add_date(duration, None, context)?; let days = f64::from(self.days_until(&new_date)); Ok((new_date, days)) } @@ -56,10 +56,11 @@ impl Date { pub(crate) fn add_date( &self, duration: &Duration, - overflow: ArithmeticOverflow, + overflow: Option, context: &mut C::Context, ) -> TemporalResult { // 2. If options is not present, set options to undefined. + let overflow = overflow.unwrap_or(ArithmeticOverflow::Constrain); // 3. If duration.[[Years]] ≠ 0, or duration.[[Months]] ≠ 0, or duration.[[Weeks]] ≠ 0, then if duration.date().years != 0.0 || duration.date().months != 0.0 @@ -81,7 +82,7 @@ impl Date { // 7. Let result be ? AddISODate(plainDate.[[ISOYear]], plainDate.[[ISOMonth]], plainDate.[[ISODay]], 0, 0, 0, days, overflow). let result = self .iso - .add_iso_date(&DateDuration::new(0f64, 0f64, 0f64, days)?, overflow)?; + .add_date_duration(&DateDuration::new(0f64, 0f64, 0f64, days)?, overflow)?; Ok(Self::new_unchecked(result, self.calendar().clone())) } @@ -301,11 +302,7 @@ impl Date { overflow: Option, context: &mut C::Context, ) -> TemporalResult { - self.add_date( - duration, - overflow.unwrap_or(ArithmeticOverflow::Constrain), - context, - ) + self.add_date(duration, overflow, context) } pub fn contextual_subtract( @@ -314,11 +311,7 @@ impl Date { overflow: Option, context: &mut C::Context, ) -> TemporalResult { - self.add_date( - &duration.negated(), - overflow.unwrap_or(ArithmeticOverflow::Constrain), - context, - ) + self.add_date(&duration.negated(), overflow, context) } pub fn contextual_until( diff --git a/src/components/datetime.rs b/src/components/datetime.rs index 5936dea9..8acb70d4 100644 --- a/src/components/datetime.rs +++ b/src/components/datetime.rs @@ -14,7 +14,11 @@ use crate::{ use std::str::FromStr; use tinystr::TinyAsciiStr; -use super::calendar::{CalendarDateLike, GetCalendarSlot}; +use super::{ + calendar::{CalendarDateLike, GetCalendarSlot}, + duration::normalized::NormalizedTimeDuration, + Duration, +}; /// The native Rust implementation of `Temporal.PlainDateTime` #[non_exhaustive] @@ -51,6 +55,40 @@ impl DateTime { let iso = IsoDateTime::from_epoch_nanos(&instant.nanos, offset)?; Ok(Self { iso, calendar }) } + + // 5.5.14 AddDurationToOrSubtractDurationFromPlainDateTime ( operation, dateTime, temporalDurationLike, options ) + fn add_or_subtract_duration( + &self, + duration: &Duration, + overflow: Option, + context: &mut C::Context, + ) -> TemporalResult { + // SKIP: 1, 2, 3, 4 + // 1. If operation is subtract, let sign be -1. Otherwise, let sign be 1. + // 2. Let duration be ? ToTemporalDurationRecord(temporalDurationLike). + // 3. Set options to ? GetOptionsObject(options). + // 4. Let calendarRec be ? CreateCalendarMethodsRecord(dateTime.[[Calendar]], « date-add »). + + // 5. Let norm be NormalizeTimeDuration(sign × duration.[[Hours]], sign × duration.[[Minutes]], sign × duration.[[Seconds]], sign × duration.[[Milliseconds]], sign × duration.[[Microseconds]], sign × duration.[[Nanoseconds]]). + let norm = NormalizedTimeDuration::from_time_duration(duration.time()); + + // TODO: validate Constrain is default with all the recent changes. + // 6. Let result be ? AddDateTime(dateTime.[[ISOYear]], dateTime.[[ISOMonth]], dateTime.[[ISODay]], dateTime.[[ISOHour]], dateTime.[[ISOMinute]], dateTime.[[ISOSecond]], dateTime.[[ISOMillisecond]], dateTime.[[ISOMicrosecond]], dateTime.[[ISONanosecond]], calendarRec, sign × duration.[[Years]], sign × duration.[[Months]], sign × duration.[[Weeks]], sign × duration.[[Days]], norm, options). + let result = self.iso.add_date_duration( + self.calendar(), + duration.date(), + norm, + overflow, + context, + )?; + + // 7. Assert: IsValidISODate(result.[[Year]], result.[[Month]], result.[[Day]]) is true. + // 8. Assert: IsValidTime(result.[[Hour]], result.[[Minute]], result.[[Second]], result.[[Millisecond]], result.[[Microsecond]], result.[[Nanosecond]]) is true. + assert!(result.is_within_limits()); + + // 9. Return ? CreateTemporalDateTime(result.[[Year]], result.[[Month]], result.[[Day]], result.[[Hour]], result.[[Minute]], result.[[Second]], result.[[Millisecond]], result.[[Microsecond]], result.[[Nanosecond]], dateTime.[[Calendar]]). + Ok(Self::new_unchecked(result, self.calendar.clone())) + } } // ==== Public DateTime API ==== @@ -97,63 +135,63 @@ impl DateTime { #[inline] #[must_use] pub const fn iso_year(&self) -> i32 { - self.iso.date().year + self.iso.date.year } /// Returns this `Date`'s ISO month value. #[inline] #[must_use] pub const fn iso_month(&self) -> u8 { - self.iso.date().month + self.iso.date.month } /// Returns this `Date`'s ISO day value. #[inline] #[must_use] pub const fn iso_day(&self) -> u8 { - self.iso.date().day + self.iso.date.day } /// Returns the hour value #[inline] #[must_use] pub fn hour(&self) -> u8 { - self.iso.time().hour + self.iso.time.hour } /// Returns the minute value #[inline] #[must_use] pub fn minute(&self) -> u8 { - self.iso.time().minute + self.iso.time.minute } /// Returns the second value #[inline] #[must_use] pub fn second(&self) -> u8 { - self.iso.time().second + self.iso.time.second } /// Returns the `millisecond` value #[inline] #[must_use] pub fn millisecond(&self) -> u16 { - self.iso.time().millisecond + self.iso.time.millisecond } /// Returns the `microsecond` value #[inline] #[must_use] pub fn microsecond(&self) -> u16 { - self.iso.time().microsecond + self.iso.time.microsecond } /// Returns the `nanosecond` value #[inline] #[must_use] pub fn nanosecond(&self) -> u16 { - self.iso.time().nanosecond + self.iso.time.nanosecond } /// Returns the Calendar value. @@ -166,7 +204,6 @@ impl DateTime { // ==== Calendar-derived public API ==== -// TODO: Revert to `DateTime`. impl DateTime<()> { /// Returns the calendar year value. pub fn year(&self) -> TemporalResult { @@ -245,6 +282,24 @@ impl DateTime<()> { self.calendar .in_leap_year(&CalendarDateLike::DateTime(self.clone()), &mut ()) } + + #[inline] + pub fn add( + &self, + duration: &Duration, + overflow: Option, + ) -> TemporalResult { + self.contextual_add(duration, overflow, &mut ()) + } + + #[inline] + pub fn subtract( + &self, + duration: &Duration, + overflow: Option, + ) -> TemporalResult { + self.contextual_subtract(duration, overflow, &mut ()) + } } impl DateTime { @@ -355,6 +410,26 @@ impl DateTime { this.get_calendar() .in_leap_year(&CalendarDateLike::CustomDateTime(this.clone()), context) } + + #[inline] + pub fn contextual_add( + &self, + duration: &Duration, + overflow: Option, + context: &mut C::Context, + ) -> TemporalResult { + self.add_or_subtract_duration(duration, overflow, context) + } + + #[inline] + pub fn contextual_subtract( + &self, + duration: &Duration, + overflow: Option, + context: &mut C::Context, + ) -> TemporalResult { + self.add_or_subtract_duration(&duration.negated(), overflow, context) + } } // ==== Trait impls ==== @@ -367,7 +442,7 @@ impl GetCalendarSlot for DateTime { impl IsoDateSlots for DateTime { fn iso_date(&self) -> IsoDate { - *self.iso.date() + self.iso.date } } @@ -408,7 +483,10 @@ impl FromStr for DateTime { mod tests { use std::str::FromStr; - use crate::components::calendar::CalendarSlot; + use crate::{ + components::{calendar::CalendarSlot, Duration}, + iso::{IsoDate, IsoTime}, + }; use super::DateTime; @@ -429,20 +507,118 @@ mod tests { 0, CalendarSlot::from_str("iso8601").unwrap(), ); - let positive_limit = DateTime::<()>::new( - 275_760, - 9, - 14, - 0, - 0, - 0, - 0, - 0, - 0, - CalendarSlot::from_str("iso8601").unwrap(), - ); + let positive_limit = + DateTime::<()>::new(275_760, 9, 14, 0, 0, 0, 0, 0, 0, CalendarSlot::default()); assert!(negative_limit.is_err()); assert!(positive_limit.is_err()); } + + // options-undefined.js + #[test] + fn datetime_add_test() { + let pdt = DateTime::<()>::new( + 2020, + 1, + 31, + 12, + 34, + 56, + 987, + 654, + 321, + CalendarSlot::default(), + ) + .unwrap(); + + let result = pdt.add(&Duration::one_month(1.0), None).unwrap(); + + assert_eq!(result.month(), Ok(2)); + assert_eq!(result.day(), Ok(29)); + } + + // options-undefined.js + #[test] + fn datetime_subtract_test() { + let pdt = DateTime::<()>::new( + 2000, + 3, + 31, + 12, + 34, + 56, + 987, + 654, + 321, + CalendarSlot::default(), + ) + .unwrap(); + + let result = pdt.subtract(&Duration::one_month(1.0), None).unwrap(); + + assert_eq!(result.month(), Ok(2)); + assert_eq!(result.day(), Ok(29)); + } + + // subtract/hour-overflow.js + #[test] + fn datetime_subtract_hour_overflows() { + let dt = DateTime::<()>::new( + 2019, + 10, + 29, + 10, + 46, + 38, + 271, + 986, + 102, + CalendarSlot::default(), + ) + .unwrap(); + + let result = dt.subtract(&Duration::hour(12.0), None).unwrap(); + + assert_eq!( + result.iso.date, + IsoDate { + year: 2019, + month: 10, + day: 28 + } + ); + assert_eq!( + result.iso.time, + IsoTime { + hour: 22, + minute: 46, + second: 38, + millisecond: 271, + microsecond: 986, + nanosecond: 102 + } + ); + + let result = dt.add(&Duration::hour(-12.0), None).unwrap(); + + assert_eq!( + result.iso.date, + IsoDate { + year: 2019, + month: 10, + day: 28 + } + ); + assert_eq!( + result.iso.time, + IsoTime { + hour: 22, + minute: 46, + second: 38, + millisecond: 271, + microsecond: 986, + nanosecond: 102 + } + ); + } } diff --git a/src/components/duration.rs b/src/components/duration.rs index fd2aa2b4..b4c4013b 100644 --- a/src/components/duration.rs +++ b/src/components/duration.rs @@ -47,6 +47,16 @@ pub struct Duration { // - Methods (private/public/feature) // +#[cfg(test)] +impl Duration { + pub(crate) fn hour(value: f64) -> Self { + Self::new_unchecked( + DateDuration::default(), + TimeDuration::new_unchecked(value, 0.0, 0.0, 0.0, 0.0, 0.0), + ) + } +} + // ==== Private Creation methods ==== impl Duration { diff --git a/src/components/duration/date.rs b/src/components/duration/date.rs index ff34c3fa..c15ad272 100644 --- a/src/components/duration/date.rs +++ b/src/components/duration/date.rs @@ -500,11 +500,7 @@ impl DateDuration { // i. Let dateAdd be unused. // e. Let yearsLater be ? AddDate(calendar, plainRelativeTo, yearsDuration, undefined, dateAdd). - let years_later = plain_relative_to.add_date( - &years_duration, - ArithmeticOverflow::Constrain, - context, - )?; + let years_later = plain_relative_to.add_date(&years_duration, None, context)?; // f. Let yearsMonthsWeeks be ! CreateTemporalDuration(years, months, weeks, 0, 0, 0, 0, 0, 0, 0). let years_months_weeks = Duration::new_unchecked( @@ -513,11 +509,8 @@ impl DateDuration { ); // g. Let yearsMonthsWeeksLater be ? AddDate(calendar, plainRelativeTo, yearsMonthsWeeks, undefined, dateAdd). - let years_months_weeks_later = plain_relative_to.add_date( - &years_months_weeks, - ArithmeticOverflow::Constrain, - context, - )?; + let years_months_weeks_later = + plain_relative_to.add_date(&years_months_weeks, None, context)?; // h. Let monthsWeeksInDays be DaysUntil(yearsLater, yearsMonthsWeeksLater). let months_weeks_in_days = years_later.days_until(&years_months_weeks_later); @@ -529,7 +522,7 @@ impl DateDuration { fractional_days += f64::from(months_weeks_in_days); // k. Let isoResult be ! AddISODate(plainRelativeTo.[[ISOYear]]. plainRelativeTo.[[ISOMonth]], plainRelativeTo.[[ISODay]], 0, 0, 0, truncate(fractionalDays), "constrain"). - let iso_result = plain_relative_to.iso.add_iso_date( + let iso_result = plain_relative_to.iso.add_date_duration( &DateDuration::new_unchecked(0.0, 0.0, 0.0, fractional_days.trunc()), ArithmeticOverflow::Constrain, )?; @@ -609,11 +602,8 @@ impl DateDuration { // i. Let dateAdd be unused. // e. Let yearsMonthsLater be ? AddDate(calendar, plainRelativeTo, yearsMonths, undefined, dateAdd). - let years_months_later = plain_relative_to.add_date( - &years_months, - ArithmeticOverflow::Constrain, - context, - )?; + let years_months_later = + plain_relative_to.add_date(&years_months, None, context)?; // f. Let yearsMonthsWeeks be ! CreateTemporalDuration(years, months, weeks, 0, 0, 0, 0, 0, 0, 0). let years_months_weeks = Duration::from_date_duration( @@ -621,11 +611,8 @@ impl DateDuration { ); // g. Let yearsMonthsWeeksLater be ? AddDate(calendar, plainRelativeTo, yearsMonthsWeeks, undefined, dateAdd). - let years_months_weeks_later = plain_relative_to.add_date( - &years_months_weeks, - ArithmeticOverflow::Constrain, - context, - )?; + let years_months_weeks_later = + plain_relative_to.add_date(&years_months_weeks, None, context)?; // h. Let weeksInDays be DaysUntil(yearsMonthsLater, yearsMonthsWeeksLater). let weeks_in_days = years_months_later.days_until(&years_months_weeks_later); diff --git a/src/components/duration/normalized.rs b/src/components/duration/normalized.rs index 490a8f30..f073672c 100644 --- a/src/components/duration/normalized.rs +++ b/src/components/duration/normalized.rs @@ -8,13 +8,14 @@ use super::{DateDuration, TimeDuration}; const MAX_TIME_DURATION: f64 = 2e53 * 10e9 - 1.0; +// TODO: This should be moved to i128 /// A Normalized `TimeDuration` that represents the current `TimeDuration` in nanoseconds. #[derive(Debug, Clone, Copy, Default, PartialEq, PartialOrd)] pub struct NormalizedTimeDuration(pub(crate) f64); impl NormalizedTimeDuration { /// Equivalent: 7.5.20 NormalizeTimeDuration ( hours, minutes, seconds, milliseconds, microseconds, nanoseconds ) - pub(super) fn from_time_duration(time: &TimeDuration) -> Self { + 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; @@ -56,12 +57,12 @@ impl NormalizedTimeDuration { } /// Return the seconds value of the `NormalizedTimeDuration`. - pub(super) fn seconds(&self) -> i64 { - (self.0 / 10e9).trunc() as i64 + pub(crate) fn seconds(&self) -> i64 { + (self.0.div_euclid(1e9)).trunc() as i64 } /// Returns the subsecond components of the `NormalizedTimeDuration`. - pub(super) fn subseconds(&self) -> i32 { + pub(crate) fn subseconds(&self) -> i32 { // SAFETY: Remainder is 10e9 which is in range of i32 (self.0 % 10e9f64) as i32 } diff --git a/src/error.rs b/src/error.rs index 6aee99a8..3d9f7c4e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -5,7 +5,7 @@ use core::fmt; use icu_calendar::CalendarError; /// `TemporalError`'s error type. -#[derive(Debug, Default, Clone, Copy)] +#[derive(Debug, Default, Clone, Copy, PartialEq)] pub enum ErrorKind { /// Error. #[default] @@ -31,7 +31,7 @@ impl fmt::Display for ErrorKind { } /// The error type for `boa_temporal`. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct TemporalError { kind: ErrorKind, msg: Box, diff --git a/src/iso.rs b/src/iso.rs index f7a48c4c..3343938e 100644 --- a/src/iso.rs +++ b/src/iso.rs @@ -13,7 +13,11 @@ //! An `IsoDateTime` has the internal slots of both an `IsoDate` and `IsoTime`. use crate::{ - components::duration::{DateDuration, TimeDuration}, + components::{ + calendar::{CalendarProtocol, CalendarSlot}, + duration::{normalized::NormalizedTimeDuration, DateDuration, TimeDuration}, + Date, Duration, + }, error::TemporalError, options::{ArithmeticOverflow, TemporalRoundingMode, TemporalUnit}, utils, TemporalResult, NS_PER_DAY, @@ -26,8 +30,8 @@ use num_traits::{cast::FromPrimitive, ToPrimitive}; #[non_exhaustive] #[derive(Debug, Default, Clone, Copy)] pub struct IsoDateTime { - date: IsoDate, - time: IsoTime, + pub(crate) date: IsoDate, + pub(crate) time: IsoTime, } impl IsoDateTime { @@ -118,12 +122,45 @@ impl IsoDateTime { iso_dt_within_valid_limits(self.date, &self.time) } - pub(crate) const fn date(&self) -> &IsoDate { - &self.date - } + /// Specification equivalent to 5.5.9 `AddDateTime`. + pub(crate) fn add_date_duration( + &self, + calendar: &CalendarSlot, + date_duration: &DateDuration, + norm: NormalizedTimeDuration, + overflow: Option, + context: &mut C::Context, + ) -> TemporalResult { + // 1. Assert: IsValidISODate(year, month, day) is true. + // 2. Assert: ISODateTimeWithinLimits(year, month, day, hour, minute, second, millisecond, microsecond, nanosecond) is true. + // 3. Let timeResult be AddTime(hour, minute, second, millisecond, microsecond, nanosecond, norm). + let t_result = self.time.add(norm); + + // 4. Let datePart be ! CreateTemporalDate(year, month, day, calendarRec.[[Receiver]]). + let date = Date::new_unchecked(self.date, calendar.clone()); + + // 5. Let dateDuration be ? CreateTemporalDuration(years, months, weeks, days + timeResult.[[Days]], 0, 0, 0, 0, 0, 0). + let duration = Duration::new( + date_duration.years, + date_duration.months, + date_duration.weeks, + date_duration.days + f64::from(t_result.0), + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + )?; - pub(crate) const fn time(&self) -> &IsoTime { - &self.time + // 6. Let addedDate be ? AddDate(calendarRec, datePart, dateDuration, options). + let added_date = date.add_date(&duration, overflow, context)?; + + // 7. Return ISO Date-Time Record { [[Year]]: addedDate.[[ISOYear]], [[Month]]: addedDate.[[ISOMonth]], + // [[Day]]: addedDate.[[ISODay]], [[Hour]]: timeResult.[[Hour]], [[Minute]]: timeResult.[[Minute]], + // [[Second]]: timeResult.[[Second]], [[Millisecond]]: timeResult.[[Millisecond]], + // [[Microsecond]]: timeResult.[[Microsecond]], [[Nanosecond]]: timeResult.[[Nanosecond]] }. + Ok(Self::new_unchecked(added_date.iso, t_result.1)) } } @@ -204,7 +241,7 @@ impl IsoDate { } /// Returns the resulting `IsoDate` from adding a provided `Duration` to this `IsoDate` - pub(crate) fn add_iso_date( + pub(crate) fn add_date_duration( self, duration: &DateDuration, overflow: ArithmeticOverflow, @@ -662,6 +699,22 @@ impl IsoTime { && sub_second.contains(&self.nanosecond) } + pub(crate) fn add(&self, norm: NormalizedTimeDuration) -> (i32, Self) { + // 1. Set second to second + NormalizedTimeDurationSeconds(norm). + let seconds = f64::from(self.second) + norm.seconds() as f64; + // 2. Set nanosecond to nanosecond + NormalizedTimeDurationSubseconds(norm). + let nanos = i32::from(self.nanosecond) + norm.subseconds(); + // 3. Return BalanceTime(hour, minute, second, millisecond, microsecond, nanosecond). + Self::balance( + f64::from(self.hour), + f64::from(self.minute), + seconds, + f64::from(self.millisecond), + f64::from(self.microsecond), + f64::from(nanos), + ) + } + /// `IsoTimeToEpochMs` /// /// Note: This method is library specific and not in spec