diff --git a/src/components/calendar.rs b/src/components/calendar.rs index ad6b77294..8596250f0 100644 --- a/src/components/calendar.rs +++ b/src/components/calendar.rs @@ -493,16 +493,19 @@ impl CalendarSlot { ) -> TemporalResult> { match self { CalendarSlot::Builtin(AnyCalendar::Iso(_)) => { - // 8. Let norm be NormalizeTimeDuration(duration.[[Hours]], duration.[[Minutes]], duration.[[Seconds]], duration.[[Milliseconds]], duration.[[Microseconds]], duration.[[Nanoseconds]]). + // 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). + + // 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.years(), duration.months(), duration.weeks(), duration.days() + balance_days, @@ -536,6 +539,10 @@ impl CalendarSlot { context: &mut C::Context, ) -> TemporalResult { match self { + CalendarSlot::Builtin(AnyCalendar::Iso(_)) => { + let date_duration = one.iso.diff_iso_date(&two.iso, largest_unit)?; + Ok(Duration::from_date_duration(&date_duration)) + } CalendarSlot::Builtin(_) => { Err(TemporalError::range().with_message("Not yet implemented.")) } diff --git a/src/components/date.rs b/src/components/date.rs index d1a898bb2..40f63edd3 100644 --- a/src/components/date.rs +++ b/src/components/date.rs @@ -339,9 +339,9 @@ impl Date { ) -> 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 + 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. @@ -382,7 +382,7 @@ impl Date { if largest_unit == TemporalUnit::Day { let days = self.days_until(other); - return Ok(Duration::from_date_duration(DateDuration::new( + return Ok(Duration::from_date_duration(&DateDuration::new( 0f64, 0f64, 0f64, diff --git a/src/components/duration.rs b/src/components/duration.rs index edf9b9b6a..ff815f69b 100644 --- a/src/components/duration.rs +++ b/src/components/duration.rs @@ -1,9 +1,10 @@ //! This module implements `Duration` along with it's methods and components. use crate::{ - components::{Date, DateTime, ZonedDateTime}, - options::{ArithmeticOverflow, TemporalRoundingMode, TemporalUnit}, + components::DateTime, + options::{RelativeTo, TemporalRoundingMode, TemporalUnit}, parser::{duration::parse_duration, Cursor}, + utils::{self, validate_temporal_rounding_increment}, TemporalError, TemporalResult, }; use std::str::FromStr; @@ -16,6 +17,9 @@ mod date; pub(crate) mod normalized; mod time; +#[cfg(test)] +mod tests; + #[doc(inline)] pub use date::DateDuration; #[doc(inline)] @@ -47,23 +51,64 @@ pub struct Duration { impl Duration { /// Creates a new `Duration` from a `DateDuration` and `TimeDuration`. + #[inline] pub(crate) const fn new_unchecked(date: DateDuration, time: TimeDuration) -> Self { Self { date, time } } /// Utility function to create a year duration. + #[inline] pub(crate) fn one_year(year_value: f64) -> Self { - Self::from_date_duration(DateDuration::new_unchecked(year_value, 0f64, 0f64, 0f64)) + Self::from_date_duration(&DateDuration::new_unchecked(year_value, 1f64, 0f64, 0f64)) } /// Utility function to create a month duration. + #[inline] pub(crate) fn one_month(month_value: f64) -> Self { - Self::from_date_duration(DateDuration::new_unchecked(0f64, month_value, 0f64, 0f64)) + Self::from_date_duration(&DateDuration::new_unchecked(0f64, month_value, 0f64, 0f64)) } /// Utility function to create a week duration. + #[inline] pub(crate) fn one_week(week_value: f64) -> Self { - Self::from_date_duration(DateDuration::new_unchecked(0f64, 0f64, week_value, 0f64)) + Self::from_date_duration(&DateDuration::new_unchecked(0f64, 0f64, week_value, 0f64)) + } + + /// Returns the a `Vec` of the fields values. + #[inline] + #[must_use] + pub(crate) fn fields(&self) -> Vec { + Vec::from(&[ + self.years(), + self.months(), + self.weeks(), + self.days(), + self.hours(), + self.minutes(), + self.seconds(), + self.milliseconds(), + self.microseconds(), + self.nanoseconds(), + ]) + } + + /// Returns whether `Duration`'s `DateDuration` is empty and is therefore a `TimeDuration`. + #[inline] + #[must_use] + pub(crate) fn is_time_duration(&self) -> bool { + self.time().fields().iter().any(|x| x != &0.0) + && self.date().fields().iter().all(|x| x == &0.0) + } + + /// Returns the `TemporalUnit` corresponding to the largest non-zero field. + #[inline] + pub(crate) fn default_largest_unit(&self) -> TemporalUnit { + self.fields() + .iter() + .enumerate() + .find(|x| x.1 != &0.0) + .map(|x| TemporalUnit::from(10 - x.0)) + .unwrap_or(TemporalUnit::Nanosecond) } } @@ -95,7 +140,7 @@ impl Duration { nanoseconds, ), ); - if !is_valid_duration(&duration.into_iter().collect()) { + if !is_valid_duration(&duration.fields()) { return Err(TemporalError::range().with_message("Duration was not valid.")); } Ok(duration) @@ -112,9 +157,9 @@ impl Duration { /// Creates a `Duration` from only a `DateDuration`. #[must_use] - pub fn from_date_duration(date: DateDuration) -> Self { + pub fn from_date_duration(date: &DateDuration) -> Self { Self { - date, + date: *date, time: TimeDuration::default(), } } @@ -123,10 +168,10 @@ impl Duration { /// /// Note: `TimeDuration` records can store a day value to deal with overflow. #[must_use] - pub fn from_day_and_time(day: f64, time: TimeDuration) -> Self { + pub fn from_day_and_time(day: f64, time: &TimeDuration) -> Self { Self { date: DateDuration::new_unchecked(0.0, 0.0, 0.0, day), - time, + time: *time, } } @@ -136,7 +181,7 @@ impl Duration { date: DateDuration::from_partial(partial.date()), time: TimeDuration::from_partial(partial.time()), }; - if !is_valid_duration(&duration.into_iter().collect()) { + if !is_valid_duration(&duration.fields()) { return Err(TemporalError::range().with_message("Duration was not valid.")); } Ok(duration) @@ -148,20 +193,6 @@ impl Duration { pub fn is_time_within_range(&self) -> bool { self.time.is_within_range() } - - /// Returns whether `Duration`'s `DateDuration` isn't empty and is therefore a `DateDuration` or `Duration`. - #[inline] - #[must_use] - pub fn is_date_duration(&self) -> bool { - self.date().iter().any(|x| x != 0.0) && self.time().iter().all(|x| x == 0.0) - } - - /// Returns whether `Duration`'s `DateDuration` is empty and is therefore a `TimeDuration`. - #[inline] - #[must_use] - pub fn is_time_duration(&self) -> bool { - self.time().iter().any(|x| x != 0.0) && self.date().iter().all(|x| x == 0.0) - } } // ==== Public `Duration` Getters/Setters ==== @@ -316,512 +347,23 @@ impl Duration { pub const fn nanoseconds(&self) -> f64 { self.time.nanoseconds } - - /// Returns `Duration`'s iterator - #[must_use] - pub fn iter(&self) -> DurationIter<'_> { - <&Self as IntoIterator>::into_iter(self) - } -} - -impl<'a> IntoIterator for &'a Duration { - type Item = f64; - type IntoIter = DurationIter<'a>; - - fn into_iter(self) -> Self::IntoIter { - DurationIter { - duration: self, - index: 0, - } - } -} - -/// A Duration iterator that iterates through all duration fields. -#[derive(Debug)] -pub struct DurationIter<'a> { - duration: &'a Duration, - index: usize, -} - -impl Iterator for DurationIter<'_> { - type Item = f64; - - fn next(&mut self) -> Option { - let result = match self.index { - 0 => Some(self.duration.date.years()), - 1 => Some(self.duration.date.months()), - 2 => Some(self.duration.date.weeks()), - 3 => Some(self.duration.date.days()), - 4 => Some(self.duration.time.hours()), - 5 => Some(self.duration.time.minutes()), - 6 => Some(self.duration.time.seconds()), - 7 => Some(self.duration.time.milliseconds()), - 8 => Some(self.duration.time.microseconds()), - 9 => Some(self.duration.time.nanoseconds()), - _ => None, - }; - self.index += 1; - result - } } // ==== Private Duration methods ==== impl Duration { - /// 7.5.21 `UnbalanceDateDurationRelative ( years, months, weeks, days, largestUnit, plainRelativeTo )` - #[allow(dead_code)] - pub(crate) fn unbalance_duration_relative( - &self, - largest_unit: TemporalUnit, - plain_relative_to: Option<&Date>, - context: &mut C::Context, - ) -> TemporalResult { - // 1. Let allZero be false. - // 2. If years = 0, and months = 0, and weeks = 0, and days = 0, set allZero to true. - let all_zero = self.date.years == 0_f64 - && self.date.months == 0_f64 - && self.date.weeks == 0_f64 - && self.date.days == 0_f64; - - // 3. If largestUnit is "year" or allZero is true, then - if largest_unit == TemporalUnit::Year || all_zero { - // a. Return ! CreateDateDurationRecord(years, months, weeks, days). - return Ok(self.date); - }; - - // 4. Let sign be ! DurationSign(years, months, weeks, days, 0, 0, 0, 0, 0, 0). - // 5. Assert: sign ≠ 0. - let sign = f64::from(self.sign()); - - // 6. Let oneYear be ! CreateTemporalDuration(sign, 0, 0, 0, 0, 0, 0, 0, 0, 0). - let one_year = Self::one_year(sign); - // 7. Let oneMonth be ! CreateTemporalDuration(0, sign, 0, 0, 0, 0, 0, 0, 0, 0). - let one_month = Self::one_month(sign); - - // 9. If plainRelativeTo is not undefined, then - // a. Let calendar be plainRelativeTo.[[Calendar]]. - // 10. Else, - // a. Let calendar be undefined. - - // 11. If largestUnit is "month", then - if largest_unit == TemporalUnit::Month { - // a. If years = 0, return ! CreateDateDurationRecord(0, months, weeks, days). - if self.date.years == 0f64 { - return DateDuration::new(0f64, self.date.months, self.date.weeks, self.date.days); - } - - // b. If calendar is undefined, then - let Some(mut plain_relative_to) = plain_relative_to.map(Clone::clone) else { - // i. Throw a RangeError exception. - return Err(TemporalError::range().with_message("Calendar cannot be undefined.")); - }; - - // c. If calendar is an Object, then - // i. Let dateAdd be ? GetMethod(calendar, "dateAdd"). - // ii. Let dateUntil be ? GetMethod(calendar, "dateUntil"). - // d. Else, - // i. Let dateAdd be unused. - // ii. Let dateUntil be unused. - - let mut years = self.date.years; - let mut months = self.date.months; - // e. Repeat, while years ≠ 0, - while years != 0f64 { - // i. Let newRelativeTo be ? CalendarDateAdd(calendar, plainRelativeTo, oneYear, undefined, dateAdd). - let new_relative_to = plain_relative_to.calendar().date_add( - &plain_relative_to, - &one_year, - ArithmeticOverflow::Constrain, - context, - )?; - - // ii. Let untilOptions be OrdinaryObjectCreate(null). - // iii. Perform ! CreateDataPropertyOrThrow(untilOptions, "largestUnit", "month"). - // iv. Let untilResult be ? CalendarDateUntil(calendar, plainRelativeTo, newRelativeTo, untilOptions, dateUntil). - let until_result = plain_relative_to.calendar().date_until( - &plain_relative_to, - &new_relative_to, - TemporalUnit::Month, - context, - )?; - - // v. Let oneYearMonths be untilResult.[[Months]]. - let one_year_months = until_result.date.months; - - // vi. Set plainRelativeTo to newRelativeTo. - plain_relative_to = new_relative_to; - - // vii. Set years to years - sign. - years -= sign; - // viii. Set months to months + oneYearMonths. - months += one_year_months; - } - // f. Return ? CreateDateDurationRecord(0, months, weeks, days). - return DateDuration::new(years, months, self.date.weeks, self.date.days); - - // 12. If largestUnit is "week", then - } else if largest_unit == TemporalUnit::Week { - // a. If years = 0 and months = 0, return ! CreateDateDurationRecord(0, 0, weeks, days). - if self.date.years == 0f64 && self.date.months == 0f64 { - return DateDuration::new(0f64, 0f64, self.date.weeks, self.date.days); - } - - // b. If calendar is undefined, then - let Some(mut plain_relative_to) = plain_relative_to.map(Clone::clone) else { - // i. Throw a RangeError exception. - return Err(TemporalError::range().with_message("Calendar cannot be undefined.")); - }; - - // c. If calendar is an Object, then - // i. Let dateAdd be ? GetMethod(calendar, "dateAdd"). - // d. Else, - // i. Let dateAdd be unused. - - let mut years = self.date.years; - let mut days = self.date.days; - // e. Repeat, while years ≠ 0, - while years != 0f64 { - // i. Let moveResult be ? MoveRelativeDate(calendar, plainRelativeTo, oneYear, dateAdd). - let move_result = plain_relative_to.move_relative_date(&one_year, context)?; - - // ii. Set plainRelativeTo to moveResult.[[RelativeTo]]. - plain_relative_to = move_result.0; - // iii. Set days to days + moveResult.[[Days]]. - days += move_result.1; - // iv. Set years to years - sign. - years -= sign; - } - - let mut months = self.date.months; - // f. Repeat, while months ≠ 0, - while months != 0f64 { - // i. Let moveResult be ? MoveRelativeDate(calendar, plainRelativeTo, oneMonth, dateAdd). - let move_result = plain_relative_to.move_relative_date(&one_month, context)?; - // ii. Set plainRelativeTo to moveResult.[[RelativeTo]]. - plain_relative_to = move_result.0; - // iii. Set days to days + moveResult.[[Days]]. - days += move_result.1; - // iv. Set months to months - sign. - months -= sign; - } - // g. Return ? CreateDateDurationRecord(0, 0, weeks, days). - return DateDuration::new(0f64, 0f64, self.date.weeks(), days); - } - - // 13. If years = 0, and months = 0, and weeks = 0, return ! CreateDateDurationRecord(0, 0, 0, days). - if self.date.years == 0f64 && self.date.months == 0f64 && self.date.weeks == 0f64 { - return DateDuration::new(0f64, 0f64, 0f64, self.date.days); - } - - // NOTE: Move 8 down to past 13 as we only use one_week after making it past 13. - // 8. Let oneWeek be ! CreateTemporalDuration(0, 0, sign, 0, 0, 0, 0, 0, 0, 0). - let one_week = Self::one_week(sign); - - // 14. If calendar is undefined, then - let Some(mut plain_relative_to) = plain_relative_to.map(Clone::clone) else { - // a. Throw a RangeError exception. - return Err(TemporalError::range().with_message("Calendar cannot be undefined.")); - }; - - // 15. If calendar is an Object, then - // a. Let dateAdd be ? GetMethod(calendar, "dateAdd"). - // 16. Else, - // a. Let dateAdd be unused. - - let mut years = self.date.years; - let mut days = self.date.days; - // a. Let moveResult be ? MoveRelativeDate(calendar, plainRelativeTo, oneYear, dateAdd). - while years != 0f64 { - // a. Let moveResult be ? MoveRelativeDate(calendar, plainRelativeTo, oneYear, dateAdd). - let move_result = plain_relative_to.move_relative_date(&one_year, context)?; - - // b. Set plainRelativeTo to moveResult.[[RelativeTo]]. - plain_relative_to = move_result.0; - // c. Set days to days + moveResult.[[Days]]. - days += move_result.1; - // d. Set years to years - sign. - years -= sign; - } - - let mut months = self.date.months; - // 18. Repeat, while months ≠ 0, - while months != 0f64 { - // a. Let moveResult be ? MoveRelativeDate(calendar, plainRelativeTo, oneMonth, dateAdd). - let move_result = plain_relative_to.move_relative_date(&one_month, context)?; - // b. Set plainRelativeTo to moveResult.[[RelativeTo]]. - plain_relative_to = move_result.0; - // c. Set days to days +moveResult.[[Days]]. - days += move_result.1; - // d. Set months to months - sign. - months -= sign; - } - - let mut weeks = self.date.weeks; - // 19. Repeat, while weeks ≠ 0, - while weeks != 0f64 { - // a. Let moveResult be ? MoveRelativeDate(calendar, plainRelativeTo, oneWeek, dateAdd). - let move_result = plain_relative_to.move_relative_date(&one_week, context)?; - // b. Set plainRelativeTo to moveResult.[[RelativeTo]]. - plain_relative_to = move_result.0; - // c. Set days to days + moveResult.[[Days]]. - days += move_result.1; - // d. Set weeks to weeks - sign. - weeks -= sign; - } - - // 20. Return ? CreateDateDurationRecord(0, 0, 0, days). - DateDuration::new(0f64, 0f64, 0f64, days) - } - - // TODO: Move to DateDuration - /// `BalanceDateDurationRelative` - #[allow(unused)] - pub fn balance_date_duration_relative( - &self, - largest_unit: TemporalUnit, - plain_relative_to: Option<&Date>, - context: &mut C::Context, - ) -> TemporalResult { - let mut result = self.date; - - // 1. Let allZero be false. - // 2. If years = 0, and months = 0, and weeks = 0, and days = 0, set allZero to true. - let all_zero = self.date.years == 0.0 - && self.date.months == 0.0 - && self.date.weeks == 0.0 - && self.date.days == 0.0; - - // 3. If largestUnit is not one of "year", "month", or "week", or allZero is true, then - match largest_unit { - TemporalUnit::Year | TemporalUnit::Month | TemporalUnit::Week if !all_zero => {} - _ => { - // a. Return ! CreateDateDurationRecord(years, months, weeks, days). - return Ok(result); - } - } - - // 4. If plainRelativeTo is undefined, then - let Some(mut plain_relative_to) = plain_relative_to.map(Clone::clone) else { - // a. Throw a RangeError exception. - return Err(TemporalError::range().with_message("relativeTo cannot be undefined.")); - }; - - // 5. Let sign be ! DurationSign(years, months, weeks, days, 0, 0, 0, 0, 0, 0). - // 6. Assert: sign ≠ 0. - let sign = f64::from(self.sign()); - - // 7. Let oneYear be ! CreateTemporalDuration(sign, 0, 0, 0, 0, 0, 0, 0, 0, 0). - let one_year = Self::one_year(sign); - // 8. Let oneMonth be ! CreateTemporalDuration(0, sign, 0, 0, 0, 0, 0, 0, 0, 0). - let one_month = Self::one_month(sign); - // 9. Let oneWeek be ! CreateTemporalDuration(0, 0, sign, 0, 0, 0, 0, 0, 0, 0). - let one_week = Self::one_week(sign); - - // 10. Let calendar be relativeTo.[[Calendar]]. - - match largest_unit { - // 12. If largestUnit is "year", then - TemporalUnit::Year => { - // a. If calendar is an Object, then - // i. Let dateAdd be ? GetMethod(calendar, "dateAdd"). - // b. Else, - // i. Let dateAdd be unused. - - // c. Let moveResult be ? MoveRelativeDate(calendar, relativeTo, oneYear, dateAdd). - // d. Let newRelativeTo be moveResult.[[RelativeTo]]. - // e. Let oneYearDays be moveResult.[[Days]]. - let (mut new_relative_to, mut one_year_days) = - plain_relative_to.move_relative_date(&one_year, context)?; - - // f. Repeat, while abs(days) ≥ abs(oneYearDays), - while result.days().abs() >= one_year_days.abs() { - // i. Set days to days - oneYearDays. - result.days -= one_year_days; - - // ii. Set years to years + sign. - result.years += sign; - - // iii. Set relativeTo to newRelativeTo. - plain_relative_to = new_relative_to; - // iv. Set moveResult to ? MoveRelativeDate(calendar, relativeTo, oneYear, dateAdd). - let move_result = plain_relative_to.move_relative_date(&one_year, context)?; - - // v. Set newRelativeTo to moveResult.[[RelativeTo]]. - new_relative_to = move_result.0; - // vi. Set oneYearDays to moveResult.[[Days]]. - one_year_days = move_result.1; - } - - // g. Set moveResult to ? MoveRelativeDate(calendar, relativeTo, oneMonth, dateAdd). - // h. Set newRelativeTo to moveResult.[[RelativeTo]]. - // i. Let oneMonthDays be moveResult.[[Days]]. - let (mut new_relative_to, mut one_month_days) = - plain_relative_to.move_relative_date(&one_month, context)?; - - // j. Repeat, while abs(days) ≥ abs(oneMonthDays), - while result.days().abs() >= one_month_days.abs() { - // i. Set days to days - oneMonthDays. - result.days -= one_month_days; - // ii. Set months to months + sign. - result.months += sign; - - // iii. Set relativeTo to newRelativeTo. - plain_relative_to = new_relative_to; - // iv. Set moveResult to ? MoveRelativeDate(calendar, relativeTo, oneMonth, dateAdd). - let move_result = plain_relative_to.move_relative_date(&one_month, context)?; - - // v. Set newRelativeTo to moveResult.[[RelativeTo]]. - new_relative_to = move_result.0; - // vi. Set oneMonthDays to moveResult.[[Days]]. - one_month_days = move_result.1; - } - - // k. Set newRelativeTo to ? CalendarDateAdd(calendar, relativeTo, oneYear, undefined, dateAdd). - new_relative_to = plain_relative_to.calendar().date_add( - &plain_relative_to, - &one_year, - ArithmeticOverflow::Constrain, - context, - )?; - - // l. If calendar is an Object, then - // i. Let dateUntil be ? GetMethod(calendar, "dateUntil"). - // m. Else, - // i. Let dateUntil be unused. - // n. Let untilOptions be OrdinaryObjectCreate(null). - // o. Perform ! CreateDataPropertyOrThrow(untilOptions, "largestUnit", "month"). - - // p. Let untilResult be ? CalendarDateUntil(calendar, relativeTo, newRelativeTo, untilOptions, dateUntil). - let mut until_result = plain_relative_to.calendar().date_until( - &plain_relative_to, - &new_relative_to, - TemporalUnit::Month, - context, - )?; - - // q. Let oneYearMonths be untilResult.[[Months]]. - let mut one_year_months = until_result.date.months(); - - // r. Repeat, while abs(months) ≥ abs(oneYearMonths), - while result.months().abs() >= one_year_months.abs() { - // i. Set months to months - oneYearMonths. - result.months -= one_year_months; - // ii. Set years to years + sign. - result.years += sign; - - // iii. Set relativeTo to newRelativeTo. - plain_relative_to = new_relative_to; - - // iv. Set newRelativeTo to ? CalendarDateAdd(calendar, relativeTo, oneYear, undefined, dateAdd). - new_relative_to = plain_relative_to.calendar().date_add( - &plain_relative_to, - &one_year, - ArithmeticOverflow::Constrain, - context, - )?; - - // v. Set untilOptions to OrdinaryObjectCreate(null). - // vi. Perform ! CreateDataPropertyOrThrow(untilOptions, "largestUnit", "month"). - // vii. Set untilResult to ? CalendarDateUntil(calendar, relativeTo, newRelativeTo, untilOptions, dateUntil). - until_result = plain_relative_to.calendar().date_until( - &plain_relative_to, - &new_relative_to, - TemporalUnit::Month, - context, - )?; - - // viii. Set oneYearMonths to untilResult.[[Months]]. - one_year_months = until_result.date.months(); - } - } - // 13. Else if largestUnit is "month", then - TemporalUnit::Month => { - // a. If calendar is an Object, then - // i. Let dateAdd be ? GetMethod(calendar, "dateAdd"). - // b. Else, - // i. Let dateAdd be unused. - - // c. Let moveResult be ? MoveRelativeDate(calendar, relativeTo, oneMonth, dateAdd). - // d. Let newRelativeTo be moveResult.[[RelativeTo]]. - // e. Let oneMonthDays be moveResult.[[Days]]. - let (mut new_relative_to, mut one_month_days) = - plain_relative_to.move_relative_date(&one_month, context)?; - - // f. Repeat, while abs(days) ≥ abs(oneMonthDays), - while result.days().abs() >= one_month_days.abs() { - // i. Set days to days - oneMonthDays. - result.days -= one_month_days; - - // ii. Set months to months + sign. - result.months += sign; - - // iii. Set relativeTo to newRelativeTo. - plain_relative_to = new_relative_to; - - // iv. Set moveResult to ? MoveRelativeDate(calendar, relativeTo, oneMonth, dateAdd). - let move_result = plain_relative_to.move_relative_date(&one_month, context)?; - - // v. Set newRelativeTo to moveResult.[[RelativeTo]]. - new_relative_to = move_result.0; - // vi. Set oneMonthDays to moveResult.[[Days]]. - one_month_days = move_result.1; - } - } - // 14. Else, - TemporalUnit::Week => { - // a. Assert: largestUnit is "week". - // b. If calendar is an Object, then - // i. Let dateAdd be ? GetMethod(calendar, "dateAdd"). - // c. Else, - // i. Let dateAdd be unused. - - // d. Let moveResult be ? MoveRelativeDate(calendar, relativeTo, oneWeek, dateAdd). - // e. Let newRelativeTo be moveResult.[[RelativeTo]]. - // f. Let oneWeekDays be moveResult.[[Days]]. - let (mut new_relative_to, mut one_week_days) = - plain_relative_to.move_relative_date(&one_week, context)?; - - // g. Repeat, while abs(days) ≥ abs(oneWeekDays), - while result.days().abs() >= one_week_days.abs() { - // i. Set days to days - oneWeekDays. - result.days -= one_week_days; - // ii. Set weeks to weeks + sign. - result.weeks += sign; - // iii. Set relativeTo to newRelativeTo. - plain_relative_to = new_relative_to; - // iv. Set moveResult to ? MoveRelativeDate(calendar, relativeTo, oneWeek, dateAdd). - let move_result = plain_relative_to.move_relative_date(&one_week, context)?; - - // v. Set newRelativeTo to moveResult.[[RelativeTo]]. - new_relative_to = move_result.0; - // vi. Set oneWeekDays to moveResult.[[Days]]. - one_week_days = move_result.1; - } - } - _ => unreachable!(), - } - - // 15. Return ! CreateDateDurationRecord(years, months, weeks, days). - Ok(result) - } - - // TODO (nekevss): Refactor relative_to's into a RelativeTo struct? - // TODO (nekevss): Impl `Duration::round` -> Potentially rename to `round_internal` + // TODO (nekevss): Build out `RelativeTo` handling /// Abstract Operation 7.5.26 `RoundDuration ( years, months, weeks, days, hours, minutes, /// seconds, milliseconds, microseconds, nanoseconds, increment, unit, /// roundingMode [ , plainRelativeTo [, zonedRelativeTo [, precalculatedDateTime]]] )` #[allow(clippy::type_complexity)] - #[allow(dead_code)] - pub(crate) fn round_duration( + pub(crate) fn round_internal( &self, increment: u64, unit: TemporalUnit, rounding_mode: TemporalRoundingMode, - relative_targets: ( - Option<&Date>, - Option<&ZonedDateTime>, - Option<&DateTime>, - ), + relative_to: &RelativeTo, + precalculated_dt: Option>, context: &mut C::Context, ) -> TemporalResult<(NormalizedDurationRecord, f64)> { match unit { @@ -831,7 +373,8 @@ impl Duration { increment, unit, rounding_mode, - relative_targets, + relative_to, + precalculated_dt, context, )?; let norm_record = NormalizedDurationRecord::new( @@ -847,15 +390,15 @@ impl Duration { | TemporalUnit::Microsecond | TemporalUnit::Nanosecond => { let round_result = self.time().round(increment, unit, rounding_mode)?; - let norm = NormalizedDurationRecord::new(DateDuration::default(), round_result.0)?; + let norm = NormalizedDurationRecord::new(*self.date(), round_result.0)?; Ok((norm, round_result.1 as f64)) } TemporalUnit::Auto => { Err(TemporalError::range().with_message("Invalid TemporalUnit for Duration.round")) } } - - // 18. Let duration be ? CreateDurationRecord(years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds). + // 18. Let duration be ? CreateDurationRecord(years, months, weeks, days, hours, + // minutes, seconds, milliseconds, microseconds, nanoseconds). // 19. Return the Record { [[DurationRecord]]: duration, [[Total]]: total }. } } @@ -867,7 +410,7 @@ impl Duration { #[inline] #[must_use] pub fn sign(&self) -> i32 { - duration_sign(&self.iter().collect()) + duration_sign(&self.fields()) } /// Returns whether the current `Duration` is zero. @@ -876,7 +419,7 @@ impl Duration { #[inline] #[must_use] pub fn is_zero(&self) -> bool { - duration_sign(&self.iter().collect()) == 0 + self.sign() == 0 } /// Returns a negated `Duration` @@ -899,36 +442,205 @@ impl Duration { } } - /// 7.5.12 `DefaultTemporalLargestUnit ( years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds )` + /// Rounds the current `Duration`. #[inline] - #[must_use] - pub fn default_temporal_largest_unit(&self) -> TemporalUnit { - for (index, value) in self.into_iter().enumerate() { - if value == 0f64 { - continue; - } + pub fn round( + &self, + increment: Option, + smallest_unit: Option, + largest_unit: Option, + rounding_mode: Option, + relative_to: &RelativeTo, + context: &mut C::Context, + ) -> TemporalResult { + // NOTE: Steps 1-14 seem to be implementation specific steps. - match index { - 0 => return TemporalUnit::Year, - 1 => return TemporalUnit::Month, - 2 => return TemporalUnit::Week, - 3 => return TemporalUnit::Day, - 4 => return TemporalUnit::Hour, - 5 => return TemporalUnit::Minute, - 6 => return TemporalUnit::Second, - 7 => return TemporalUnit::Millisecond, - 8 => return TemporalUnit::Microsecond, - _ => {} - } + // 22. If smallestUnitPresent is false and largestUnitPresent is false, then + if largest_unit.is_none() && smallest_unit.is_none() { + // a. Throw a RangeError exception. + return Err(TemporalError::range() + .with_message("smallestUnit and largestUnit cannot both be None.")); } - TemporalUnit::Nanosecond - } + // 14. Let roundingIncrement be ? ToTemporalRoundingIncrement(roundTo). + let increment = utils::to_rounding_increment(increment)?; + // 15. Let roundingMode be ? ToTemporalRoundingMode(roundTo, "halfExpand"). + let mode = rounding_mode.unwrap_or_default(); + + // 16. Let smallestUnit be ? GetTemporalUnit(roundTo, "smallestUnit", DATETIME, undefined). + // 17. If smallestUnit is undefined, then + // a. Set smallestUnitPresent to false. + // b. Set smallestUnit to "nanosecond". + let smallest_unit = smallest_unit.unwrap_or(TemporalUnit::Nanosecond); + + // 18. Let existingLargestUnit be ! DefaultTemporalLargestUnit(duration.[[Years]], + // duration.[[Months]], duration.[[Weeks]], duration.[[Days]], duration.[[Hours]], + // duration.[[Minutes]], duration.[[Seconds]], duration.[[Milliseconds]], + // duration.[[Microseconds]]). + let existing_largest_unit = self.default_largest_unit(); + + // 19. Let defaultLargestUnit be LargerOfTwoTemporalUnits(existingLargestUnit, smallestUnit). + let default_largest = existing_largest_unit.max(smallest_unit); + + // 20. If largestUnit is undefined, then + // a. Set largestUnitPresent to false. + // b. Set largestUnit to defaultLargestUnit. + // 21. Else if largestUnit is "auto", then + // a. Set largestUnit to defaultLargestUnit. + let largest_unit = match largest_unit { + Some(TemporalUnit::Auto) | None => default_largest, + Some(unit) => unit, + }; - /// 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().to_normalized(), unit) + // 23. If LargerOfTwoTemporalUnits(largestUnit, smallestUnit) is not largestUnit, throw a RangeError exception. + if largest_unit.max(smallest_unit) != largest_unit { + return Err(TemporalError::range().with_message( + "largestUnit when rounding Duration was not the largest provided unit", + )); + } + + // 24. Let maximum be MaximumTemporalDurationRoundingIncrement(smallestUnit). + let maximum = smallest_unit.to_maximum_rounding_increment(); + // 25. If maximum is not undefined, perform ? ValidateTemporalRoundingIncrement(roundingIncrement, maximum, false). + if let Some(max) = maximum { + validate_temporal_rounding_increment(increment, max.into(), false)?; + } + + // 26. Let hoursToDaysConversionMayOccur be false. + // 27. If duration.[[Days]] ≠ 0 and zonedRelativeTo is not undefined, set hoursToDaysConversionMayOccur to true. + // 28. Else if abs(duration.[[Hours]]) ≥ 24, set hoursToDaysConversionMayOccur to true. + let hours_to_days_may_occur = + (self.days() != 0.0 && relative_to.zdt.is_some()) || self.hours().abs() >= 24.0; + + // 29. If smallestUnit is "nanosecond" and roundingIncrement = 1, let roundingGranularityIsNoop + // be true; else let roundingGranularityIsNoop be false. + let is_noop = smallest_unit == TemporalUnit::Nanosecond && increment == 1; + // 30. If duration.[[Years]] = 0 and duration.[[Months]] = 0 and duration.[[Weeks]] = 0, + // let calendarUnitsPresent be false; else let calendarUnitsPresent be true. + let calendar_units_present = + !(self.years() == 0.0 && self.months() == 0.0 && self.weeks() == 0.0); + + // 31. If roundingGranularityIsNoop is true, and largestUnit is existingLargestUnit, and calendarUnitsPresent is false, + // and hoursToDaysConversionMayOccur is false, and abs(duration.[[Minutes]]) < 60, and abs(duration.[[Seconds]]) < 60, + // and abs(duration.[[Milliseconds]]) < 1000, and abs(duration.[[Microseconds]]) < 1000, and abs(duration.[[Nanoseconds]]) < 1000, then + if is_noop + && largest_unit == existing_largest_unit + && !calendar_units_present + && !hours_to_days_may_occur + && self.minutes().abs() < 60.0 + && self.seconds().abs() < 60.0 + && self.milliseconds() < 1000.0 + && self.microseconds() < 1000.0 + && self.nanoseconds() < 1000.0 + { + // a. NOTE: The above conditions mean that the operation will have no effect: the + // smallest unit and rounding increment will leave the total duration unchanged, + // and it can be determined without calling a calendar or time zone method that + // no balancing will take place. + // b. Return ! CreateTemporalDuration(duration.[[Years]], duration.[[Months]], + // duration.[[Weeks]], duration.[[Days]], duration.[[Hours]], duration.[[Minutes]], + // duration.[[Seconds]], duration.[[Milliseconds]], duration.[[Microseconds]], + // duration.[[Nanoseconds]]). + return Ok(*self); + } + + // 32. Let precalculatedPlainDateTime be undefined. + // 33. If roundingGranularityIsNoop is false, or IsCalendarUnit(largestUnit) is true, or largestUnit is "day", + // or calendarUnitsPresent is true, or duration.[[Days]] ≠ 0, let plainDateTimeOrRelativeToWillBeUsed be true; + // else let plainDateTimeOrRelativeToWillBeUsed be false. + let pdtr_will_be_used = !is_noop + || largest_unit.is_calendar_unit() + || largest_unit == TemporalUnit::Day + || calendar_units_present + || self.days() == 0.0; + + // 34. If zonedRelativeTo is not undefined and plainDateTimeOrRelativeToWillBeUsed is true, then + let precalculated = if relative_to.zdt.is_some() && pdtr_will_be_used { + return Err(TemporalError::general("Not yet implemented.")); + // a. NOTE: The above conditions mean that the corresponding Temporal.PlainDateTime or + // Temporal.PlainDate for zonedRelativeTo will be used in one of the operations below. + // b. Let instant be ! CreateTemporalInstant(zonedRelativeTo.[[Nanoseconds]]). + // c. Set precalculatedPlainDateTime to ? GetPlainDateTimeFor(timeZoneRec, instant, zonedRelativeTo.[[Calendar]]). + // d. Set plainRelativeTo to ! CreateTemporalDate(precalculatedPlainDateTime.[[ISOYear]], + // precalculatedPlainDateTime.[[ISOMonth]], precalculatedPlainDateTime.[[ISODay]], zonedRelativeTo.[[Calendar]]). + } else { + None + }; + // 35. Let calendarRec be ? CreateCalendarMethodsRecordFromRelativeTo(plainRelativeTo, zonedRelativeTo, « DATE-ADD, DATE-UNTIL »). + + // TODO: Validate below expect -> Potentially return error. + let relative_to_date = relative_to.date.expect("Date must exist at this point."); + + // 36. Let unbalanceResult be ? UnbalanceDateDurationRelative(duration.[[Years]], duration.[[Months]], duration.[[Weeks]], duration.[[Days]], largestUnit, plainRelativeTo, calendarRec). + let unbalanced = + self.date() + .unbalance_relative(largest_unit, Some(relative_to_date), context)?; + + // NOTE: Step 37 handled in round duration + // 37. Let norm be NormalizeTimeDuration(duration.[[Hours]], duration.[[Minutes]], duration.[[Seconds]], + // duration.[[Milliseconds]], duration.[[Microseconds]], duration.[[Nanoseconds]]). + // 38. Let roundRecord be ? RoundDuration(unbalanceResult.[[Years]], unbalanceResult.[[Months]], + // unbalanceResult.[[Weeks]], unbalanceResult.[[Days]], norm, roundingIncrement, smallestUnit, + // roundingMode, plainRelativeTo, calendarRec, zonedRelativeTo, timeZoneRec, precalculatedPlainDateTime). + let (round_result, _) = Self::new_unchecked(unbalanced, *self.time()).round_internal( + increment, + smallest_unit, + mode, + relative_to, + precalculated, + context, + )?; + + // 39. Let roundResult be roundRecord.[[NormalizedDuration]]. + // 40. If zonedRelativeTo is not undefined, then + let balance_result = if relative_to.zdt.is_some() { + return Err(TemporalError::general("Not yet implemented.")); + // a. Set roundResult to ? AdjustRoundedDurationDays(roundResult.[[Years]], roundResult.[[Months]], + // roundResult.[[Weeks]], roundResult.[[Days]], roundResult.[[NormalizedTime]], roundingIncrement, + // smallestUnit, roundingMode, zonedRelativeTo, calendarRec, timeZoneRec, precalculatedPlainDateTime). + // b. Let balanceResult be ? BalanceTimeDurationRelative(roundResult.[[Days]], + // roundResult.[[NormalizedTime]], largestUnit, zonedRelativeTo, timeZoneRec, precalculatedPlainDateTime). + // 41. Else, + } else { + // NOTE: DateDuration::round will always return a NormalizedTime::default as per spec. + // a. Let normWithDays be ? Add24HourDaysToNormalizedTimeDuration(roundResult.[[NormalizedTime]], roundResult.[[Days]]). + let norm_with_days = round_result.0 .1.add_days(round_result.0 .0.days)?; + // b. Let balanceResult be BalanceTimeDuration(normWithDays, largestUnit). + TimeDuration::from_normalized(norm_with_days, largest_unit)? + }; + + // 42. Let result be ? BalanceDateDurationRelative(roundResult.[[Years]], + // roundResult.[[Months]], roundResult.[[Weeks]], balanceResult.[[Days]], + // largestUnit, smallestUnit, plainRelativeTo, calendarRec). + let intermediate = DateDuration::new_unchecked( + round_result.0 .0.years, + round_result.0 .0.months, + round_result.0 .0.weeks, + balance_result.0, + ); + let result = intermediate.balance_relative( + largest_unit, + smallest_unit, + Some(relative_to_date), + context, + )?; + + // 43. Return ! CreateTemporalDuration(result.[[Years]], result.[[Months]], + // result.[[Weeks]], result.[[Days]], balanceResult.[[Hours]], balanceResult.[[Minutes]], + // balanceResult.[[Seconds]], balanceResult.[[Milliseconds]], balanceResult.[[Microseconds]], + // balanceResult.[[Nanoseconds]]). + Self::new( + result.years, + result.months, + result.weeks, + result.days, + balance_result.1.hours, + balance_result.1.minutes, + balance_result.1.seconds, + balance_result.1.milliseconds, + balance_result.1.microseconds, + balance_result.1.nanoseconds, + ) } } diff --git a/src/components/duration/date.rs b/src/components/duration/date.rs index bc1cd5f28..961f01f04 100644 --- a/src/components/duration/date.rs +++ b/src/components/duration/date.rs @@ -3,9 +3,9 @@ use crate::{ components::{ calendar::CalendarProtocol, duration::TimeDuration, tz::TzProtocol, Date, DateTime, - Duration, ZonedDateTime, + Duration, }, - options::{ArithmeticOverflow, TemporalRoundingMode, TemporalUnit}, + options::{ArithmeticOverflow, RelativeTo, TemporalRoundingMode, TemporalUnit}, utils, TemporalError, TemporalResult, NS_PER_DAY, }; @@ -20,10 +20,14 @@ use super::normalized::NormalizedTimeDuration; #[non_exhaustive] #[derive(Debug, Default, Clone, Copy)] pub struct DateDuration { - pub(crate) years: f64, - pub(crate) months: f64, - pub(crate) weeks: f64, - pub(crate) days: f64, + /// `DateDuration`'s internal year value. + pub years: f64, + /// `DateDuration`'s internal month value. + pub months: f64, + /// `DateDuration`'s internal week value. + pub weeks: f64, + /// `DateDuration`'s internal day value. + pub days: f64, } impl DateDuration { @@ -38,13 +42,314 @@ impl DateDuration { days, } } + + /// Returns the `TemporalUnit` corresponding to the largest non-zero field. + #[inline] + pub(crate) fn default_largest_unit(&self) -> TemporalUnit { + self.fields() + .iter() + .enumerate() + .find(|x| x.1 != &0.0) + .map(|x| TemporalUnit::from(10 - x.0)) + .unwrap_or(TemporalUnit::Nanosecond) + } + + /// 7.5.38 `UnbalanceDateDurationRelative ( years, months, weeks, days, largestUnit, plainRelativeTo )` + #[inline] + pub(crate) fn unbalance_relative( + &self, + largest_unit: TemporalUnit, + plain_relative_to: Option<&Date>, + context: &mut C::Context, + ) -> TemporalResult { + // 1. Assert: If plainRelativeTo is not undefined, calendarRec is not undefined. + let plain_relative = plain_relative_to.expect("plainRelativeTo must not be undefined."); + + // 2. Let defaultLargestUnit be DefaultTemporalLargestUnit(years, months, weeks, days, 0, 0, 0, 0, 0). + let default_largest = self.default_largest_unit(); + + // 3. Let effectiveLargestUnit be LargerOfTwoTemporalUnits(largestUnit, "day"). + let effective_largest = largest_unit.max(TemporalUnit::Day); + + // 4. If effectiveLargestUnit is LargerOfTwoTemporalUnits(defaultLargestUnit, effectiveLargestUnit), then + if default_largest <= effective_largest { + // a. Return ! CreateDateDurationRecord(years, months, weeks, days). + return Ok(*self); + } + + // NOTE: Should the assertion in 5 be an early error. + // 5. Assert: effectiveLargestUnit is not "year". + // 6. If calendarRec is undefined, then + // a. Throw a RangeError exception. + // 7. Assert: CalendarMethodsRecordHasLookedUp(calendarRec, DATE-ADD) is true. + + match effective_largest { + // 8. If effectiveLargestUnit is "month", then + TemporalUnit::Month => { + // a. Assert: CalendarMethodsRecordHasLookedUp(calendarRec, DATE-UNTIL) is true. + // b. Let yearsDuration be ! CreateTemporalDuration(years, 0, 0, 0, 0, 0, 0, 0, 0, 0). + let years = + Duration::from_date_duration(&Self::new_unchecked(self.years, 0.0, 0.0, 0.0)); + + // c. Let later be ? CalendarDateAdd(calendarRec, plainRelativeTo, yearsDuration). + let later = plain_relative.calendar().date_add( + plain_relative, + &years, + ArithmeticOverflow::Constrain, + context, + )?; + + // d. Let untilOptions be OrdinaryObjectCreate(null). + // e. Perform ! CreateDataPropertyOrThrow(untilOptions, "largestUnit", "month"). + // f. Let untilResult be ? CalendarDateUntil(calendarRec, plainRelativeTo, later, untilOptions). + let until = plain_relative.calendar().date_until( + plain_relative, + &later, + largest_unit, + context, + )?; + + // g. Let yearsInMonths be untilResult.[[Months]]. + // h. Return ? CreateDateDurationRecord(0, months + yearsInMonths, weeks, days). + Self::new(0.0, self.months + until.months(), self.weeks, self.days) + } + // 9. If effectiveLargestUnit is "week", then + TemporalUnit::Week => { + // a. Let yearsMonthsDuration be ! CreateTemporalDuration(years, months, 0, 0, 0, 0, 0, 0, 0, 0). + let years_months = Duration::from_date_duration(&Self::new_unchecked( + self.years, + self.months, + 0.0, + 0.0, + )); + + // b. Let later be ? CalendarDateAdd(calendarRec, plainRelativeTo, yearsMonthsDuration). + let later = plain_relative.calendar().date_add( + plain_relative, + &years_months, + ArithmeticOverflow::Constrain, + context, + )?; + + // c. Let yearsMonthsInDays be DaysUntil(plainRelativeTo, later). + let years_months_in_days = + plain_relative.iso.to_epoch_days() - later.iso.to_epoch_days(); + // d. Return ? CreateDateDurationRecord(0, 0, weeks, days + yearsMonthsInDays). + Self::new( + 0.0, + 0.0, + self.weeks, + self.days + f64::from(years_months_in_days), + ) + } + TemporalUnit::Day => { + // 10. NOTE: largestUnit can be any time unit as well as "day". + // 11. Let yearsMonthsWeeksDuration be ! CreateTemporalDuration(years, months, weeks, 0, 0, 0, 0, 0, 0, 0). + let years_months_weeks = Duration::from_date_duration(&Self::new_unchecked( + self.years, + self.months, + self.weeks, + 0.0, + )); + + // 12. Let later be ? CalendarDateAdd(calendarRec, plainRelativeTo, yearsMonthsWeeksDuration). + let later = plain_relative.calendar().date_add( + plain_relative, + &years_months_weeks, + ArithmeticOverflow::Constrain, + context, + )?; + + // 13. Let yearsMonthsWeeksInDays be DaysUntil(plainRelativeTo, later). + let years_months_weeks_in_days = + plain_relative.iso.to_epoch_days() - later.iso.to_epoch_days(); + // 14. Return ? CreateDateDurationRecord(0, 0, 0, days + yearsMonthsWeeksInDays). + Self::new( + 0.0, + 0.0, + self.weeks, + self.days + f64::from(years_months_weeks_in_days), + ) + } + _ => Err(TemporalError::general( + "Invalid TemporalUnit provided to UnbalanceDateDurationRelative", + )), + } + } + + /// 7.5.38 BalanceDateDurationRelative ( years, months, weeks, days, largestUnit, smallestUnit, plainRelativeTo, calendarRec ) + pub fn balance_relative( + &self, + largest_unit: TemporalUnit, + smallest_unit: TemporalUnit, + plain_relative_to: Option<&Date>, + context: &mut C::Context, + ) -> TemporalResult { + // TODO: Confirm 1 or 5 based off response to issue. + // 1. Assert: If plainRelativeTo is not undefined, calendarRec is not undefined. + let plain_relative = plain_relative_to.expect("plainRelativeTo must not be undefined."); + + // 2. Let allZero be false. + // 3. If years = 0, and months = 0, and weeks = 0, and days = 0, set allZero to true. + let all_zero = + self.years == 0.0 && self.months == 0.0 && self.weeks == 0.0 && self.days == 0.0; + + // 4. If largestUnit is not one of "year", "month", or "week", or allZero is true, then + match largest_unit { + TemporalUnit::Year | TemporalUnit::Month | TemporalUnit::Week if !all_zero => {} + _ => { + // a. Return ! CreateDateDurationRecord(years, months, weeks, days). + return Ok(*self); + } + } + + // NOTE: See Step 1. + // 5. If plainRelativeTo is undefined, then + // a. Throw a RangeError exception. + // 6. Assert: CalendarMethodsRecordHasLookedUp(calendarRec, DATE-ADD) is true. + // 7. Assert: CalendarMethodsRecordHasLookedUp(calendarRec, DATE-UNTIL) is true. + // 8. Let untilOptions be OrdinaryObjectCreate(null). + // 9. Perform ! CreateDataPropertyOrThrow(untilOptions, "largestUnit", largestUnit). + + match largest_unit { + // 10. If largestUnit is "year", then + TemporalUnit::Year => { + // a. If smallestUnit is "week", then + if smallest_unit == TemporalUnit::Week { + // i. Assert: days = 0. + // ii. Let yearsMonthsDuration be ! CreateTemporalDuration(years, months, 0, 0, 0, 0, 0, 0, 0, 0). + let years_months = Duration::from_date_duration(&Self::new_unchecked( + self.years, + self.months, + 0.0, + 0.0, + )); + + // iii. Let later be ? AddDate(calendarRec, plainRelativeTo, yearsMonthsDuration). + let later = plain_relative.calendar().date_add( + plain_relative, + &years_months, + ArithmeticOverflow::Constrain, + context, + )?; + + // iv. Let untilResult be ? CalendarDateUntil(calendarRec, plainRelativeTo, later, untilOptions). + let until = plain_relative.calendar().date_until( + plain_relative, + &later, + largest_unit, + context, + )?; + + // v. Return ? CreateDateDurationRecord(untilResult.[[Years]], untilResult.[[Months]], weeks, 0). + return Self::new(until.years(), until.months(), self.weeks, 0.0); + } + + // b. Let yearsMonthsWeeksDaysDuration be ! CreateTemporalDuration(years, months, weeks, days, 0, 0, 0, 0, 0, 0). + let years_months_weeks = Duration::from_date_duration(self); + + // c. Let later be ? AddDate(calendarRec, plainRelativeTo, yearsMonthsWeeksDaysDuration). + let later = plain_relative.calendar().date_add( + plain_relative, + &years_months_weeks, + ArithmeticOverflow::Constrain, + context, + )?; + // d. Let untilResult be ? CalendarDateUntil(calendarRec, plainRelativeTo, later, untilOptions). + let until = plain_relative.calendar().date_until( + plain_relative, + &later, + largest_unit, + context, + )?; + // e. Return ! CreateDateDurationRecord(untilResult.[[Years]], untilResult.[[Months]], untilResult.[[Weeks]], untilResult.[[Days]]). + Self::new(until.years(), until.months(), until.weeks(), until.days()) + } + // 11. If largestUnit is "month", then + TemporalUnit::Month => { + // a. Assert: years = 0. + // b. If smallestUnit is "week", then + if smallest_unit == TemporalUnit::Week { + // i. Assert: days = 0. + // ii. Return ! CreateDateDurationRecord(0, months, weeks, 0). + return Self::new(0.0, self.months, self.weeks, 0.0); + } + + // c. Let monthsWeeksDaysDuration be ! CreateTemporalDuration(0, months, weeks, days, 0, 0, 0, 0, 0, 0). + let months_weeks_days = Duration::from_date_duration(&Self::new_unchecked( + 0.0, + self.months, + self.weeks, + self.days, + )); + + // d. Let later be ? AddDate(calendarRec, plainRelativeTo, monthsWeeksDaysDuration). + let later = plain_relative.calendar().date_add( + plain_relative, + &months_weeks_days, + ArithmeticOverflow::Constrain, + context, + )?; + + // e. Let untilResult be ? CalendarDateUntil(calendarRec, plainRelativeTo, later, untilOptions). + let until = plain_relative.calendar().date_until( + plain_relative, + &later, + largest_unit, + context, + )?; + + // f. Return ! CreateDateDurationRecord(0, untilResult.[[Months]], untilResult.[[Weeks]], untilResult.[[Days]]). + Self::new(0.0, until.months(), until.weeks(), until.days()) + } + // 12. Assert: largestUnit is "week". + TemporalUnit::Week => { + // 13. Assert: years = 0. + // 14. Assert: months = 0. + // 15. Let weeksDaysDuration be ! CreateTemporalDuration(0, 0, weeks, days, 0, 0, 0, 0, 0, 0). + let weeks_days = Duration::from_date_duration(&Self::new_unchecked( + 0.0, 0.0, self.weeks, self.days, + )); + + // 16. Let later be ? AddDate(calendarRec, plainRelativeTo, weeksDaysDuration). + let later = plain_relative.calendar().date_add( + plain_relative, + &weeks_days, + ArithmeticOverflow::Constrain, + context, + )?; + + // 17. Let untilResult be ? CalendarDateUntil(calendarRec, plainRelativeTo, later, untilOptions). + let until = plain_relative.calendar().date_until( + plain_relative, + &later, + largest_unit, + context, + )?; + + // 18. Return ! CreateDateDurationRecord(0, 0, untilResult.[[Weeks]], untilResult.[[Days]]). + Self::new(0.0, 0.0, until.weeks(), until.days()) + } + _ => Err(TemporalError::general( + "largestUnit in BalanceDateDurationRelative exceeded possible values.", + )), + } + } + + /// Returns the iterator for `DateDuration` + #[inline] + #[must_use] + pub(crate) fn fields(&self) -> Vec { + Vec::from(&[self.years, self.months, self.weeks, self.days]) + } } impl DateDuration { /// Creates a new `DateDuration` with provided values. + #[inline] pub fn new(years: f64, months: f64, weeks: f64, days: f64) -> TemporalResult { let result = Self::new_unchecked(years, months, weeks, days); - if !super::is_valid_duration(&result.into_iter().collect()) { + if !super::is_valid_duration(&result.fields()) { return Err(TemporalError::range().with_message("Invalid DateDuration.")); } Ok(result) @@ -116,37 +421,7 @@ impl DateDuration { #[inline] #[must_use] pub fn sign(&self) -> i32 { - super::duration_sign(&self.iter().collect()) - } - - /// Returns the `[[years]]` value. - #[must_use] - pub const fn years(&self) -> f64 { - self.years - } - - /// Returns the `[[months]]` value. - #[must_use] - pub const fn months(&self) -> f64 { - self.months - } - - /// Returns the `[[weeks]]` value. - #[must_use] - pub const fn weeks(&self) -> f64 { - self.weeks - } - - /// Returns the `[[days]]` value. - #[must_use] - pub const fn days(&self) -> f64 { - self.days - } - - /// Returns the iterator for `DateDuration` - #[must_use] - pub fn iter(&self) -> DateIter<'_> { - <&Self as IntoIterator>::into_iter(self) + super::duration_sign(&self.fields()) } } @@ -155,26 +430,26 @@ impl DateDuration { impl DateDuration { /// Rounds the current `DateDuration` returning a tuple of the rounded `DateDuration` and /// the `total` value of the smallest unit prior to rounding. - #[allow(clippy::type_complexity, clippy::let_and_return)] + #[allow( + clippy::type_complexity, + clippy::let_and_return, + clippy::too_many_arguments + )] pub fn round( &self, normalized_time: Option, increment: u64, unit: TemporalUnit, rounding_mode: TemporalRoundingMode, - relative_targets: ( - Option<&Date>, - Option<&ZonedDateTime>, - Option<&DateTime>, - ), + relative_to: &RelativeTo, + _precalculated_dt: Option>, context: &mut C::Context, ) -> TemporalResult<(Self, f64)> { // 1. If plainRelativeTo is not present, set plainRelativeTo to undefined. - let plain_relative_to = relative_targets.0; + let plain_relative_to = relative_to.date; // 2. If zonedRelativeTo is not present, set zonedRelativeTo to undefined. - let zoned_relative_to = relative_targets.1; + let zoned_relative_to = relative_to.zdt; // 3. If precalculatedPlainDateTime is not present, set precalculatedPlainDateTime to undefined. - let _ = relative_targets.2; let mut fractional_days = match unit { // 4. If unit is "year", "month", or "week", and plainRelativeTo is undefined, then @@ -197,7 +472,7 @@ impl DateDuration { // b. Else, } else { // i. Let fractionalDays be days + DivideNormalizedTimeDuration(norm, nsPerDay). - self.days + normalized_time.unwrap_or_default().divide(NS_PER_DAY) as f64 + self.days + normalized_time.unwrap_or_default().divide(NS_PER_DAY) } // c. Set days to 0. } @@ -206,7 +481,6 @@ impl DateDuration { .with_message("Invalid TemporalUnit provided to DateDuration.round")) } }; - // 7. let total be unset. // We begin matching against unit and return the remainder value. match unit { @@ -273,10 +547,10 @@ impl DateDuration { )?; // p. Let yearsPassed be timePassed.[[Years]]. - let years_passed = time_passed.date.years(); + let years_passed = time_passed.date.years; // q. Set years to years + yearsPassed. - let years = self.years() + years_passed; + let years = self.years + years_passed; // r. Let yearsDuration be ! CreateTemporalDuration(yearsPassed, 0, 0, 0, 0, 0, 0, 0, 0, 0). let years_duration = Duration::one_year(years_passed); @@ -301,12 +575,15 @@ impl DateDuration { let (_, one_year_days) = plain_relative_to.move_relative_date(&one_year, context)?; + if one_year_days == 0.0 { + return Err(TemporalError::range().with_message("oneYearDays exceeds ranges.")); + } // aa. Let fractionalYears be years + fractionalDays / abs(oneYearDays). let frac_years = years + (fractional_days / one_year_days.abs()); // ab. Set years to RoundNumberToIncrement(fractionalYears, increment, roundingMode). let rounded_years = - utils::round_number_to_increment(frac_years as i64, increment, rounding_mode); + utils::round_number_to_increment(frac_years, increment as f64, rounding_mode); // ac. Set total to fractionalYears. // ad. Set months and weeks to 0. @@ -319,9 +596,9 @@ impl DateDuration { let plain_relative_to = plain_relative_to.expect("this must exist."); // b. Let yearsMonths be ! CreateTemporalDuration(years, months, 0, 0, 0, 0, 0, 0, 0, 0). - let years_months = Duration::from_date_duration(DateDuration::new_unchecked( - self.years(), - self.months(), + let years_months = Duration::from_date_duration(&DateDuration::new_unchecked( + self.years, + self.months, 0.0, 0.0, )); @@ -339,12 +616,9 @@ impl DateDuration { )?; // f. Let yearsMonthsWeeks be ! CreateTemporalDuration(years, months, weeks, 0, 0, 0, 0, 0, 0, 0). - let years_months_weeks = Duration::from_date_duration(DateDuration::new_unchecked( - self.years(), - self.months(), - self.weeks(), - 0.0, - )); + let years_months_weeks = Duration::from_date_duration( + &DateDuration::new_unchecked(self.years, self.months, self.weeks, 0.0), + ); // g. Let yearsMonthsWeeksLater be ? AddDate(calendar, plainRelativeTo, yearsMonthsWeeks, undefined, dateAdd). let years_months_weeks_later = plain_relative_to.contextual_add_date( @@ -397,7 +671,7 @@ impl DateDuration { // r. Set months to RoundNumberToIncrement(fractionalMonths, increment, roundingMode). let rounded_months = - utils::round_number_to_increment(frac_months as i64, increment, rounding_mode); + utils::round_number_to_increment(frac_months, increment as f64, rounding_mode); // s. Set total to fractionalMonths. // t. Set weeks to 0. @@ -449,7 +723,7 @@ impl DateDuration { // k. Set weeks to RoundNumberToIncrement(fractionalWeeks, increment, roundingMode). let rounded_weeks = - utils::round_number_to_increment(frac_weeks as i64, increment, rounding_mode); + utils::round_number_to_increment(frac_weeks, increment as f64, rounding_mode); // l. Set total to fractionalWeeks. let result = Self::new(self.years, self.months, rounded_weeks as f64, 0f64)?; Ok((result, frac_weeks)) @@ -458,8 +732,8 @@ impl DateDuration { TemporalUnit::Day => { // a. Set days to RoundNumberToIncrement(fractionalDays, increment, roundingMode). let rounded_days = utils::round_number_to_increment( - fractional_days as i64, - increment, + fractional_days, + increment as f64, rounding_mode, ); @@ -472,38 +746,3 @@ impl DateDuration { } } } - -impl<'a> IntoIterator for &'a DateDuration { - type Item = f64; - type IntoIter = DateIter<'a>; - - fn into_iter(self) -> Self::IntoIter { - DateIter { - date: self, - index: 0, - } - } -} - -/// An iterator over the `DateDuration` -#[derive(Debug)] -pub struct DateIter<'a> { - date: &'a DateDuration, - index: usize, -} - -impl Iterator for DateIter<'_> { - type Item = f64; - - fn next(&mut self) -> Option { - let result = match self.index { - 0 => Some(self.date.years), - 1 => Some(self.date.months), - 2 => Some(self.date.weeks), - 3 => Some(self.date.days), - _ => None, - }; - self.index += 1; - result - } -} diff --git a/src/components/duration/normalized.rs b/src/components/duration/normalized.rs index 3238d7247..a2341156c 100644 --- a/src/components/duration/normalized.rs +++ b/src/components/duration/normalized.rs @@ -38,9 +38,9 @@ impl NormalizedTimeDuration { // TODO: Implement as `ops::Div` /// `Divide the NormalizedTimeDuraiton` by a divisor. - pub(super) fn divide(&self, divisor: i64) -> i64 { + pub(super) fn divide(&self, divisor: i64) -> f64 { // TODO: Validate. - self.0 as i64 / divisor + self.0 / (divisor as f64) } /// Equivalent: 7.5.31 NormalizedTimeDurationSign ( d ) @@ -68,7 +68,7 @@ impl NormalizedTimeDuration { /// Round the current `NormalizedTimeDuration`. pub(super) fn round(&self, increment: u64, mode: TemporalRoundingMode) -> TemporalResult { - let rounded = utils::round_number_to_increment(self.0 as i64, increment, mode); + let rounded = utils::round_number_to_increment(self.0, increment as f64, mode); if rounded.abs() > MAX_TIME_DURATION as i64 { return Err(TemporalError::range() .with_message("normalizedTimeDuration exceeds maxTimeDuration.")); @@ -93,6 +93,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)); impl NormalizedDurationRecord { diff --git a/src/components/duration/tests.rs b/src/components/duration/tests.rs new file mode 100644 index 000000000..f9d084a8c --- /dev/null +++ b/src/components/duration/tests.rs @@ -0,0 +1,321 @@ +use crate::{ + components::{calendar::CalendarSlot, Date}, + options::ArithmeticOverflow, +}; + +use super::*; + +// roundingmode-floor.js +#[test] +fn basic_floor_rounding() { + const UNITS: [&str; 10] = [ + "years", + "months", + "weeks", + "days", + "hours", + "minutes", + "seconds", + "milliseconds", + "microseconds", + "nanoseconds", + ]; + const EXPECTED_POSITIVE: [[i32; 10]; 10] = [ + [5, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [5, 7, 0, 0, 0, 0, 0, 0, 0, 0], + [5, 6, 8, 0, 0, 0, 0, 0, 0, 0], + [5, 7, 0, 27, 0, 0, 0, 0, 0, 0], + [5, 7, 0, 27, 16, 0, 0, 0, 0, 0], + [5, 7, 0, 27, 16, 30, 0, 0, 0, 0], + [5, 7, 0, 27, 16, 30, 20, 0, 0, 0], + [5, 7, 0, 27, 16, 30, 20, 123, 0, 0], + [5, 7, 0, 27, 16, 30, 20, 123, 987, 0], + [5, 7, 0, 27, 16, 30, 20, 123, 987, 500], + ]; + const EXPECTED_NEG: [[i32; 10]; 10] = [ + [-6, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [-5, -8, 0, 0, 0, 0, 0, 0, 0, 0], + [-5, -6, -9, 0, 0, 0, 0, 0, 0, 0], + [-5, -7, 0, -28, 0, 0, 0, 0, 0, 0], + [-5, -7, 0, -27, -17, 0, 0, 0, 0, 0], + [-5, -7, 0, -27, -16, -31, 0, 0, 0, 0], + [-5, -7, 0, -27, -16, -30, -21, 0, 0, 0], + [-5, -7, 0, -27, -16, -30, -20, -124, 0, 0], + [-5, -7, 0, -27, -16, -30, -20, -123, -988, 0], + [-5, -7, 0, -27, -16, -30, -20, -123, -987, -500], + ]; + + let test_duration = + Duration::new(5.0, 6.0, 7.0, 8.0, 40.0, 30.0, 20.0, 123.0, 987.0, 500.0).unwrap(); + let mode = TemporalRoundingMode::Floor; + let forward_date = Date::<()>::new( + 2020, + 4, + 1, + CalendarSlot::from_str("iso8601").unwrap(), + ArithmeticOverflow::Reject, + ) + .unwrap(); + let backward_date = Date::<()>::new( + 2020, + 12, + 1, + CalendarSlot::from_str("iso8601").unwrap(), + ArithmeticOverflow::Reject, + ) + .unwrap(); + + let relative_forward: RelativeTo<'_, (), ()> = RelativeTo { + date: Some(&forward_date), + zdt: None, + }; + let relative_backward: RelativeTo<'_, (), ()> = RelativeTo { + date: Some(&backward_date), + zdt: None, + }; + + for i in 0..10 { + let unit = TemporalUnit::from_str(UNITS[i]).unwrap(); + let result = test_duration + .round( + None, + Some(unit), + None, + Some(mode), + &relative_forward, + &mut (), + ) + .unwrap(); + assert!(result + .fields() + .iter() + .zip(&EXPECTED_POSITIVE[i]) + .all(|r| *r.0 as i32 == *r.1)); + let neg_result = test_duration + .negated() + .round( + None, + Some(unit), + None, + Some(mode), + &relative_backward, + &mut (), + ) + .unwrap(); + assert!(neg_result + .fields() + .iter() + .zip(&EXPECTED_NEG[i]) + .all(|r| *r.0 as i32 == *r.1)); + } +} + +// roundingmode-ceil.js +#[test] +fn basic_ceil_rounding() { + const UNITS: [&str; 10] = [ + "years", + "months", + "weeks", + "days", + "hours", + "minutes", + "seconds", + "milliseconds", + "microseconds", + "nanoseconds", + ]; + const EXPECTED_POSITIVE: [[i32; 10]; 10] = [ + [6, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [5, 8, 0, 0, 0, 0, 0, 0, 0, 0], + [5, 6, 9, 0, 0, 0, 0, 0, 0, 0], + [5, 7, 0, 28, 0, 0, 0, 0, 0, 0], + [5, 7, 0, 27, 17, 0, 0, 0, 0, 0], + [5, 7, 0, 27, 16, 31, 0, 0, 0, 0], + [5, 7, 0, 27, 16, 30, 21, 0, 0, 0], + [5, 7, 0, 27, 16, 30, 20, 124, 0, 0], + [5, 7, 0, 27, 16, 30, 20, 123, 988, 0], + [5, 7, 0, 27, 16, 30, 20, 123, 987, 500], + ]; + const EXPECTED_NEG: [[i32; 10]; 10] = [ + [-5, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [-5, -7, 0, 0, 0, 0, 0, 0, 0, 0], + [-5, -6, -8, 0, 0, 0, 0, 0, 0, 0], + [-5, -7, 0, -27, 0, 0, 0, 0, 0, 0], + [-5, -7, 0, -27, -16, 0, 0, 0, 0, 0], + [-5, -7, 0, -27, -16, -30, 0, 0, 0, 0], + [-5, -7, 0, -27, -16, -30, -20, 0, 0, 0], + [-5, -7, 0, -27, -16, -30, -20, -123, 0, 0], + [-5, -7, 0, -27, -16, -30, -20, -123, -987, 0], + [-5, -7, 0, -27, -16, -30, -20, -123, -987, -500], + ]; + + let test_duration = + Duration::new(5.0, 6.0, 7.0, 8.0, 40.0, 30.0, 20.0, 123.0, 987.0, 500.0).unwrap(); + let mode = TemporalRoundingMode::Ceil; + let forward_date = Date::<()>::new( + 2020, + 4, + 1, + CalendarSlot::from_str("iso8601").unwrap(), + ArithmeticOverflow::Reject, + ) + .unwrap(); + let backward_date = Date::<()>::new( + 2020, + 12, + 1, + CalendarSlot::from_str("iso8601").unwrap(), + ArithmeticOverflow::Reject, + ) + .unwrap(); + + let relative_forward: RelativeTo<'_, (), ()> = RelativeTo { + date: Some(&forward_date), + zdt: None, + }; + let relative_backward: RelativeTo<'_, (), ()> = RelativeTo { + date: Some(&backward_date), + zdt: None, + }; + + for i in 0..10 { + let unit = TemporalUnit::from_str(UNITS[i]).unwrap(); + let result = test_duration + .round( + None, + Some(unit), + None, + Some(mode), + &relative_forward, + &mut (), + ) + .unwrap(); + assert!(result + .fields() + .iter() + .zip(&EXPECTED_POSITIVE[i]) + .all(|r| *r.0 as i32 == *r.1)); + let neg_result = test_duration + .negated() + .round( + None, + Some(unit), + None, + Some(mode), + &relative_backward, + &mut (), + ) + .unwrap(); + assert!(neg_result + .fields() + .iter() + .zip(&EXPECTED_NEG[i]) + .all(|r| *r.0 as i32 == *r.1)); + } +} + +// roundingmode-expand.js +#[test] +fn basic_expand_rounding() { + const UNITS: [&str; 10] = [ + "years", + "months", + "weeks", + "days", + "hours", + "minutes", + "seconds", + "milliseconds", + "microseconds", + "nanoseconds", + ]; + const EXPECTED_POSITIVE: [[i32; 10]; 10] = [ + [6, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [5, 8, 0, 0, 0, 0, 0, 0, 0, 0], + [5, 6, 9, 0, 0, 0, 0, 0, 0, 0], + [5, 7, 0, 28, 0, 0, 0, 0, 0, 0], + [5, 7, 0, 27, 17, 0, 0, 0, 0, 0], + [5, 7, 0, 27, 16, 31, 0, 0, 0, 0], + [5, 7, 0, 27, 16, 30, 21, 0, 0, 0], + [5, 7, 0, 27, 16, 30, 20, 124, 0, 0], + [5, 7, 0, 27, 16, 30, 20, 123, 988, 0], + [5, 7, 0, 27, 16, 30, 20, 123, 987, 500], + ]; + const EXPECTED_NEG: [[i32; 10]; 10] = [ + [-6, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [-5, -8, 0, 0, 0, 0, 0, 0, 0, 0], + [-5, -6, -9, 0, 0, 0, 0, 0, 0, 0], + [-5, -7, 0, -28, 0, 0, 0, 0, 0, 0], + [-5, -7, 0, -27, -17, 0, 0, 0, 0, 0], + [-5, -7, 0, -27, -16, -31, 0, 0, 0, 0], + [-5, -7, 0, -27, -16, -30, -21, 0, 0, 0], + [-5, -7, 0, -27, -16, -30, -20, -124, 0, 0], + [-5, -7, 0, -27, -16, -30, -20, -123, -988, 0], + [-5, -7, 0, -27, -16, -30, -20, -123, -987, -500], + ]; + + let test_duration = + Duration::new(5.0, 6.0, 7.0, 8.0, 40.0, 30.0, 20.0, 123.0, 987.0, 500.0).unwrap(); + let mode = TemporalRoundingMode::Expand; + let forward_date = Date::<()>::new( + 2020, + 4, + 1, + CalendarSlot::from_str("iso8601").unwrap(), + ArithmeticOverflow::Reject, + ) + .unwrap(); + let backward_date = Date::<()>::new( + 2020, + 12, + 1, + CalendarSlot::from_str("iso8601").unwrap(), + ArithmeticOverflow::Reject, + ) + .unwrap(); + + let relative_forward: RelativeTo<'_, (), ()> = RelativeTo { + date: Some(&forward_date), + zdt: None, + }; + let relative_backward: RelativeTo<'_, (), ()> = RelativeTo { + date: Some(&backward_date), + zdt: None, + }; + + for i in 0..10 { + let unit = TemporalUnit::from_str(UNITS[i]).unwrap(); + let result = test_duration + .round( + None, + Some(unit), + None, + Some(mode), + &relative_forward, + &mut (), + ) + .unwrap(); + assert!(result + .fields() + .iter() + .zip(&EXPECTED_POSITIVE[i]) + .all(|r| *r.0 as i32 == *r.1)); + let neg_result = test_duration + .negated() + .round( + None, + Some(unit), + None, + Some(mode), + &relative_backward, + &mut (), + ) + .unwrap(); + assert!(neg_result + .fields() + .iter() + .zip(&EXPECTED_NEG[i]) + .all(|r| *r.0 as i32 == *r.1)); + } +} diff --git a/src/components/duration/time.rs b/src/components/duration/time.rs index c267090c1..22fc494f2 100644 --- a/src/components/duration/time.rs +++ b/src/components/duration/time.rs @@ -20,12 +20,18 @@ const NANOSECONDS_PER_HOUR: u64 = NANOSECONDS_PER_MINUTE * 60; #[non_exhaustive] #[derive(Debug, Default, Clone, Copy)] pub struct TimeDuration { - pub(crate) hours: f64, - pub(crate) minutes: f64, - pub(crate) seconds: f64, - pub(crate) milliseconds: f64, - pub(crate) microseconds: f64, - pub(crate) nanoseconds: f64, + /// `TimeDuration`'s internal hour value. + pub hours: f64, + /// `TimeDuration`'s internal minute value. + pub minutes: f64, + /// `TimeDuration`'s internal second value. + pub seconds: f64, + /// `TimeDuration`'s internal millisecond value. + pub milliseconds: f64, + /// `TimeDuration`'s internal microsecond value. + pub microseconds: f64, + /// `TimeDuration`'s internal nanosecond value. + pub nanoseconds: f64, } // ==== TimeDuration Private API ==== @@ -49,85 +55,6 @@ impl TimeDuration { nanoseconds, } } -} - -// ==== 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 - }, - } - } /// Balances and creates `TimeDuration` from a `NormalizedTimeDuration`. This method will return /// a tuple (f64, TimeDuration) where f64 is the overflow day value from balancing. @@ -159,32 +86,32 @@ impl TimeDuration { // a. Set microseconds to floor(nanoseconds / 1000). microseconds = (nanoseconds / 1000f64).floor(); // b. Set nanoseconds to nanoseconds modulo 1000. - nanoseconds %= 1000f64; + nanoseconds = nanoseconds.rem_euclid(1000.0); // c. Set milliseconds to floor(microseconds / 1000). milliseconds = (microseconds / 1000f64).floor(); // d. Set microseconds to microseconds modulo 1000. - microseconds %= 1000f64; + microseconds = microseconds.rem_euclid(1000.0); // e. Set seconds to floor(milliseconds / 1000). seconds = (milliseconds / 1000f64).floor(); // f. Set milliseconds to milliseconds modulo 1000. - milliseconds %= 1000f64; + milliseconds = milliseconds.rem_euclid(1000.0); // g. Set minutes to floor(seconds / 60). minutes = (seconds / 60f64).floor(); // h. Set seconds to seconds modulo 60. - seconds %= 60f64; + seconds = seconds.rem_euclid(60.0); // i. Set hours to floor(minutes / 60). hours = (minutes / 60f64).floor(); // j. Set minutes to minutes modulo 60. - minutes %= 60f64; + minutes = minutes.rem_euclid(60.0); // k. Set days to floor(hours / 24). days = (hours / 24f64).floor(); // l. Set hours to hours modulo 24. - hours %= 24f64; + hours = hours.rem_euclid(24.0); } // 5. Else if largestUnit is "hour", then TemporalUnit::Hour => { @@ -310,6 +237,104 @@ impl TimeDuration { Ok((days, result)) } + /// Returns this `TimeDuration` as a `NormalizedTimeDuration`. + #[inline] + pub(crate) fn to_normalized(self) -> NormalizedTimeDuration { + NormalizedTimeDuration::from_time_duration(&self) + } + + /// Returns the value of `TimeDuration`'s fields. + #[inline] + #[must_use] + pub(crate) fn fields(&self) -> Vec { + Vec::from(&[ + self.hours, + self.minutes, + self.seconds, + self.milliseconds, + self.microseconds, + self.nanoseconds, + ]) + } +} + +// ==== 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.fields()) { + 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] @@ -349,53 +374,6 @@ impl TimeDuration { && self.milliseconds.abs() < 1000f64 && self.milliseconds.abs() < 1000f64 } - - /// Returns the `[[hours]]` value. - #[must_use] - pub const fn hours(&self) -> f64 { - self.hours - } - - /// Returns the `[[minutes]]` value. - #[must_use] - pub const fn minutes(&self) -> f64 { - self.minutes - } - - /// Returns the `[[seconds]]` value. - #[must_use] - pub const fn seconds(&self) -> f64 { - self.seconds - } - - /// Returns the `[[milliseconds]]` value. - #[must_use] - pub const fn milliseconds(&self) -> f64 { - self.milliseconds - } - - /// Returns the `[[microseconds]]` value. - #[must_use] - pub const fn microseconds(&self) -> f64 { - self.microseconds - } - - /// Returns the `[[nanoseconds]]` value. - #[must_use] - pub const fn nanoseconds(&self) -> f64 { - self.nanoseconds - } - - /// Returns the `TimeDuration`'s iterator. - #[must_use] - 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 ==== @@ -431,43 +409,43 @@ impl TimeDuration { let total = norm.divide(NANOSECONDS_PER_HOUR as i64); // c. Set norm to ? RoundNormalizedTimeDurationToIncrement(norm, divisor × increment, roundingMode). let norm = norm.round(NANOSECONDS_PER_HOUR * increment, mode)?; - Ok((norm, total)) + Ok((norm, total as i64)) } // 13. Else if unit is "minute", then TemporalUnit::Minute => { // a. Let divisor be 6 × 10**10. // b. Set total to DivideNormalizedTimeDuration(norm, divisor). - let total = norm.divide(NANOSECONDS_PER_HOUR as i64); + let total = norm.divide(NANOSECONDS_PER_MINUTE as i64); // c. Set norm to ? RoundNormalizedTimeDurationToIncrement(norm, divisor × increment, roundingMode). - let norm = norm.round(NANOSECONDS_PER_HOUR * increment, mode)?; - Ok((norm, total)) + let norm = norm.round(NANOSECONDS_PER_MINUTE * increment, mode)?; + Ok((norm, total as i64)) } // 14. Else if unit is "second", then TemporalUnit::Second => { // a. Let divisor be 10**9. // b. Set total to DivideNormalizedTimeDuration(norm, divisor). - let total = norm.divide(NANOSECONDS_PER_HOUR as i64); + let total = norm.divide(NANOSECONDS_PER_SECOND as i64); // c. Set norm to ? RoundNormalizedTimeDurationToIncrement(norm, divisor × increment, roundingMode). - let norm = norm.round(NANOSECONDS_PER_HOUR * increment, mode)?; - Ok((norm, total)) + let norm = norm.round(NANOSECONDS_PER_SECOND * increment, mode)?; + Ok((norm, total as i64)) } // 15. Else if unit is "millisecond", then TemporalUnit::Millisecond => { // a. Let divisor be 10**6. // b. Set total to DivideNormalizedTimeDuration(norm, divisor). - let total = norm.divide(NANOSECONDS_PER_HOUR as i64); + let total = norm.divide(1_000_000); // c. Set norm to ? RoundNormalizedTimeDurationToIncrement(norm, divisor × increment, roundingMode). - let norm = norm.round(NANOSECONDS_PER_HOUR * increment, mode)?; - Ok((norm, total)) + let norm = norm.round(1_000_000 * increment, mode)?; + Ok((norm, total as i64)) } // 16. Else if unit is "microsecond", then TemporalUnit::Microsecond => { // a. Let divisor be 10**3. // b. Set total to DivideNormalizedTimeDuration(norm, divisor). - let total = norm.divide(NANOSECONDS_PER_HOUR as i64); + let total = norm.divide(1_000); // c. Set norm to ? RoundNormalizedTimeDurationToIncrement(norm, divisor × increment, roundingMode). - let norm = norm.round(NANOSECONDS_PER_HOUR * increment, mode)?; - Ok((norm, total)) + let norm = norm.round(1_000 * increment, mode)?; + Ok((norm, total as i64)) } // 17. Else, TemporalUnit::Nanosecond => { @@ -483,40 +461,3 @@ impl TimeDuration { } } } - -impl<'a> IntoIterator for &'a TimeDuration { - type Item = f64; - type IntoIter = TimeIter<'a>; - - fn into_iter(self) -> Self::IntoIter { - TimeIter { - time: self, - index: 0, - } - } -} - -/// An iterator over a `TimeDuration`. -#[derive(Debug, Clone)] -pub struct TimeIter<'a> { - time: &'a TimeDuration, - index: usize, -} - -impl Iterator for TimeIter<'_> { - type Item = f64; - - fn next(&mut self) -> Option { - let result = match self.index { - 0 => Some(self.time.hours), - 1 => Some(self.time.minutes), - 2 => Some(self.time.seconds), - 3 => Some(self.time.milliseconds), - 4 => Some(self.time.microseconds), - 5 => Some(self.time.nanoseconds), - _ => None, - }; - self.index += 1; - result - } -} diff --git a/src/components/instant.rs b/src/components/instant.rs index 097f63e1c..600496ecc 100644 --- a/src/components/instant.rs +++ b/src/components/instant.rs @@ -116,8 +116,8 @@ impl Instant { }; let rounded = utils::round_number_to_increment_as_if_positive( - self.to_f64() as u64, // TODO: Update in numeric refactor. - increment, + self.to_f64(), // TODO: Update in numeric refactor. + increment as f64, rounding_mode, ); @@ -243,7 +243,7 @@ impl Instant { _ => return Err(TemporalError::range().with_message("Invalid roundTo unit provided.")), }; // NOTE: to_rounding_increment returns an f64 within a u32 range. - utils::validate_temporal_rounding_increment(increment as u32, maximum, true)?; + utils::validate_temporal_rounding_increment(increment, maximum, true)?; let round_result = self.round_instant(increment, unit, mode)?; Self::new(round_result) diff --git a/src/components/time.rs b/src/components/time.rs index 142abd67e..c19e21981 100644 --- a/src/components/time.rs +++ b/src/components/time.rs @@ -34,12 +34,12 @@ impl Time { /// Spec Equivalent: `AddDurationToOrSubtractDurationFromPlainTime` AND `AddTime`. pub(crate) fn add_to_time(&self, duration: &TimeDuration) -> Self { let (_, result) = IsoTime::balance( - f64::from(self.hour()) + duration.hours(), - f64::from(self.minute()) + duration.minutes(), - f64::from(self.second()) + duration.seconds(), - f64::from(self.millisecond()) + duration.milliseconds(), - f64::from(self.microsecond()) + duration.microseconds(), - f64::from(self.nanosecond()) + duration.nanoseconds(), + f64::from(self.hour()) + duration.hours, + f64::from(self.minute()) + duration.minutes, + f64::from(self.second()) + duration.seconds, + f64::from(self.millisecond()) + duration.milliseconds, + f64::from(self.microsecond()) + duration.microseconds, + f64::from(self.nanosecond()) + duration.nanoseconds, ); // NOTE (nekevss): IsoTime::balance should never return an invalid `IsoTime` @@ -165,7 +165,7 @@ impl Time { })?; // Safety (nekevss): to_rounding_increment returns a value in the range of a u32. - utils::validate_temporal_rounding_increment(increment as u32, u64::from(max), false)?; + utils::validate_temporal_rounding_increment(increment, u64::from(max), false)?; let (_, result) = self.iso.round(increment, smallest_unit, mode, None)?; diff --git a/src/iso.rs b/src/iso.rs index f51bfd8e9..a8c62851c 100644 --- a/src/iso.rs +++ b/src/iso.rs @@ -143,7 +143,7 @@ pub trait IsoDateSlots { /// These fields are used for the `Temporal.PlainDate` object, the /// `Temporal.YearMonth` object, and the `Temporal.MonthDay` object. #[non_exhaustive] -#[derive(Debug, Clone, Copy, Default)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)] pub struct IsoDate { pub(crate) year: i32, pub(crate) month: u8, @@ -197,7 +197,7 @@ impl IsoDate { /// /// Equivalent to `IsoDateToEpochDays` pub(crate) fn to_epoch_days(self) -> i32 { - iso_date_to_epoch_days(self.year, self.month.into(), self.day.into()) + iso_date_to_epoch_days(self.year, (self.month - 1).into(), self.day.into()) } /// Returns if the current `IsoDate` is valid. @@ -214,23 +214,22 @@ impl IsoDate { // 1. Assert: year, month, day, years, months, weeks, and days are integers. // 2. Assert: overflow is either "constrain" or "reject". // 3. Let intermediate be ! BalanceISOYearMonth(year + years, month + months). - let mut intermediate_year = self.year + duration.years() as i32; - let mut intermediate_month = i32::from(self.month) + duration.months() as i32; - - intermediate_year += (intermediate_month - 1) / 12; - intermediate_month = (intermediate_month - 1) % 12 + 1; + let intermediate = balance_iso_year_month( + self.year + duration.years as i32, + i32::from(self.month) + duration.months as i32, + ); // 4. Let intermediate be ? RegulateISODate(intermediate.[[Year]], intermediate.[[Month]], day, overflow). let intermediate = Self::new( - intermediate_year, - intermediate_month, + intermediate.0, + intermediate.1, i32::from(self.day), overflow, )?; // 5. Set days to days + 7 × weeks. + let additional_days = duration.days as i32 + (duration.weeks as i32 * 7); // 6. Let d be intermediate.[[Day]] + days. - let additional_days = duration.days() as i32 + (duration.weeks() as i32 * 7); let d = i32::from(intermediate.day) + additional_days; // 7. Return BalanceISODate(intermediate.[[Year]], intermediate.[[Month]], d). @@ -240,6 +239,94 @@ impl IsoDate { d, )) } + + pub(crate) fn diff_iso_date( + &self, + other: &Self, + largest_unit: TemporalUnit, + ) -> TemporalResult { + // 1. Assert: IsValidISODate(y1, m1, d1) is true. + // 2. Assert: IsValidISODate(y2, m2, d2) is true. + // 3. Let sign be -CompareISODate(y1, m1, d1, y2, m2, d2). + let sign = -(self.cmp(other) as i8); + // 4. If sign = 0, return ! CreateDateDurationRecord(0, 0, 0, 0). + if sign == 0 { + return Ok(DateDuration::default()); + }; + + // 5. Let years be 0. + let mut years = 0; + // 6. If largestUnit is "year", then + if largest_unit == TemporalUnit::Year || largest_unit == TemporalUnit::Month { + // a. Let candidateYears be sign. + let mut candidate_years: i32 = sign.into(); + // b. Repeat, while ISODateSurpasses(sign, y1 + candidateYears, m1, d1, y2, m2, d2) is false, + while !iso_date_surpasses( + &IsoDate::new_unchecked(self.year + candidate_years, self.month, self.day), + other, + sign, + ) { + // i. Set years to candidateYears. + years = candidate_years; + // ii. Set candidateYears to candidateYears + sign. + candidate_years += i32::from(sign); + } + } + + // 7. Let months be 0. + let mut months = 0; + // 8. If largestUnit is "year" or largestUnit is "month", then + if largest_unit == TemporalUnit::Year || largest_unit == TemporalUnit::Month { + // a. Let candidateMonths be sign. + let mut candidate_months: i32 = sign.into(); + // b. Let intermediate be BalanceISOYearMonth(y1 + years, m1 + candidateMonths). + let (mut intermediate_year, mut intermediate_month) = + balance_iso_year_month(self.year + years, i32::from(self.month) + candidate_months); + // c. Repeat, while ISODateSurpasses(sign, intermediate.[[Year]], intermediate.[[Month]], d1, y2, m2, d2) is false, + // Safety: balance_iso_year_month should always return a month value from 1..=12 + while !iso_date_surpasses( + &IsoDate::new_unchecked(intermediate_year, intermediate_month as u8, self.day), + other, + sign, + ) { + // i. Set months to candidateMonths. + months = candidate_months; + // ii. Set candidateMonths to candidateMonths + sign. + candidate_months += i32::from(sign); + // iii. Set intermediate to BalanceISOYearMonth(intermediate.[[Year]], intermediate.[[Month]] + sign). + (intermediate_year, intermediate_month) = + balance_iso_year_month(intermediate_year, intermediate_month + i32::from(sign)); + } + } + + // 9. Set intermediate to BalanceISOYearMonth(y1 + years, m1 + months). + let intermediate = + balance_iso_year_month(self.year + years, i32::from(self.month) + months); + // 10. Let constrained be ! RegulateISODate(intermediate.[[Year]], intermediate.[[Month]], d1, "constrain"). + let constrained = Self::new( + intermediate.0, + intermediate.1, + self.day.into(), + ArithmeticOverflow::Constrain, + )?; + + // NOTE: Below is adapted from the polyfill. Preferring this as it avoids looping. + // 11. Let weeks be 0. + let days = iso_date_to_epoch_days(other.year, i32::from(other.month) - 1, other.day.into()) + - iso_date_to_epoch_days( + constrained.year, + i32::from(constrained.month) - 1, + constrained.day.into(), + ); + + let (weeks, days) = if largest_unit == TemporalUnit::Week { + (days / 7, days.rem_euclid(7)) + } else { + (0, days) + }; + // 17. Return ! CreateDateDurationRecord(years, months, weeks, days). + DateDuration::new(years as f64, months as f64, weeks as f64, days as f64) + } } impl IsoDate { @@ -481,8 +568,8 @@ impl IsoTime { // TODO: Verify validity of cast or handle better. // 9. Let result be RoundNumberToIncrement(quantity, increment, roundingMode). let result = (utils::round_number_to_increment( - quantity as i64, - (ns_per_unit as u64) * increment, + quantity as f64, + ((ns_per_unit as u64) * increment) as f64, mode, ) / ns_per_unit) as f64; @@ -579,7 +666,9 @@ impl IsoTime { #[inline] /// Utility function to determine if a `DateTime`'s components create a `DateTime` within valid limits fn iso_dt_within_valid_limits(date: IsoDate, time: &IsoTime) -> bool { - if iso_date_to_epoch_days(date.year, (date.month).into(), date.day.into()).abs() > 100_000_001 { + if iso_date_to_epoch_days(date.year, (date.month - 1).into(), date.day.into()).abs() + > 100_000_001 + { return false; } let Some(ns) = utc_epoch_nanos(date, time, 0.0) else { @@ -612,11 +701,12 @@ fn utc_epoch_nanos(date: IsoDate, time: &IsoTime, offset: f64) -> Option #[inline] fn iso_date_to_epoch_days(year: i32, month: i32, day: i32) -> i32 { // 1. Let resolvedYear be year + floor(month / 12). - let resolved_year = year + (f64::from(month) / 12_f64).floor() as i32; + let resolved_year = year + (month / 12); // 2. Let resolvedMonth be month modulo 12. - let resolved_month = month % 12; + let resolved_month = month.rem_euclid(12); - // 3. Find a time t such that EpochTimeToEpochYear(t) is resolvedYear, EpochTimeToMonthInYear(t) is resolvedMonth, and EpochTimeToDate(t) is 1. + // 3. Find a time t such that EpochTimeToEpochYear(t) is resolvedYear, + // EpochTimeToMonthInYear(t) is resolvedMonth, and EpochTimeToDate(t) is 1. let year_t = utils::epoch_time_for_year(resolved_year); let month_t = utils::epoch_time_for_month_given_year(resolved_month, resolved_year); @@ -635,6 +725,19 @@ fn is_valid_date(year: i32, month: i32, day: i32) -> bool { (1..=days_in_month).contains(&day) } +#[inline] +/// Returns with the `this` surpasses `other`. +fn iso_date_surpasses(this: &IsoDate, other: &IsoDate, sign: i8) -> bool { + this.cmp(other) as i8 * sign == 1 +} + +#[inline] +fn balance_iso_year_month(year: i32, month: i32) -> (i32, i32) { + let y = year + (month - 1) / 12; + let m = (month - 1).rem_euclid(12) + 1; + (y, m) +} + // ==== `IsoTime` specific utilities ==== #[inline] diff --git a/src/options.rs b/src/options.rs index 75192802a..b0148cf5e 100644 --- a/src/options.rs +++ b/src/options.rs @@ -5,7 +5,17 @@ use core::{fmt, str::FromStr}; -use crate::TemporalError; +use crate::{ + components::{calendar::CalendarProtocol, tz::TzProtocol, Date, ZonedDateTime}, + TemporalError, +}; + +// ==== RelativeTo Object ==== + +pub struct RelativeTo<'a, C: CalendarProtocol, Z: TzProtocol> { + pub date: Option<&'a Date>, + pub zdt: Option<&'a ZonedDateTime>, +} // ==== Options enums and methods ==== @@ -81,6 +91,30 @@ impl TemporalUnit { Nanosecond => Some(1f64), } } + + #[must_use] + pub fn is_calendar_unit(&self) -> bool { + use TemporalUnit::{Month, Week, Year}; + matches!(self, Year | Month | Week) + } +} + +impl From for TemporalUnit { + fn from(value: usize) -> Self { + match value { + 10 => Self::Year, + 9 => Self::Month, + 8 => Self::Week, + 7 => Self::Day, + 6 => Self::Hour, + 5 => Self::Minute, + 4 => Self::Second, + 3 => Self::Millisecond, + 2 => Self::Microsecond, + 1 => Self::Nanosecond, + _ => Self::Auto, + } + } } /// A parsing error for `TemporalUnit` @@ -118,7 +152,7 @@ impl fmt::Display for TemporalUnit { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Auto => "auto", - Self::Year => "constrain", + Self::Year => "year", Self::Month => "month", Self::Week => "week", Self::Day => "day", diff --git a/src/parser/tests.rs b/src/parser/tests.rs index 077dc74d4..63c1aa22b 100644 --- a/src/parser/tests.rs +++ b/src/parser/tests.rs @@ -162,15 +162,15 @@ fn temporal_duration_parsing() { let sub_second = durations[2].parse::().unwrap(); - assert_eq!(sub_second.time().milliseconds(), -123.0); - assert_eq!(sub_second.time().microseconds(), -456.0); - assert_eq!(sub_second.time().nanoseconds(), -789.0); + assert_eq!(sub_second.milliseconds(), -123.0); + assert_eq!(sub_second.microseconds(), -456.0); + assert_eq!(sub_second.nanoseconds(), -789.0); let test_result = durations[3].parse::().unwrap(); - assert_eq!(test_result.date().years(), -1f64); - assert_eq!(test_result.date().weeks(), -3f64); - assert_eq!(test_result.time().minutes(), -30.0); + assert_eq!(test_result.years(), -1f64); + assert_eq!(test_result.weeks(), -3f64); + assert_eq!(test_result.minutes(), -30.0); } #[test] diff --git a/src/utils.rs b/src/utils.rs index 6d194a830..3ff17ca3e 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -30,59 +30,58 @@ pub(crate) fn to_rounding_increment(increment: Option) -> TemporalResult u64 { // 1. If x is equal to r1, return r1. - if x % increment == 0 { - return ceil; - }; - + if quotient == floor { + return floor as u64; + } // 2. Assert: r1 < x < r2. // 3. Assert: unsignedRoundingMode is not undefined. // 4. If unsignedRoundingMode is zero, return r1. if unsigned_rounding_mode == TemporalUnsignedRoundingMode::Zero { - return floor; + return floor as u64; }; // 5. If unsignedRoundingMode is infinity, return r2. if unsigned_rounding_mode == TemporalUnsignedRoundingMode::Infinity { - return ceil; + return ceil as u64; }; // 6. Let d1 be x – r1. // 7. Let d2 be r2 – x. + let d1 = quotient - floor; + let d2 = ceil - quotient; // 8. If d1 < d2, return r1. // 9. If d2 < d1, return r2. - let remainder = x % increment; - let half = increment / 2; - match remainder.cmp(&half) { - Ordering::Less => floor, - Ordering::Greater => ceil, - Ordering::Equal => { + match d1.partial_cmp(&d2) { + Some(Ordering::Less) => floor as u64, + Some(Ordering::Greater) => ceil as u64, + Some(Ordering::Equal) => { // 10. Assert: d1 is equal to d2. // 11. If unsignedRoundingMode is half-zero, return r1. if unsigned_rounding_mode == TemporalUnsignedRoundingMode::HalfZero { - return floor; + return floor as u64; }; // 12. If unsignedRoundingMode is half-infinity, return r2. if unsigned_rounding_mode == TemporalUnsignedRoundingMode::HalfInfinity { - return ceil; + return ceil as u64; }; // 13. Assert: unsignedRoundingMode is half-even. assert!(unsigned_rounding_mode == TemporalUnsignedRoundingMode::HalfEven); // 14. Let cardinality be (r1 / (r2 – r1)) modulo 2. - let cardinality = (floor / (ceil - floor)) % 2; + let cardinality = (floor / (ceil - floor)) % 2.0; // 15. If cardinality is 0, return r1. - if cardinality == 0 { - return floor; + if cardinality == 0.0 { + return floor as u64; } // 16. Return r2. - ceil + ceil as u64 } + None => unreachable!(), } } @@ -90,33 +89,33 @@ fn apply_unsigned_rounding_mode( // Tracking issue: https://github.com/rust-lang/rust/issues/88581 /// 13.28 `RoundNumberToIncrement ( x, increment, roundingMode )` pub(crate) fn round_number_to_increment( - x: i64, - increment: u64, + x: f64, + increment: f64, rounding_mode: TemporalRoundingMode, ) -> i64 { // 1. Let quotient be x / increment. + let quotient = x / increment; // 2. If quotient < 0, then - let is_negative = if x / (increment as i64) < 0 { + let (is_negative, quotient) = if quotient < 0.0 { // a. Let isNegative be true. // b. Set quotient to -quotient. - true + (true, quotient.abs()) // 3. Else, } else { // a. Let isNegative be false. - false + (false, quotient) }; // 4. Let unsignedRoundingMode be GetUnsignedRoundingMode(roundingMode, isNegative). let unsigned_rounding_mode = rounding_mode.get_unsigned_round_mode(is_negative); - let x = x.unsigned_abs(); // 5. Let r1 be the largest integer such that r1 ≤ quotient. - let floor = x / increment; + let floor = quotient.floor(); // 6. Let r2 be the smallest integer such that r2 > quotient. - let ceil = x.div_ceil(increment); + let ceil = quotient.ceil(); // 7. Let rounded be ApplyUnsignedRoundingMode(quotient, r1, r2, unsignedRoundingMode). - let rounded = apply_unsigned_rounding_mode(x, increment, floor, ceil, unsigned_rounding_mode); + let rounded = apply_unsigned_rounding_mode(quotient, floor, ceil, unsigned_rounding_mode); // 8. If isNegative is true, set rounded to -rounded. let rounded = if is_negative { @@ -131,35 +130,36 @@ pub(crate) fn round_number_to_increment( /// Rounds provided number assuming that the increment is greater than 0. pub(crate) fn round_number_to_increment_as_if_positive( - nanos: u64, - increment: u64, + x: f64, + increment: f64, rounding_mode: TemporalRoundingMode, ) -> u64 { // 1. Let quotient be x / increment. + let quotient = x / increment; // 2. Let unsignedRoundingMode be GetUnsignedRoundingMode(roundingMode, false). let unsigned_rounding_mode = rounding_mode.get_unsigned_round_mode(false); // 3. Let r1 be the largest integer such that r1 ≤ quotient. - let r1 = nanos / increment; + let r1 = quotient.floor(); // 4. Let r2 be the smallest integer such that r2 > quotient. - let r2 = nanos / increment + 1; + let r2 = quotient.ceil(); // 5. Let rounded be ApplyUnsignedRoundingMode(quotient, r1, r2, unsignedRoundingMode). - let rounded = apply_unsigned_rounding_mode(nanos, increment, r1, r2, unsigned_rounding_mode); + let rounded = apply_unsigned_rounding_mode(quotient, r1, r2, unsigned_rounding_mode); // 6. Return rounded × increment. - rounded * increment + rounded * (increment as u64) } pub(crate) fn validate_temporal_rounding_increment( - increment: u32, + increment: u64, dividend: u64, inclusive: bool, ) -> TemporalResult<()> { let max = if inclusive { dividend } else { dividend - 1 }; - if u64::from(increment) > max { + if increment > max { return Err(TemporalError::range().with_message("roundingIncrement exceeds maximum.")); } - if dividend.rem_euclid(u64::from(increment)) != 0 { + if dividend.rem_euclid(increment) != 0 { return Err( TemporalError::range().with_message("dividend is not divisble by roundingIncrement.") ); @@ -233,8 +233,8 @@ pub(crate) fn mathematical_in_leap_year(t: f64) -> i32 { } pub(crate) fn epoch_time_to_month_in_year(t: f64) -> u8 { - const DAYS: [i32; 11] = [30, 58, 89, 120, 150, 181, 212, 242, 272, 303, 333]; - const LEAP_DAYS: [i32; 11] = [30, 59, 90, 121, 151, 182, 213, 242, 272, 303, 334]; + const DAYS: [i32; 11] = [30, 58, 89, 119, 150, 180, 211, 242, 272, 303, 333]; + const LEAP_DAYS: [i32; 11] = [30, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]; let in_leap_year = mathematical_in_leap_year(t) == 1; let day = epoch_time_to_day_in_year(t); @@ -250,18 +250,20 @@ pub(crate) fn epoch_time_to_month_in_year(t: f64) -> u8 { } } +// Returns the time for a month in a given year plus date(t) = 1. pub(crate) fn epoch_time_for_month_given_year(m: i32, y: i32) -> f64 { let leap_day = mathematical_days_in_year(y) - 365; + // Includes day. i.e. end of month + 1 let days = match m { - 0 => 1, + 0 => 0, 1 => 31, 2 => 59 + leap_day, 3 => 90 + leap_day, - 4 => 121 + leap_day, + 4 => 120 + leap_day, 5 => 151 + leap_day, - 6 => 182 + leap_day, - 7 => 213 + leap_day, + 6 => 181 + leap_day, + 7 => 212 + leap_day, 8 => 243 + leap_day, 9 => 273 + leap_day, 10 => 304 + leap_day, @@ -345,4 +347,135 @@ mod tests { assert_eq!(epoch_time_to_month_in_year(mar_1_2021), 2); assert_eq!(mathematical_in_leap_year(mar_1_2021), 0); } + + #[test] + fn time_for_month_and_year() { + // NOTE: Month is 0-11 + + // Test standard year. + let standard_year_t = epoch_time_for_year(2015); + assert_eq!( + epoch_time_to_date(standard_year_t + epoch_time_for_month_given_year(0, 2015)), + 1, + "January is unaligned." + ); + assert_eq!( + epoch_time_to_date(standard_year_t + epoch_time_for_month_given_year(1, 2015)), + 1, + "February is unaligned." + ); + assert_eq!( + epoch_time_to_date(standard_year_t + epoch_time_for_month_given_year(2, 2015)), + 1, + "March is unaligned." + ); + assert_eq!( + epoch_time_to_date(standard_year_t + epoch_time_for_month_given_year(3, 2015)), + 1, + "April is unaligned." + ); + assert_eq!( + epoch_time_to_date(standard_year_t + epoch_time_for_month_given_year(4, 2015)), + 1, + "May is unaligned." + ); + assert_eq!( + epoch_time_to_date(standard_year_t + epoch_time_for_month_given_year(5, 2015)), + 1, + "June is unaligned." + ); + assert_eq!( + epoch_time_to_date(standard_year_t + epoch_time_for_month_given_year(6, 2015)), + 1, + "July is unaligned." + ); + assert_eq!( + epoch_time_to_date(standard_year_t + epoch_time_for_month_given_year(7, 2015)), + 1, + "August is unaligned." + ); + assert_eq!( + epoch_time_to_date(standard_year_t + epoch_time_for_month_given_year(8, 2015)), + 1, + "September is unaligned." + ); + assert_eq!( + epoch_time_to_date(standard_year_t + epoch_time_for_month_given_year(9, 2015)), + 1, + "October is unaligned." + ); + assert_eq!( + epoch_time_to_date(standard_year_t + epoch_time_for_month_given_year(10, 2015)), + 1, + "November is unaligned." + ); + assert_eq!( + epoch_time_to_date(standard_year_t + epoch_time_for_month_given_year(11, 2015)), + 1, + "December is unaligned." + ); + + // Test leap Year + let leap_year_t = epoch_time_for_year(2020); + assert_eq!( + epoch_time_to_date(leap_year_t + epoch_time_for_month_given_year(0, 2020)), + 1, + "January is unaligned." + ); + assert_eq!( + epoch_time_to_date(leap_year_t + epoch_time_for_month_given_year(1, 2020)), + 1, + "February is unaligned." + ); + assert_eq!( + epoch_time_to_date(leap_year_t + epoch_time_for_month_given_year(2, 2020)), + 1, + "March is unaligned." + ); + assert_eq!( + epoch_time_to_date(leap_year_t + epoch_time_for_month_given_year(3, 2020)), + 1, + "April is unaligned." + ); + assert_eq!( + epoch_time_to_date(leap_year_t + epoch_time_for_month_given_year(4, 2020)), + 1, + "May is unaligned." + ); + assert_eq!( + epoch_time_to_date(leap_year_t + epoch_time_for_month_given_year(5, 2020)), + 1, + "June is unaligned." + ); + assert_eq!( + epoch_time_to_date(leap_year_t + epoch_time_for_month_given_year(6, 2020)), + 1, + "July is unaligned." + ); + assert_eq!( + epoch_time_to_date(leap_year_t + epoch_time_for_month_given_year(7, 2020)), + 1, + "August is unaligned." + ); + assert_eq!( + epoch_time_to_date(leap_year_t + epoch_time_for_month_given_year(8, 2020)), + 1, + "September is unaligned." + ); + assert_eq!( + epoch_time_to_date(leap_year_t + epoch_time_for_month_given_year(9, 2020)), + 1, + "October is unaligned." + ); + assert_eq!( + epoch_time_to_date(leap_year_t + epoch_time_for_month_given_year(10, 2020)), + 1, + "November is unaligned." + ); + assert_eq!( + epoch_time_to_date(leap_year_t + epoch_time_for_month_given_year(11, 2020)), + 1, + "December is unaligned." + ); + } }