From 1e3f30118219e0c0032d9e8b044b559b7cd11f3d Mon Sep 17 00:00:00 2001 From: nekevss Date: Sat, 16 Mar 2024 10:13:39 -0400 Subject: [PATCH] Further build out date methods --- src/components/date.rs | 488 ++++++++++++++++++++++---- src/components/duration/date.rs | 10 +- src/components/duration/normalized.rs | 2 +- 3 files changed, 422 insertions(+), 78 deletions(-) diff --git a/src/components/date.rs b/src/components/date.rs index 40f63edd..96441b4a 100644 --- a/src/components/date.rs +++ b/src/components/date.rs @@ -9,9 +9,9 @@ use crate::{ DateTime, Duration, }, iso::{IsoDate, IsoDateSlots}, - options::{ArithmeticOverflow, TemporalUnit}, + options::{ArithmeticOverflow, RelativeTo, TemporalRoundingMode, TemporalUnit}, parser::parse_date_time, - TemporalError, TemporalResult, + utils, TemporalError, TemporalResult, }; use std::str::FromStr; @@ -45,11 +45,185 @@ impl Date { duration: &Duration, context: &mut C::Context, ) -> TemporalResult<(Self, f64)> { - let new_date = - self.contextual_add_date(duration, ArithmeticOverflow::Constrain, context)?; + let new_date = self.add_date(duration, ArithmeticOverflow::Constrain, context)?; let days = f64::from(self.days_until(&new_date)); Ok((new_date, days)) } + /// Returns the date after adding the given duration to date with a provided context. + /// + /// Temporal Equivalent: 3.5.13 `AddDate ( calendar, plainDate, duration [ , options [ , dateAdd ] ] )` + #[inline] + pub(crate) fn add_date( + &self, + duration: &Duration, + overflow: ArithmeticOverflow, + context: &mut C::Context, + ) -> TemporalResult { + // 2. If options is not present, set options to undefined. + // 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 + || duration.date().weeks != 0.0 + { + // a. If dateAdd is not present, then + // i. Set dateAdd to unused. + // ii. If calendar is an Object, set dateAdd to ? GetMethod(calendar, "dateAdd"). + // b. Return ? CalendarDateAdd(calendar, plainDate, duration, options, dateAdd). + return self.calendar().date_add(self, duration, overflow, context); + } + + // 4. Let overflow be ? ToTemporalOverflow(options). + // 5. Let norm be NormalizeTimeDuration(duration.[[Hours]], duration.[[Minutes]], duration.[[Seconds]], duration.[[Milliseconds]], duration.[[Microseconds]], duration.[[Nanoseconds]]). + // 6. Let days be duration.[[Days]] + BalanceTimeDuration(norm, "day").[[Days]]. + let days = duration.days() + + TimeDuration::from_normalized(duration.time().to_normalized(), TemporalUnit::Day)?.0; + + // 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)?; + + Ok(Self::new_unchecked(result, self.calendar().clone())) + } + + /// Returns a duration representing the difference between the dates one and two with a provided context. + /// + /// Temporal Equivalent: 3.5.6 `DifferenceDate ( calendar, one, two, options )` + #[inline] + pub(crate) fn internal_diff_date( + &self, + other: &Self, + largest_unit: TemporalUnit, + context: &mut C::Context, + ) -> TemporalResult { + if self.iso.year == other.iso.year + && self.iso.month == other.iso.month + && self.iso.day == other.iso.day + { + return Ok(Duration::default()); + } + + if largest_unit == TemporalUnit::Day { + let days = self.days_until(other); + return Ok(Duration::from_date_duration(&DateDuration::new( + 0f64, + 0f64, + 0f64, + f64::from(days), + )?)); + } + + self.calendar() + .date_until(self, other, largest_unit, context) + } + + /// Equivalent: DifferenceTemporalPlainDate + #[allow(clippy::too_many_arguments)] + pub(crate) fn diff_date( + &self, + op: bool, + other: &Self, + rounding_mode: Option, + rounding_increment: Option, + largest_unit: Option, + smallest_unit: Option, + context: &mut C::Context, + ) -> TemporalResult { + // 1. If operation is SINCE, let sign be -1. Otherwise, let sign be 1. + // 2. Set other to ? ToTemporalDate(other). + + // TODO(improvement): Implement `PartialEq` for `CalendarSlot` + // 3. If ? CalendarEquals(temporalDate.[[Calendar]], other.[[Calendar]]) is false, throw a RangeError exception. + if self.calendar().identifier(context)? != other.calendar().identifier(context)? { + return Err(TemporalError::range() + .with_message("Calendars are for difference operation are not the same.")); + } + + // 4. Let resolvedOptions be ? SnapshotOwnProperties(? GetOptionsObject(options), null). + // 5. Let settings be ? GetDifferenceSettings(operation, resolvedOptions, DATE, « », "day", "day"). + let increment = utils::to_rounding_increment(rounding_increment)?; + let (sign, rounding_mode) = if op { + ( + -1.0, + rounding_mode + .unwrap_or(TemporalRoundingMode::Trunc) + .negate(), + ) + } else { + (1.0, rounding_mode.unwrap_or(TemporalRoundingMode::Trunc)) + }; + let smallest_unit = smallest_unit.unwrap_or(TemporalUnit::Day); + // Use the defaultlargestunit which is max smallestlargestdefault and smallestunit + let largest_unit = largest_unit.unwrap_or(smallest_unit.max(TemporalUnit::Day)); + + // 6. If temporalDate.[[ISOYear]] = other.[[ISOYear]], and temporalDate.[[ISOMonth]] = other.[[ISOMonth]], + // and temporalDate.[[ISODay]] = other.[[ISODay]], then + if self.iso == other.iso { + // a. Return ! CreateTemporalDuration(0, 0, 0, 0, 0, 0, 0, 0, 0, 0). + return Ok(Duration::default()); + } + + // 7. Let calendarRec be ? CreateCalendarMethodsRecord(temporalDate.[[Calendar]], « DATE-ADD, DATE-UNTIL »). + // 8. Perform ! CreateDataPropertyOrThrow(resolvedOptions, "largestUnit", settings.[[LargestUnit]]). + // 9. Let result be ? DifferenceDate(calendarRec, temporalDate, other, resolvedOptions). + let result = self.internal_diff_date(other, largest_unit, context)?; + + // 10. If settings.[[SmallestUnit]] is "day" and settings.[[RoundingIncrement]] = 1, + // let roundingGranularityIsNoop be true; else let roundingGranularityIsNoop be false. + let is_noop = smallest_unit == TemporalUnit::Day && rounding_increment == Some(1.0); + + // 12. Return ! CreateTemporalDuration(sign × result.[[Years]], sign × result.[[Months]], sign × result.[[Weeks]], sign × result.[[Days]], 0, 0, 0, 0, 0, 0). + if is_noop { + return Duration::new( + result.years() * sign, + result.months() * sign, + result.weeks() * sign, + result.days() * sign, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ); + } + + // 11. If roundingGranularityIsNoop is false, then + // a. Let roundRecord be ? RoundDuration(result.[[Years]], result.[[Months]], result.[[Weeks]], + // result.[[Days]], ZeroTimeDuration(), settings.[[RoundingIncrement]], settings.[[SmallestUnit]], + // settings.[[RoundingMode]], temporalDate, calendarRec). + // TODO: Look into simplifying round_internal's parameters. + let round_record = result.round_internal( + increment, + smallest_unit, + rounding_mode, + &RelativeTo:: { + zdt: None, + date: Some(self), + }, + None, + context, + )?; + // b. Let roundResult be roundRecord.[[NormalizedDuration]]. + let round_result = round_record.0 .0 .0; + // c. Set result to ? BalanceDateDurationRelative(roundResult.[[Years]], roundResult.[[Months]], roundResult.[[Weeks]], + // roundResult.[[Days]], settings.[[LargestUnit]], settings.[[SmallestUnit]], temporalDate, calendarRec). + let result = + round_result.balance_relative(largest_unit, smallest_unit, Some(self), context)?; + + Duration::new( + result.years * sign, + result.months * sign, + result.weeks * sign, + result.days * sign, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ) + } } // ==== Public API ==== @@ -120,6 +294,72 @@ impl Date { pub fn days_until(&self, other: &Self) -> i32 { other.iso.to_epoch_days() - self.iso.to_epoch_days() } + + pub fn contextual_add( + &self, + duration: &Duration, + overflow: Option, + context: &mut C::Context, + ) -> TemporalResult { + self.add_date( + duration, + overflow.unwrap_or(ArithmeticOverflow::Constrain), + context, + ) + } + + pub fn contextual_subtract( + &self, + duration: &Duration, + overflow: Option, + context: &mut C::Context, + ) -> TemporalResult { + self.add_date( + &duration.negated(), + overflow.unwrap_or(ArithmeticOverflow::Constrain), + context, + ) + } + + pub fn contextual_until( + &self, + other: &Self, + rounding_mode: Option, + rounding_increment: Option, + smallest_unit: Option, + largest_unit: Option, + context: &mut C::Context, + ) -> TemporalResult { + self.diff_date( + false, + other, + rounding_mode, + rounding_increment, + smallest_unit, + largest_unit, + context, + ) + } + + pub fn contextual_since( + &self, + other: &Self, + rounding_mode: Option, + rounding_increment: Option, + smallest_unit: Option, + largest_unit: Option, + context: &mut C::Context, + ) -> TemporalResult { + self.diff_date( + true, + other, + rounding_mode, + rounding_increment, + smallest_unit, + largest_unit, + context, + ) + } } // ==== Calendar-derived Public API ==== @@ -202,6 +442,62 @@ impl Date<()> { self.calendar .in_leap_year(&CalendarDateLike::Date(self.clone()), &mut ()) } + + /// Returns the result of adding a `Duration` to the current `Date`. + pub fn add( + &self, + duration: &Duration, + overflow: Option, + ) -> TemporalResult { + self.contextual_add(duration, overflow, &mut ()) + } + + /// Returns the result of subtracting a `Duration` from the current `Date`. + pub fn subtract( + &self, + duration: &Duration, + overflow: Option, + ) -> TemporalResult { + self.contextual_subtract(duration, overflow, &mut ()) + } + + /// Returns a `Duration` representing the time until a provided `Date`. + pub fn until( + &self, + other: &Self, + rounding_mode: Option, + rounding_increment: Option, + smallest_unit: Option, + largest_unit: Option, + ) -> TemporalResult { + self.contextual_until( + other, + rounding_mode, + rounding_increment, + smallest_unit, + largest_unit, + &mut (), + ) + } + + /// Returns a `Duration` representing the time since a provided `Date`. + pub fn since( + &self, + other: &Self, + rounding_mode: Option, + rounding_increment: Option, + smallest_unit: Option, + largest_unit: Option, + ) -> TemporalResult { + self.contextual_since( + other, + rounding_mode, + rounding_increment, + smallest_unit, + largest_unit, + &mut (), + ) + } } // NOTE(nekevss): The clone below should ideally not change the memory address, but that may @@ -326,74 +622,7 @@ impl IsoDateSlots for Date { // ==== Context based API ==== -impl Date { - /// Returns the date after adding the given duration to date with a provided context. - /// - /// Temporal Equivalent: 3.5.13 `AddDate ( calendar, plainDate, duration [ , options [ , dateAdd ] ] )` - #[inline] - pub fn contextual_add_date( - &self, - duration: &Duration, - overflow: ArithmeticOverflow, - context: &mut C::Context, - ) -> TemporalResult { - // 1. If options is not present, set options to undefined. - // 2. If duration.[[Years]] ≠ 0, or duration.[[Months]] ≠ 0, or duration.[[Weeks]] ≠ 0, then - if duration.date().years != 0.0 - || duration.date().months != 0.0 - || duration.date().weeks != 0.0 - { - // a. If dateAdd is not present, then - // i. Set dateAdd to unused. - // ii. If calendar is an Object, set dateAdd to ? GetMethod(calendar, "dateAdd"). - // b. Return ? CalendarDateAdd(calendar, plainDate, duration, options, dateAdd). - return self.calendar().date_add(self, duration, overflow, context); - } - - // 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().to_normalized(), TemporalUnit::Day)?; - - // 5. 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)?; - - Ok(Self::new_unchecked(result, self.calendar().clone())) - } - - /// Returns a duration representing the difference between the dates one and two with a provided context. - /// - /// Temporal Equivalent: 3.5.6 `DifferenceDate ( calendar, one, two, options )` - #[inline] - pub fn contextual_difference_date( - &self, - other: &Self, - largest_unit: TemporalUnit, - context: &mut C::Context, - ) -> TemporalResult { - if self.iso.year == other.iso.year - && self.iso.month == other.iso.month - && self.iso.day == other.iso.day - { - return Ok(Duration::default()); - } - - if largest_unit == TemporalUnit::Day { - let days = self.days_until(other); - return Ok(Duration::from_date_duration(&DateDuration::new( - 0f64, - 0f64, - 0f64, - f64::from(days), - )?)); - } - - self.calendar() - .date_until(self, other, largest_unit, context) - } -} +impl Date {} // ==== Trait impls ==== @@ -418,3 +647,118 @@ impl FromStr for Date { )) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn simple_date_add() { + let base = Date::<()>::from_str("1976-11-18").unwrap(); + + // Test 1 + let result = base + .add(&Duration::from_str("P43Y").unwrap(), None) + .unwrap(); + assert_eq!( + result.iso, + IsoDate { + year: 2019, + month: 11, + day: 18, + } + ); + + // Test 2 + let result = base.add(&Duration::from_str("P3M").unwrap(), None).unwrap(); + assert_eq!( + result.iso, + IsoDate { + year: 1977, + month: 2, + day: 18, + } + ); + + // Test 3 + let result = base + .add(&Duration::from_str("P20D").unwrap(), None) + .unwrap(); + assert_eq!( + result.iso, + IsoDate { + year: 1976, + month: 12, + day: 8, + } + ) + } + + #[test] + fn simple_date_subtract() { + let base = Date::<()>::from_str("2019-11-18").unwrap(); + + // Test 1 + let result = base + .subtract(&Duration::from_str("P43Y").unwrap(), None) + .unwrap(); + assert_eq!( + result.iso, + IsoDate { + year: 1976, + month: 11, + day: 18, + } + ); + + // Test 2 + let result = base + .subtract(&Duration::from_str("P11M").unwrap(), None) + .unwrap(); + assert_eq!( + result.iso, + IsoDate { + year: 2018, + month: 12, + day: 18, + } + ); + + // Test 3 + let result = base + .subtract(&Duration::from_str("P20D").unwrap(), None) + .unwrap(); + assert_eq!( + result.iso, + IsoDate { + year: 2019, + month: 10, + day: 29, + } + ) + } + + #[test] + fn simple_date_until() { + let earlier = Date::<()>::from_str("1969-07-24").unwrap(); + let later = Date::<()>::from_str("1969-10-05").unwrap(); + let result = earlier.until(&later, None, None, None, None).unwrap(); + assert_eq!(result.days(), 73.0,); + + let later = Date::<()>::from_str("1996-03-03").unwrap(); + let result = earlier.until(&later, None, None, None, None).unwrap(); + assert_eq!(result.days(), 9719.0,); + } + + #[test] + fn simple_date_since() { + let earlier = Date::<()>::from_str("1969-07-24").unwrap(); + let later = Date::<()>::from_str("1969-10-05").unwrap(); + let result = later.since(&earlier, None, None, None, None).unwrap(); + assert_eq!(result.days(), 73.0,); + + let later = Date::<()>::from_str("1996-03-03").unwrap(); + let result = later.since(&earlier, None, None, None, None).unwrap(); + assert_eq!(result.days(), 9719.0,); + } +} diff --git a/src/components/duration/date.rs b/src/components/duration/date.rs index 961f01f0..ff34c3fa 100644 --- a/src/components/duration/date.rs +++ b/src/components/duration/date.rs @@ -500,7 +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.contextual_add_date( + let years_later = plain_relative_to.add_date( &years_duration, ArithmeticOverflow::Constrain, context, @@ -513,7 +513,7 @@ impl DateDuration { ); // g. Let yearsMonthsWeeksLater be ? AddDate(calendar, plainRelativeTo, yearsMonthsWeeks, undefined, dateAdd). - let years_months_weeks_later = plain_relative_to.contextual_add_date( + let years_months_weeks_later = plain_relative_to.add_date( &years_months_weeks, ArithmeticOverflow::Constrain, context, @@ -540,7 +540,7 @@ impl DateDuration { // m. Let untilOptions be OrdinaryObjectCreate(null). // n. Perform ! CreateDataPropertyOrThrow(untilOptions, "largestUnit", "year"). // o. Let timePassed be ? DifferenceDate(calendar, plainRelativeTo, wholeDaysLater, untilOptions). - let time_passed = plain_relative_to.contextual_difference_date( + let time_passed = plain_relative_to.internal_diff_date( &whole_days_later, TemporalUnit::Year, context, @@ -609,7 +609,7 @@ impl DateDuration { // i. Let dateAdd be unused. // e. Let yearsMonthsLater be ? AddDate(calendar, plainRelativeTo, yearsMonths, undefined, dateAdd). - let years_months_later = plain_relative_to.contextual_add_date( + let years_months_later = plain_relative_to.add_date( &years_months, ArithmeticOverflow::Constrain, context, @@ -621,7 +621,7 @@ impl DateDuration { ); // g. Let yearsMonthsWeeksLater be ? AddDate(calendar, plainRelativeTo, yearsMonthsWeeks, undefined, dateAdd). - let years_months_weeks_later = plain_relative_to.contextual_add_date( + let years_months_weeks_later = plain_relative_to.add_date( &years_months_weeks, ArithmeticOverflow::Constrain, context, diff --git a/src/components/duration/normalized.rs b/src/components/duration/normalized.rs index a2341156..0a3c1cf8 100644 --- a/src/components/duration/normalized.rs +++ b/src/components/duration/normalized.rs @@ -94,7 +94,7 @@ impl Add for NormalizedTimeDuration { /// A normalized `DurationRecord` that contains a `DateDuration` and `NormalizedTimeDuration`. #[derive(Debug, Clone, Copy)] -pub struct NormalizedDurationRecord(pub(super) (DateDuration, NormalizedTimeDuration)); +pub struct NormalizedDurationRecord(pub(crate) (DateDuration, NormalizedTimeDuration)); impl NormalizedDurationRecord { /// Creates a new `NormalizedDurationRecord`.