diff --git a/src/builtins/core/duration/normalized.rs b/src/builtins/core/duration/normalized.rs index 78957de58..3264fe5ce 100644 --- a/src/builtins/core/duration/normalized.rs +++ b/src/builtins/core/duration/normalized.rs @@ -18,7 +18,8 @@ use crate::{ primitive::{DoubleDouble, FiniteF64}, provider::TimeZoneProvider, rounding::IncrementRounder, - Calendar, TemporalError, TemporalResult, TemporalUnwrap, NS_PER_DAY, NS_PER_DAY_NONZERO, + temporal_assert, Calendar, TemporalError, TemporalResult, TemporalUnwrap, NS_PER_DAY, + NS_PER_DAY_NONZERO, }; use super::{DateDuration, Duration, Sign}; @@ -390,15 +391,23 @@ impl InternalDurationRecord { #[derive(Debug)] struct NudgeRecord { normalized: InternalDurationRecord, - total: Option, nudge_epoch_ns: i128, expanded: bool, } +#[derive(Debug)] +struct NudgeWindow { + r1: i128, + r2: i128, + start_epoch_ns: EpochNanoseconds, + end_epoch_ns: EpochNanoseconds, + start_duration: DateDuration, + end_duration: DateDuration, +} + impl InternalDurationRecord { - // TODO: Add assertion into impl. - // TODO: Add unit tests specifically for nudge_calendar_unit if possible. - fn nudge_calendar_unit( + /// `compute_and_adjust_nudge_window` in `temporal_rs` refers to step 1-12 of `NudgeToCalendarUnit`. + fn compute_and_adjust_nudge_window( &self, sign: Sign, origin_epoch_ns: EpochNanoseconds, @@ -406,7 +415,68 @@ impl InternalDurationRecord { dt: &PlainDateTime, time_zone: Option<(&TimeZone, &(impl TimeZoneProvider + ?Sized))>, // ??? options: ResolvedRoundingOptions, - ) -> TemporalResult { + ) -> TemporalResult<(NudgeWindow, bool)> { + let dest_epoch_ns = EpochNanoseconds(dest_epoch_ns); + + // 1. Let didExpandCalendarUnit be false. + let mut did_expand_calendar_unit = false; + + // 2. Let nudgeWindow be ? ComputeNudgeWindow(sign, duration, originEpochNs, isoDateTime, timeZone, calendar, increment, unit, false). + let mut nudge_window = + self.compute_nudge_window(sign, origin_epoch_ns, dt, time_zone, options, false)?; + + // 3. Let startEpochNs be nudgeWindow.[[StartEpochNs]]. + // 4. Let endEpochNs be nudgeWindow.[[EndEpochNs]]. + // (implicitly used) + + // 5. If sign is 1, then + if sign != Sign::Negative { + // a. If startEpochNs ≤ destEpochNs ≤ endEpochNs is false, then + if !(nudge_window.start_epoch_ns <= dest_epoch_ns + && dest_epoch_ns <= nudge_window.end_epoch_ns) + { + // i. Set nudgeWindow to ? ComputeNudgeWindow(sign, duration, originEpochNs, isoDateTime, timeZone, calendar, increment, unit, true). + nudge_window = + self.compute_nudge_window(sign, origin_epoch_ns, dt, time_zone, options, true)?; + // ii. Assert: nudgeWindow.[[StartEpochNs]] ≤ destEpochNs ≤ nudgeWindow.[[EndEpochNs]]. + temporal_assert!( + nudge_window.start_epoch_ns <= dest_epoch_ns + && dest_epoch_ns <= nudge_window.end_epoch_ns + ); + // iii. Set didExpandCalendarUnit to true. + did_expand_calendar_unit = true; + } + } else { + // a. If endEpochNs ≤ destEpochNs ≤ startEpochNs is false, then + if !(nudge_window.end_epoch_ns <= dest_epoch_ns + && dest_epoch_ns <= nudge_window.start_epoch_ns) + { + // i. Set nudgeWindow to ? ComputeNudgeWindow(sign, duration, originEpochNs, isoDateTime, timeZone, calendar, increment, unit, true). + nudge_window = + self.compute_nudge_window(sign, origin_epoch_ns, dt, time_zone, options, true)?; + // ii. Assert: nudgeWindow.[[EndEpochNs]] ≤ destEpochNs ≤ nudgeWindow.[[StartEpochNs]]. + temporal_assert!( + nudge_window.end_epoch_ns <= dest_epoch_ns + && dest_epoch_ns <= nudge_window.start_epoch_ns + ); + // iii. Set didExpandCalendarUnit to true. + did_expand_calendar_unit = true; + } + } + + Ok((nudge_window, did_expand_calendar_unit)) + } + + /// + fn compute_nudge_window( + &self, + sign: Sign, + origin_epoch_ns: EpochNanoseconds, + dt: &PlainDateTime, + time_zone: Option<(&TimeZone, &(impl TimeZoneProvider + ?Sized))>, // ??? + options: ResolvedRoundingOptions, + additional_shift: bool, + ) -> TemporalResult { // NOTE: r2 may never be used...need to test. let (r1, r2, start_duration, end_duration) = match options.smallest_unit { // 1. If unit is "year", then @@ -417,11 +487,18 @@ impl InternalDurationRecord { options.increment.as_extended_increment(), )? .round(RoundingMode::Trunc); - // b. Let r1 be years. - let r1 = years; - // c. Let r2 be years + increment × sign. - let r2 = years - + i128::from(options.increment.get()) * i128::from(sign.as_sign_multiplier()); + let increment_x_sign = + i128::from(options.increment.get()) * i128::from(sign.as_sign_multiplier()); + // b. If additionalShift is false, then + let r1 = if !additional_shift { + // i. Let r1 be years. + years + } else { + // i. Let r1 be years + increment × sign. + years + increment_x_sign + }; + // c. Let r2 be r1 + increment × sign. + let r2 = r1 + increment_x_sign; // d. Let startDuration be ? CreateNormalizedDurationRecord(r1, 0, 0, 0, ZeroTimeDuration()). // e. Let endDuration be ? CreateNormalizedDurationRecord(r2, 0, 0, 0, ZeroTimeDuration()). ( @@ -449,27 +526,33 @@ impl InternalDurationRecord { options.increment.as_extended_increment(), )? .round(RoundingMode::Trunc); - // b. Let r1 be months. - let r1 = months; - // c. Let r2 be months + increment × sign. - let r2 = months - + i128::from(options.increment.get()) * i128::from(sign.as_sign_multiplier()); - // d. Let startDuration be ? CreateNormalizedDurationRecord(duration.[[Years]], r1, 0, 0, ZeroTimeDuration()). - // e. Let endDuration be ? CreateNormalizedDurationRecord(duration.[[Years]], r2, 0, 0, ZeroTimeDuration()). + let increment_x_sign = + i128::from(options.increment.get()) * i128::from(sign.as_sign_multiplier()); + // b. If additionalShift is false, then + let r1 = if !additional_shift { + // i. Let r1 be months. + months + // c. Else + } else { + // i. Let r1 be months + increment × sign. + months + increment_x_sign + }; + // d. Let r2 be r1 + increment × sign. + let r2 = r1 + increment_x_sign; + // e. Let startDuration be ? AdjustDateDurationRecord(duration.[[Date]], 0, 0, r1). + // f. Let endDuration be ? AdjustDateDurationRecord(duration.[[Date]], 0, 0, r2). ( r1, r2, - DateDuration::new( - self.date().years, - i64::try_from(r1).map_err(|_| TemporalError::range())?, - 0, + self.date().adjust( 0, + None, + Some(i64::try_from(r1).map_err(|_| TemporalError::range())?), )?, - DateDuration::new( - self.date().years, - i64::try_from(r2).map_err(|_| TemporalError::range())?, - 0, + self.date().adjust( 0, + None, + Some(i64::try_from(r2).map_err(|_| TemporalError::range())?), )?, ) } @@ -602,6 +685,14 @@ impl InternalDurationRecord { } }; + // 5. Assert: If sign is 1, r1 ≥ 0 and r1 < r2. + // 6. Assert: If sign is -1, r1 ≤ 0 and r1 > r2. + // n.b. sign == 1 means nonnegative + crate::temporal_assert!( + (sign != Sign::Negative && r1 >= 0 && r1 < r2) + || (sign == Sign::Negative && r1 <= 0 && r1 > r2) + ); + let start_epoch_ns = if r1 == 0 { origin_epoch_ns } else { @@ -646,60 +737,232 @@ impl InternalDurationRecord { end.as_nanoseconds() }; - // TODO: look into handling asserts - // 13. If sign is 1, then - // a. Assert: startEpochNs ≤ destEpochNs ≤ endEpochNs. - // 14. Else, - // a. Assert: endEpochNs ≤ destEpochNs ≤ startEpochNs. - // 15. Assert: startEpochNs ≠ endEpochNs. - - // TODO: Don't use f64 below ... - // NOTE(nekevss): Step 12..13 could be problematic...need tests - // and verify, or completely change the approach involved. - // TODO(nekevss): Validate that the `f64` casts here are valid in all scenarios - // 16. Let progress be (destEpochNs - startEpochNs) / (endEpochNs - startEpochNs). - // 17. Let total be r1 + progress × increment × sign. - let progress = - (dest_epoch_ns - start_epoch_ns.0) as f64 / (end_epoch_ns.0 - start_epoch_ns.0) as f64; - let total = r1 as f64 - + progress * options.increment.get() as f64 * f64::from(sign.as_sign_multiplier()); - - // 14. NOTE: The above two steps cannot be implemented directly using floating-point arithmetic. + Ok(NudgeWindow { + r1, + r2, + start_epoch_ns, + end_epoch_ns, + start_duration, + end_duration, + }) + } + + fn nudge_calendar_unit_total( + &self, + sign: Sign, + origin_epoch_ns: EpochNanoseconds, + dest_epoch_ns: i128, + dt: &PlainDateTime, + time_zone: Option<(&TimeZone, &(impl TimeZoneProvider + ?Sized))>, // ??? + options: ResolvedRoundingOptions, + ) -> TemporalResult { + let (nudge_window, _) = self.compute_and_adjust_nudge_window( + sign, + origin_epoch_ns, + dest_epoch_ns, + dt, + time_zone, + options, + )?; + // 7. Let r1 be nudgeWindow.[[R1]]. + // 8. Let r2 be nudgeWindow.[[R2]]. + // 9. Set startEpochNs to nudgeWindow.[[StartEpochNs]]. + // 10. Set endEpochNs to nudgeWindow.[[EndEpochNs]]. + // 11. Let startDuration be nudgeWindow.[[StartDuration]]. + // 12. Let endDuration be nudgeWindow.[[EndDuration]]. + + let NudgeWindow { + r1, + start_epoch_ns, + end_epoch_ns, + .. + } = nudge_window; + + // 13. Assert: startEpochNs ≠ endEpochNs. + temporal_assert!(start_epoch_ns != end_epoch_ns); + + // NOTE (nekevss): re: nudge_calendar_unit + // + // We change our calculations here to limit f64 usage, but also to preserve + // precision on the calculation. + // + // So let's go over what we do to handle this ... well, basically, + // just math. + // + // We take `r1 + progress * increment * sign` and plug in the progress calculation + // + // So, in other words, stepping through the calculations + // + // NOTE: For shorthand, + // + // dividend = (destEpochNS - startEpochNS) + // divisor = (endEpochNS - startEpochNS) + // + // progress = dividend / divisor + // + // 1. r1 + progress * increment * sign + // 2. r1 + (dividend / divisor) * increment * sign + // + // Bring in increment and sign + // + // 3. r1 + (dividend * increment * sign) / divisor + // + // Now also move the r1 into the progress fraction. + // + // 4. ((r1 * divisor) + dividend * increment * sign) / divisor + // + // 14. Let progress be (destEpochNs - startEpochNs) / (endEpochNs - startEpochNs). + // 15. Let total be r1 + progress × increment × sign. + let progress_numerator = dest_epoch_ns - start_epoch_ns.0; + let denominator = end_epoch_ns.0 - start_epoch_ns.0; + let total_numerator = (r1 * denominator) + + progress_numerator + * options.increment.get() as i128 + * i128::from(sign.as_sign_multiplier()); + + Ok(Fraction::new(total_numerator, denominator as f64).to_finite_f64()) + } + + // TODO: Add assertion into impl. + // TODO: Add unit tests specifically for nudge_calendar_unit if possible. + fn nudge_calendar_unit( + &self, + sign: Sign, + origin_epoch_ns: EpochNanoseconds, + dest_epoch_ns: i128, + dt: &PlainDateTime, + time_zone: Option<(&TimeZone, &(impl TimeZoneProvider + ?Sized))>, // ??? + options: ResolvedRoundingOptions, + ) -> TemporalResult { + let (nudge_window, did_expand_calendar_unit) = self.compute_and_adjust_nudge_window( + sign, + origin_epoch_ns, + dest_epoch_ns, + dt, + time_zone, + options, + )?; + // 7. Let r1 be nudgeWindow.[[R1]]. + // 8. Let r2 be nudgeWindow.[[R2]]. + // 9. Set startEpochNs to nudgeWindow.[[StartEpochNs]]. + // 10. Set endEpochNs to nudgeWindow.[[EndEpochNs]]. + // 11. Let startDuration be nudgeWindow.[[StartDuration]]. + // 12. Let endDuration be nudgeWindow.[[EndDuration]]. + + let NudgeWindow { + r1, + r2, + start_epoch_ns, + end_epoch_ns, + start_duration, + end_duration, + } = nudge_window; + + // 13. Assert: startEpochNs ≠ endEpochNs. + temporal_assert!(start_epoch_ns != end_epoch_ns); + + // NOTE (nekevss): + // + // We change our calculations to remove any f64 usage. + // + // So let's go over what we do to handle this ... well, basically, + // just math. + // + // We take `r1 + progress * increment * sign` and plug in the progress calculation + // + // So, in other words, stepping through the calculations + // + // NOTE: For shorthand, + // + // dividend = (destEpochNS - startEpochNS) + // divisor = (endEpochNS - startEpochNS) + // + // progress = dividend / divisor + // + // 1. r1 + progress * increment * sign + // 2. r1 + (dividend / divisor) * increment * sign + // + // Bring in increment and sign + // + // 3. r1 + (dividend * increment * sign) / divisor + // + // Now also move the r1 into the progress fraction. + // + // 4. ((r1 * divisor) + dividend * increment * sign) / divisor + // + // 14. Let progress be (destEpochNs - startEpochNs) / (endEpochNs - startEpochNs). + // 15. Let total be r1 + progress × increment × sign. + let dividend = dest_epoch_ns - start_epoch_ns.0; + let divisor = end_epoch_ns.0 - start_epoch_ns.0; + + // We add r1 to the dividend + let total_dividend = dividend + + (r1 * divisor) + * options.increment.get() as i128 + * i128::from(sign.as_sign_multiplier()); + + // 16. NOTE: The above two steps cannot be implemented directly using floating-point arithmetic. // This division can be implemented as if constructing Normalized Time Duration Records for the denominator // and numerator of total and performing one division operation with a floating-point result. - // 15. Let roundedUnit be ApplyUnsignedRoundingMode(total, r1, r2, unsignedRoundingMode). - let rounded_unit = - IncrementRounder::from_signed_num(total, options.increment.as_extended_increment())? - .round(options.rounding_mode); - - // 16. If roundedUnit - total < 0, let roundedSign be -1; else let roundedSign be 1. - // 19. Return Duration Nudge Result Record { [[Duration]]: resultDuration, [[Total]]: total, [[NudgedEpochNs]]: nudgedEpochNs, [[DidExpandCalendarUnit]]: didExpandCalendarUnit }. - // 17. If roundedSign = sign, then - if rounded_unit == r2 { + // 17. Assert: 0 ≤ progress ≤ 1. + // 18. If sign < 0, let isNegative be negative; else let isNegative be positive. + // (used implicitly) + + // 19. Let unsignedRoundingMode be GetUnsignedRoundingMode(roundingMode, isNegative). + // n.b. get_unsigned_round_mode takes is_positive, but it actually cares about nonnegative + let unsigned_rounding_mode = options + .rounding_mode + .get_unsigned_round_mode(sign != Sign::Negative); + + // NOTE (nekevss): + // + // Now we need to eliminate whether our "total" would be the value of r2 + // exactly, AKA `progress = 1`. + // + // To do this, we check if the quotient of dividend and divisor is r2 and + // that there is no remainder caused by the calculation. + // + // 20. If progress = 1, then + let total_is_r2 = + total_dividend.div_euclid(divisor) == r2 && total_dividend.rem_euclid(divisor) == 0; + let rounded_unit = if total_is_r2 { + // a. Let roundedUnit be abs(r2). + r2.abs() + } else { + // a. Assert: abs(r1) ≤ abs(total) < abs(r2). + // b. Let roundedUnit be ApplyUnsignedRoundingMode(abs(total), abs(r1), abs(r2), unsignedRoundingMode). + // TODO: what happens to r2 here? + unsigned_rounding_mode.apply( + total_dividend.unsigned_abs(), + divisor.unsigned_abs(), + r1.abs(), + r2.abs(), + ) + }; + + // 22. If roundedUnit is abs(r2), then + if rounded_unit == r2.abs() { // a. Let didExpandCalendarUnit be true. // b. Let resultDuration be endDuration. // c. Let nudgedEpochNs be endEpochNs. Ok(NudgeRecord { normalized: InternalDurationRecord::new(end_duration, TimeDuration::default())?, - total: Some(FiniteF64::try_from(total)?), nudge_epoch_ns: end_epoch_ns.0, expanded: true, }) // 18. Else, } else { - // a. Let didExpandCalendarUnit be false. // b. Let resultDuration be startDuration. // c. Let nudgedEpochNs be startEpochNs. Ok(NudgeRecord { normalized: InternalDurationRecord::new(start_duration, TimeDuration::default())?, - total: Some(FiniteF64::try_from(total)?), nudge_epoch_ns: start_epoch_ns.0, - expanded: false, + expanded: did_expand_calendar_unit, }) } } - // TODO: Clean up + // Round a duration to a time unit based on a relative `ZonedDateTime` #[inline] fn nudge_to_zoned_time( &self, @@ -786,7 +1049,6 @@ impl InternalDurationRecord { Ok(NudgeRecord { normalized, nudge_epoch_ns: nudge_ns.0, - total: None, expanded, }) } @@ -851,7 +1113,6 @@ impl InternalDurationRecord { // 16. Return Duration Nudge Result Record { [[Duration]]: resultDuration, [[NudgedEpochNs]]: nudgedEpochNs, [[DidExpandCalendarUnit]]: didExpandDays }. Ok(NudgeRecord { normalized: result_duration, - total: None, nudge_epoch_ns: nudged_ns, expanded: did_expand_days, }) @@ -1078,7 +1339,8 @@ impl InternalDurationRecord { // a. Let sign be InternalDurationSign(duration). let sign = self.sign(); // b. Let record be ? NudgeToCalendarUnit(sign, duration, destEpochNs, isoDateTime, timeZone, calendar, 1, unit, trunc). - let record = self.nudge_calendar_unit( + // c. Return record.[[Total]]. + return self.nudge_calendar_unit_total( sign, origin_epoch_ns, dest_epoch_ns, @@ -1090,10 +1352,7 @@ impl InternalDurationRecord { increment: RoundingIncrement::default(), rounding_mode: RoundingMode::Trunc, }, - )?; - - // c. Return record.[[Total]]. - return record.total.temporal_unwrap(); + ); } // 2. Let timeDuration be ! Add24HourDaysToTimeDuration(duration.[[Time]], duration.[[Date]].[[Days]]). let time_duration = self diff --git a/src/builtins/core/duration/tests.rs b/src/builtins/core/duration/tests.rs index c5931d984..51c88abb7 100644 --- a/src/builtins/core/duration/tests.rs +++ b/src/builtins/core/duration/tests.rs @@ -452,3 +452,129 @@ fn total_full_numeric_precision() { let d = Duration::new(0, 0, 0, 0, 0, 0, 0, MAX_SAFE_INTEGER + 1, 1999, 0).unwrap(); assert_eq!(d.total(Unit::Millisecond, None).unwrap(), 9007199254740994.); } + +/// Test for https://github.com/tc39/proposal-temporal/pull/3172/ +/// +/// test262: built-ins/Temporal/Duration/prototype/total/rounding-window +#[test] +#[cfg(feature = "compiled_data")] +fn test_nudge_relative_date_total() { + use crate::Calendar; + use crate::PlainDate; + let d = Duration::new(1, 0, 0, 0, 1, 0, 0, 0, 0, 0).unwrap(); + let relative = PlainDate::new(2020, 2, 29, Calendar::ISO).unwrap(); + assert_eq!( + d.total(Unit::Year, Some(relative.into())).unwrap(), + 1.0001141552511414 + ); + + let d = Duration::new(0, 1, 0, 0, 10, 0, 0, 0, 0, 0).unwrap(); + let relative = PlainDate::new(2020, 1, 31, Calendar::ISO).unwrap(); + assert_eq!( + d.total(Unit::Month, Some(relative.into())).unwrap(), + 1.0134408602150538 + ); +} + +// Adapted from roundingincrement-addition-out-of-range.js +#[test] +#[cfg(feature = "compiled_data")] +fn rounding_out_of_range() { + use crate::options::{DifferenceSettings, RoundingMode}; + use crate::{TimeZone, ZonedDateTime}; + let earlier = ZonedDateTime::try_new_iso(0, TimeZone::utc()).unwrap(); + let later = ZonedDateTime::try_new_iso(5, TimeZone::utc()).unwrap(); + + let options = DifferenceSettings { + smallest_unit: Some(Unit::Day), + increment: Some(RoundingIncrement::try_new(100_000_001).unwrap()), + ..Default::default() + }; + let error = later.since(&earlier, options); + assert!( + error.is_err(), + "Ending bound 100_000_001 is out of range and should fail." + ); + + let error = earlier.since(&later, options); + assert!( + error.is_err(), + "Ending bound -100_000_001 is out of range and should fail." + ); + + let options = DifferenceSettings { + smallest_unit: Some(Unit::Day), + increment: Some(RoundingIncrement::try_new(100_000_000).unwrap()), + rounding_mode: Some(RoundingMode::Expand), + ..Default::default() + }; + let duration = later.since(&earlier, options).unwrap(); + assert_eq!(duration.days(), 100_000_000); + + let duration = earlier.since(&later, options).unwrap(); + assert_eq!(duration.days(), -100_000_000); +} + +#[test] +#[cfg(feature = "compiled_data")] +fn total_precision() { + use crate::PlainDate; + + let d = Duration::new(0, 0, 5, 5, 0, 0, 0, 0, 0, 0).unwrap(); + + let relative_to = PlainDate::try_new_iso(1972, 1, 31).unwrap(); + let result = d.total(Unit::Month, Some(relative_to.into())).unwrap(); + + assert_eq!( + result.0, 1.3548387096774193, + "Loss of precision on Duration::total" + ); +} + +#[test] +#[cfg(feature = "compiled_data")] +fn rounding_window() { + use crate::PlainDate; + + fn duration(years: i64, months: i64, weeks: i64, days: i64, hours: i64) -> Duration { + Duration::new(years, months, weeks, days, hours, 0, 0, 0, 0, 0).unwrap() + } + + let d = duration(1, 0, 0, 0, 1); + let relative_to = PlainDate::try_new_iso(2020, 2, 29).unwrap(); + let options = RoundingOptions { + smallest_unit: Some(Unit::Year), + ..Default::default() + }; + let result = d.round(options, Some(relative_to.into())).unwrap(); + assert_eq!(result.years(), 1, "years must round down to 1"); + + let d = duration(0, 1, 0, 0, 10); + let relative_to = PlainDate::try_new_iso(2020, 1, 31).unwrap(); + let options = RoundingOptions { + smallest_unit: Some(Unit::Month), + rounding_mode: Some(crate::options::RoundingMode::Expand), + ..Default::default() + }; + let result = d.round(options, Some(relative_to.into())).unwrap(); + assert_eq!(result.months(), 2, "months rounding should expand to 2"); + + let d = duration(2345, 0, 0, 0, 12); + let relative_to = PlainDate::try_new_iso(2020, 2, 29).unwrap(); + let options = RoundingOptions { + smallest_unit: Some(Unit::Year), + rounding_mode: Some(crate::options::RoundingMode::Expand), + ..Default::default() + }; + let result = d.round(options, Some(relative_to.into())).unwrap(); + assert_eq!(result.years(), 2346, "years rounding should expand to 2346"); + + let d = duration(1, 0, 0, 0, 0); + let relative_to = PlainDate::try_new_iso(2020, 2, 29).unwrap(); + let options = RoundingOptions { + smallest_unit: Some(Unit::Month), + ..Default::default() + }; + let result = d.round(options, Some(relative_to.into())).unwrap(); + assert_eq!(result.years(), 1, "months rounding should no-op"); +} diff --git a/src/builtins/core/plain_date.rs b/src/builtins/core/plain_date.rs index bd7b04a16..9647b1a0d 100644 --- a/src/builtins/core/plain_date.rs +++ b/src/builtins/core/plain_date.rs @@ -707,6 +707,8 @@ impl FromStr for PlainDate { mod tests { use tinystr::tinystr; + use crate::options::{RoundingIncrement, RoundingMode}; + use super::*; #[test] @@ -1042,4 +1044,46 @@ mod tests { assert!(PlainDate::from_str(s).is_err()) } } + + #[test] + fn rounding_increment_observed() { + let earlier = PlainDate::try_new_iso(2019, 1, 8).unwrap(); + let later = PlainDate::try_new_iso(2021, 9, 7).unwrap(); + + let settings = DifferenceSettings { + smallest_unit: Some(Unit::Year), + rounding_mode: Some(RoundingMode::HalfExpand), + increment: Some(RoundingIncrement::try_new(4).unwrap()), + ..Default::default() + }; + let result = later.since(&earlier, settings).unwrap(); + assert_eq!(result.years(), 4); + + let settings = DifferenceSettings { + smallest_unit: Some(Unit::Month), + rounding_mode: Some(RoundingMode::HalfExpand), + increment: Some(RoundingIncrement::try_new(10).unwrap()), + ..Default::default() + }; + let result = later.since(&earlier, settings).unwrap(); + assert_eq!(result.months(), 30); + + let settings = DifferenceSettings { + smallest_unit: Some(Unit::Week), + rounding_mode: Some(RoundingMode::HalfExpand), + increment: Some(RoundingIncrement::try_new(12).unwrap()), + ..Default::default() + }; + let result = later.since(&earlier, settings).unwrap(); + assert_eq!(result.weeks(), 144); + + let settings = DifferenceSettings { + smallest_unit: Some(Unit::Day), + rounding_mode: Some(RoundingMode::HalfExpand), + increment: Some(RoundingIncrement::try_new(100).unwrap()), + ..Default::default() + }; + let result = later.since(&earlier, settings).unwrap(); + assert_eq!(result.days(), 1000); + } } diff --git a/src/options.rs b/src/options.rs index c7f6c025a..8461a8785 100644 --- a/src/options.rs +++ b/src/options.rs @@ -6,6 +6,7 @@ use crate::parsers::Precision; use crate::TemporalUnwrap; use crate::{error::ErrorMessage, TemporalError, TemporalResult, MS_PER_DAY, NS_PER_DAY}; +use core::cmp::Ordering; use core::num::NonZeroU128; use core::ops::Add; use core::{fmt, str::FromStr}; @@ -847,6 +848,60 @@ impl RoundingMode { } } +impl UnsignedRoundingMode { + /// + pub(crate) fn apply(self, dividend: u128, divisor: u128, r1: i128, r2: i128) -> i128 { + // 1. If x = r1, return r1. + if is_exact(dividend, divisor) { + return r1; + } + // 4. If unsignedRoundingMode is zero, return r1. + if self == UnsignedRoundingMode::Zero { + return r1; + } else if self == UnsignedRoundingMode::Infinity { + return r2; + } + // 6. Let d1 be x – r1. + // 7. Let d2 be r2 – x. + match compare_remainder(dividend, divisor) { + Ordering::Less => r1, + Ordering::Greater => r2, + Ordering::Equal => { + match self { + UnsignedRoundingMode::HalfZero => r1, + UnsignedRoundingMode::HalfInfinity => r2, + // HalfEven + _ => { + // 14. Let cardinality be (r1 / (r2 – r1)) modulo 2. + let diff = r2 - r1; + let cardinality = r1.div_euclid(diff).rem_euclid(2); + // 15. If cardinality = 0, return r1. + if cardinality == 0 { + r1 + } else { + r2 + } + } + } + } + } + } +} + +fn is_exact(dividend: u128, divisor: u128) -> bool { + dividend.rem_euclid(divisor) == 0 +} + +fn compare_remainder(dividend: u128, divisor: u128) -> Ordering { + let midway = divisor.div_euclid(2); + let cmp = dividend.rem_euclid(divisor).cmp(&midway); + if cmp == Ordering::Equal && divisor.rem_euclid(2) != 0 { + Ordering::Less + } else { + cmp + } +} + impl FromStr for RoundingMode { type Err = TemporalError;