Skip to content

Commit 0e9d5c6

Browse files
committed
Fix bug with rounding with zoned RelativeTo and increment
1 parent 1acd2c1 commit 0e9d5c6

File tree

3 files changed

+49
-27
lines changed

3 files changed

+49
-27
lines changed

src/builtins/compiled/duration/tests.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -882,3 +882,25 @@ fn round_zero_duration() {
882882

883883
assert_eq!(rounded_duration, d0);
884884
}
885+
886+
#[test]
887+
fn round_increment_regression_test() {
888+
let duration = Duration::new(0, 0, 0, 0, 48, 0, 0, 0, 0, 0).unwrap();
889+
let relative_to = ZonedDateTime::try_new(0_i128, TimeZone::utc(), Calendar::ISO).unwrap();
890+
let options = RoundingOptions {
891+
smallest_unit: Some(Unit::Day),
892+
increment: Some(RoundingIncrement::new_unchecked(
893+
NonZeroU32::new(2).unwrap(),
894+
)),
895+
..Default::default()
896+
};
897+
898+
// let result = duration.round(options.clone(), None).unwrap();
899+
// assert_eq!(result.days(), 2);
900+
901+
// The result should be same with a UTC relativeTo
902+
let result = duration
903+
.round(options, Some(RelativeTo::ZonedDateTime(relative_to)))
904+
.unwrap();
905+
assert_eq!(result.days(), 2);
906+
}

src/builtins/core/duration/normalized.rs

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -893,27 +893,25 @@ impl InternalDurationRecord {
893893
//
894894
// progress = dividend / divisor
895895
//
896-
// 1. r1 + progress * increment * sign
897-
// 2. r1 + (dividend / divisor) * increment * sign
896+
// 1. total = r1 + progress * increment * sign
897+
// 2. total = r1 + (dividend / divisor) * increment * sign
898898
//
899899
// Bring in increment and sign
900900
//
901-
// 3. r1 + (dividend * increment * sign) / divisor
901+
// 3. total = r1 + (dividend * increment * sign) / divisor
902902
//
903903
// Now also move the r1 into the progress fraction.
904904
//
905-
// 4. ((r1 * divisor) + dividend * increment * sign) / divisor
905+
// 4. total = ((r1 * divisor) + dividend * increment * sign) / divisor
906906
//
907907
// 14. Let progress be (destEpochNs - startEpochNs) / (endEpochNs - startEpochNs).
908908
// 15. Let total be r1 + progress × increment × sign.
909909
let dividend = dest_epoch_ns - start_epoch_ns.0;
910910
let divisor = end_epoch_ns.0 - start_epoch_ns.0;
911911

912912
// We add r1 to the dividend
913-
let total_dividend = dividend
914-
+ (r1 * divisor)
915-
* options.increment.get() as i128
916-
* i128::from(sign.as_sign_multiplier());
913+
let total_times_divisor = (r1 * divisor)
914+
+ dividend * options.increment.get() as i128 * i128::from(sign.as_sign_multiplier());
917915

918916
// 16. NOTE: The above two steps cannot be implemented directly using floating-point arithmetic.
919917
// This division can be implemented as if constructing Normalized Time Duration Records for the denominator
@@ -937,8 +935,8 @@ impl InternalDurationRecord {
937935
// that there is no remainder caused by the calculation.
938936
//
939937
// 20. If progress = 1, then
940-
let total_is_r2 =
941-
total_dividend.div_euclid(divisor) == r2 && total_dividend.rem_euclid(divisor) == 0;
938+
let total_is_r2 = total_times_divisor.div_euclid(divisor) == r2
939+
&& total_times_divisor.rem_euclid(divisor) == 0;
942940
let rounded_unit = if total_is_r2 {
943941
// a. Let roundedUnit be abs(r2).
944942
r2.abs()
@@ -947,7 +945,7 @@ impl InternalDurationRecord {
947945
// b. Let roundedUnit be ApplyUnsignedRoundingMode(abs(total), abs(r1), abs(r2), unsignedRoundingMode).
948946
// TODO: what happens to r2 here?
949947
unsigned_rounding_mode.apply(
950-
total_dividend.unsigned_abs(),
948+
total_times_divisor.unsigned_abs(),
951949
divisor.unsigned_abs(),
952950
r1.abs(),
953951
r2.abs(),

src/options.rs

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -850,9 +850,18 @@ impl RoundingMode {
850850

851851
impl UnsignedRoundingMode {
852852
/// <https://tc39.es/proposal-temporal/#sec-applyunsignedroundingmode>
853+
///
854+
/// We represent `x` as `dividend / divisor` so that we can perform math in integer space.
853855
pub(crate) fn apply(self, dividend: u128, divisor: u128, r1: i128, r2: i128) -> i128 {
856+
// In order to perform the math in integer space, we multiply by `divisor`.
857+
//
858+
// I think the math stays in range: dividend and divisor are nanosecond
859+
// diffs, so they won't be larger than 2 * 8.64e21, which is 74 bits.
860+
//
861+
// r1 and r2 are calendar units, which aren't larger than 200,000,000.
862+
854863
// 1. If x = r1, return r1.
855-
if is_exact(dividend, divisor) {
864+
if dividend as i128 == r1 * divisor as i128 {
856865
return r1;
857866
}
858867
// 4. If unsignedRoundingMode is zero, return r1.
@@ -861,9 +870,16 @@ impl UnsignedRoundingMode {
861870
} else if self == UnsignedRoundingMode::Infinity {
862871
return r2;
863872
}
873+
864874
// 6. Let d1 be x – r1.
865875
// 7. Let d2 be r2 – x.
866-
match compare_remainder(dividend, divisor) {
876+
877+
let d1_times_divisor = dividend as i128 - r1 * divisor as i128;
878+
let d2_times_divisor = r2 * divisor as i128 - dividend as i128;
879+
// 8. If d1 < d2, return r1.
880+
// 9. If d2 < d1, return r2.
881+
// 10. Assert: d1 is equal to d2.
882+
match d1_times_divisor.cmp(&d2_times_divisor) {
867883
Ordering::Less => r1,
868884
Ordering::Greater => r2,
869885
Ordering::Equal => {
@@ -888,20 +904,6 @@ impl UnsignedRoundingMode {
888904
}
889905
}
890906

891-
fn is_exact(dividend: u128, divisor: u128) -> bool {
892-
dividend.rem_euclid(divisor) == 0
893-
}
894-
895-
fn compare_remainder(dividend: u128, divisor: u128) -> Ordering {
896-
let midway = divisor.div_euclid(2);
897-
let cmp = dividend.rem_euclid(divisor).cmp(&midway);
898-
if cmp == Ordering::Equal && divisor.rem_euclid(2) != 0 {
899-
Ordering::Less
900-
} else {
901-
cmp
902-
}
903-
}
904-
905907
impl FromStr for RoundingMode {
906908
type Err = TemporalError;
907909

0 commit comments

Comments
 (0)