diff --git a/CHANGELOG.md b/CHANGELOG.md index 381646c6cb8..7e79b9f292f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -476,6 +476,7 @@ - [Added `.round`, `.truncate`, `.ceil`, and `.floor` to `Column`.][6817] - [Added execution control to `Table.write` and various bug fixes.][6835] - [Implemented `Table.add_row_number`.][6890] +- [Handling edge cases in rounding.][6922] [debug-shortcuts]: https://github.com/enso-org/enso/blob/develop/app/gui/docs/product/shortcuts.md#debug @@ -691,6 +692,7 @@ [6817]: https://github.com/enso-org/enso/pull/6817 [6835]: https://github.com/enso-org/enso/pull/6835 [6890]: https://github.com/enso-org/enso/pull/6890 +[6922]: https://github.com/enso-org/enso/pull/6922 #### Enso Compiler diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Numbers.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Numbers.enso index 88917f73a33..2003eba9d3b 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Numbers.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Numbers.enso @@ -615,23 +615,16 @@ type Decimal msg = "round cannot accept " + self.to_text Error.throw (Arithmetic_Error.Error msg) False -> check_round_input self <| - decimal_result = case use_bankers of - False -> - scale = 10 ^ decimal_places - ((self * scale) + 0.5).floor / scale - True -> - ## If the largest integer <= self is odd, use normal - round-towards-positive-infinity rounding; otherwise, - use round-towards-negative-infinity rounding. - scale = 10 ^ decimal_places - scaled_self = self * scale - self_scaled_floor = scaled_self.floor - is_even = (self_scaled_floor % 2) == 0 - case is_even of - False -> - (scaled_self + 0.5).floor / scale - True -> - (scaled_self - 0.5).ceil / scale + decimal_result = + # Algorithm taken from https://stackoverflow.com/a/7211688 + scale = 10 ^ decimal_places + scaled = self * scale + round_base = scaled.floor + round_midpoint = (round_base + 0.5) / scale + even_is_up = if self >= 0 then (scaled.truncate % 2) != 0 else (scaled.truncate % 2) == 0 + half_goes_up = if use_bankers then even_is_up else True + do_round_up = if half_goes_up then self >= round_midpoint else self > round_midpoint + if do_round_up then ((round_base + 1.0) / scale) else (round_base / scale) # Convert to integer if it's really an integer anyway. if decimal_places > 0 then decimal_result else decimal_result.truncate @@ -940,11 +933,24 @@ type Integer 12250 . round -2 use_bankers=True == 12200 round : Integer -> Boolean -> Integer ! Illegal_Argument round self decimal_places=0 use_bankers=False = - check_decimal_places decimal_places <| check_round_input self <| - ## It's already an integer so unless decimal_places is - negative, the value is unchanged. - if decimal_places >= 0 then self else - self.to_decimal.round decimal_places use_bankers . truncate + ## It's already an integer so unless decimal_places is + negative, the value is unchanged. + if decimal_places >= 0 then self else + check_decimal_places decimal_places <| check_round_input self <| + scale = 10 ^ -decimal_places + halfway = scale.div 2 + remainder = self % scale + scaled_down = self.div scale + result_unnudged = scaled_down * scale + case self >= 0 of + True -> + half_goes_up = if use_bankers then (scaled_down % 2) != 0 else True + round_up = if half_goes_up then remainder >= halfway else remainder > halfway + if round_up then result_unnudged + scale else result_unnudged + False -> + half_goes_up = if use_bankers then (scaled_down % 2) == 0 else True + round_up = if half_goes_up then remainder < -halfway else remainder <= -halfway + if round_up then result_unnudged - scale else result_unnudged ## Compute the negation of this. diff --git a/test/Tests/src/Data/Numbers_Spec.enso b/test/Tests/src/Data/Numbers_Spec.enso index 09eac34a8d7..1663c79092b 100644 --- a/test/Tests/src/Data/Numbers_Spec.enso +++ b/test/Tests/src/Data/Numbers_Spec.enso @@ -472,6 +472,7 @@ spec = Number.nan . equals 0 . should_fail_with Incomparable_Values Test.group "Decimal.round" <| + Test.specify "Can round positive decimals correctly" <| 3.0 . round . should_equal 3 3.00001 . round . should_equal 3 @@ -585,6 +586,64 @@ spec = 231.2 . round . should_be_a Integer 231.2 . round -1 . should_be_a Integer + Test.specify "Can round correctly near the precision limit" <| + 1.22222222225 . round 10 . should_equal 1.2222222223 + 1.222222222225 . round 11 . should_equal 1.22222222223 + 1.2222222222225 . round 12 . should_equal 1.222222222223 + 1.22222222222225 . round 13 . should_equal 1.2222222222223 + 1.222222222222225 . round 14 . should_equal 1.22222222222223 + 1.2222222222222225 . round 15 . should_equal 1.222222222222223 + + -1.22222222225 . round 10 . should_equal -1.2222222222 + -1.222222222225 . round 11 . should_equal -1.22222222222 + -1.2222222222225 . round 12 . should_equal -1.222222222222 + -1.22222222222225 . round 13 . should_equal -1.2222222222222 + -1.222222222222225 . round 14 . should_equal -1.22222222222222 + -1.2222222222222225 . round 15 . should_equal -1.222222222222222 + + 1.22222222235 . round 10 . should_equal 1.2222222224 + 1.222222222235 . round 11 . should_equal 1.22222222224 + 1.2222222222235 . round 12 . should_equal 1.222222222224 + 1.22222222222235 . round 13 . should_equal 1.2222222222224 + 1.222222222222235 . round 14 . should_equal 1.22222222222224 + 1.2222222222222235 . round 15 . should_equal 1.222222222222224 + + -1.22222222235 . round 10 . should_equal -1.2222222223 + -1.222222222235 . round 11 . should_equal -1.22222222223 + -1.2222222222235 . round 12 . should_equal -1.222222222223 + -1.22222222222235 . round 13 . should_equal -1.2222222222223 + -1.222222222222235 . round 14 . should_equal -1.22222222222223 + -1.2222222222222235 . round 15 . should_equal -1.222222222222223 + + Test.specify "Can round correctly near the precision limit, using banker's rounding" <| + 1.22222222225 . round 10 use_bankers=True . should_equal 1.2222222222 + 1.222222222225 . round 11 use_bankers=True . should_equal 1.22222222222 + 1.2222222222225 . round 12 use_bankers=True . should_equal 1.222222222222 + 1.22222222222225 . round 13 use_bankers=True . should_equal 1.2222222222222 + 1.222222222222225 . round 14 use_bankers=True . should_equal 1.22222222222222 + 1.2222222222222225 . round 15 use_bankers=True . should_equal 1.222222222222222 + + -1.22222222225 . round 10 use_bankers=True . should_equal -1.2222222222 + -1.222222222225 . round 11 use_bankers=True . should_equal -1.22222222222 + -1.2222222222225 . round 12 use_bankers=True . should_equal -1.222222222222 + -1.22222222222225 . round 13 use_bankers=True . should_equal -1.2222222222222 + -1.222222222222225 . round 14 use_bankers=True . should_equal -1.22222222222222 + -1.2222222222222225 . round 15 use_bankers=True . should_equal -1.222222222222222 + + 1.22222222235 . round 10 use_bankers=True . should_equal 1.2222222224 + 1.222222222235 . round 11 use_bankers=True . should_equal 1.22222222224 + 1.2222222222235 . round 12 use_bankers=True . should_equal 1.222222222224 + 1.22222222222235 . round 13 use_bankers=True . should_equal 1.2222222222224 + 1.222222222222235 . round 14 use_bankers=True . should_equal 1.22222222222224 + 1.2222222222222235 . round 15 use_bankers=True . should_equal 1.222222222222224 + + -1.22222222235 . round 10 use_bankers=True . should_equal -1.2222222224 + -1.222222222235 . round 11 use_bankers=True . should_equal -1.22222222224 + -1.2222222222235 . round 12 use_bankers=True . should_equal -1.222222222224 + -1.22222222222235 . round 13 use_bankers=True . should_equal -1.2222222222224 + -1.222222222222235 . round 14 use_bankers=True . should_equal -1.22222222222224 + -1.2222222222222235 . round 15 use_bankers=True . should_equal -1.222222222222224 + Test.specify "Input out of range" <| 100000000000000.0 . round . should_fail_with Illegal_Argument -100000000000000.0 . round . should_fail_with Illegal_Argument @@ -602,10 +661,12 @@ spec = Number.positive_infinity . round . should_fail_with Arithmetic_Error Number.negative_infinity . round . should_fail_with Arithmetic_Error - Test.specify "Banker's rounding failure" pending="Fails because of basic floating-point inaccuracy when adding 0.5" <| + Test.specify "Floating point imperfect representation counter-examples" <| 1.225 . round 2 use_bankers=True . should_equal 1.22 # Actual result 1.23 + 37.785 . round 2 . should_equal 37.79 Test.group "Integer.round" <| + Test.specify "Can round small integers to a specified number of decimal places correctly (value is unchanged)" 0 . round . should_equal 0 3 . round . should_equal 3 @@ -615,76 +676,110 @@ spec = 3 . round 1 . should_equal 3 -3 . round 1 . should_equal -3 - Test.specify "Can round small integers to a specified number of negative places correctly" + Test.specify "Can round integers to a specified number of negative places correctly" + 0 . round -1 . should_equal 0 4 . round -1 . should_equal 0 + 5 . round -1 . should_equal 10 + 6 . round -1 . should_equal 10 + 9 . round -1 . should_equal 10 + 10 . round -1 . should_equal 10 + 11 . round -1 . should_equal 10 24 . round -1 . should_equal 20 25 . round -1 . should_equal 30 29 . round -1 . should_equal 30 30 . round -1 . should_equal 30 31 . round -1 . should_equal 30 + 2000 . round -3 . should_equal 2000 + 2001 . round -3 . should_equal 2000 2412 . round -3 . should_equal 2000 2499 . round -3 . should_equal 2000 2500 . round -3 . should_equal 3000 + 2501 . round -3 . should_equal 3000 2511 . round -3 . should_equal 3000 2907 . round -3 . should_equal 3000 + 2999 . round -3 . should_equal 3000 3000 . round -3 . should_equal 3000 + 3001 . round -3 . should_equal 3000 3098 . round -3 . should_equal 3000 3101 . round -3 . should_equal 3000 - Test.specify "Can round negative small integers to a specified number of negative places correctly" + Test.specify "Can round negative integers to a specified number of negative places correctly" -4 . round -1 . should_equal 0 + -5 . round -1 . should_equal 0 + -6 . round -1 . should_equal -10 + -9 . round -1 . should_equal -10 + -10 . round -1 . should_equal -10 + -11 . round -1 . should_equal -10 -24 . round -1 . should_equal -20 -25 . round -1 . should_equal -20 -29 . round -1 . should_equal -30 -30 . round -1 . should_equal -30 -31 . round -1 . should_equal -30 + -2000 . round -3 . should_equal -2000 + -2001 . round -3 . should_equal -2000 -2412 . round -3 . should_equal -2000 -2499 . round -3 . should_equal -2000 -2500 . round -3 . should_equal -2000 + -2501 . round -3 . should_equal -3000 -2511 . round -3 . should_equal -3000 -2907 . round -3 . should_equal -3000 + -2999 . round -3 . should_equal -3000 -3000 . round -3 . should_equal -3000 + -3001 . round -3 . should_equal -3000 -3098 . round -3 . should_equal -3000 -3101 . round -3 . should_equal -3000 - Test.specify "Banker's rounding handles half-way values correctly" <| - 12350 . round -2 use_bankers=True . should_equal 12400 - 12250 . round -2 use_bankers=True . should_equal 12200 - -12350 . round -2 use_bankers=True . should_equal -12400 - -12250 . round -2 use_bankers=True . should_equal -12200 - - Test.specify "Banker's rounding handles non-half-way values just like normal rounding" <| + Test.specify "Can round negative integers to a specified number of negative places with banker's rounding correctly" <| + 12300 . round -2 use_bankers=True . should_equal 12300 + 12301 . round -2 use_bankers=True . should_equal 12300 12330 . round -2 use_bankers=True . should_equal 12300 + 12349 . round -2 use_bankers=True . should_equal 12300 + 12350 . round -2 use_bankers=True . should_equal 12400 + 12351 . round -2 use_bankers=True . should_equal 12400 12370 . round -2 use_bankers=True . should_equal 12400 12430 . round -2 use_bankers=True . should_equal 12400 12470 . round -2 use_bankers=True . should_equal 12500 + 12249 . round -2 use_bankers=True . should_equal 12200 + 12250 . round -2 use_bankers=True . should_equal 12200 + 12251 . round -2 use_bankers=True . should_equal 12300 + + -12300 . round -2 use_bankers=True . should_equal -12300 + -12301 . round -2 use_bankers=True . should_equal -12300 -12330 . round -2 use_bankers=True . should_equal -12300 + -12349 . round -2 use_bankers=True . should_equal -12300 + -12350 . round -2 use_bankers=True . should_equal -12400 + -12351 . round -2 use_bankers=True . should_equal -12400 -12370 . round -2 use_bankers=True . should_equal -12400 -12430 . round -2 use_bankers=True . should_equal -12400 -12470 . round -2 use_bankers=True . should_equal -12500 + -12249 . round -2 use_bankers=True . should_equal -12200 + -12250 . round -2 use_bankers=True . should_equal -12200 + -12251 . round -2 use_bankers=True . should_equal -12300 + Test.specify "Returns the correct type" <| 231 . round 1 . should_be_a Integer 231 . round 0 . should_be_a Integer 231 . round . should_be_a Integer 231 . round -1 . should_be_a Integer - Test.specify "Decimal places out of range" <| - 3 . round 16 . should_fail_with Illegal_Argument - 3 . round -16 . should_fail_with Illegal_Argument - Test.specify "Input out of range" <| - 100000000000000 . round . should_fail_with Illegal_Argument - -100000000000000 . round . should_fail_with Illegal_Argument 100000000000000 . round -2 . should_fail_with Illegal_Argument -100000000000000 . round -2 . should_fail_with Illegal_Argument 99999999999999 . round -2 . should_equal 100000000000000 -99999999999999 . round -2 . should_equal -100000000000000 + Test.specify "Input out of range is ignored when the implementation returns its argument immediately" <| + 100000000000000 . round . should_equal 100000000000000 + -100000000000000 . round . should_equal -100000000000000 + 100000000000000 . round 1 . should_equal 100000000000000 + -100000000000000 . round 1 . should_equal -100000000000000 + Test.group "Decimal.truncate" + Test.specify "Correctly converts to Integer" <| 0.1.truncate . should_equal 0 0.9.truncate . should_equal 0 @@ -696,6 +791,7 @@ spec = -3.9.truncate . should_equal -3 Test.group "Integer.truncate" + Test.specify "Returns its argument" <| 0.truncate . should_equal 0 3.truncate . should_equal 3