Add Decimal.round (#9672)

This commit is contained in:
GregoryTravis 2024-04-11 11:47:50 -04:00 committed by GitHub
parent bf90e2e8a8
commit e3afa5561d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 118 additions and 12 deletions

View File

@ -648,6 +648,7 @@
- [Added `Decimal.parse` and `.format`.][9637]
- [Added `Decimal.abs`, `.negate` and `.signum`.][9641]
- [Added `Decimal.min` and `.max`.][9663]
- [Added `Decimal.round`.][9672]
[debug-shortcuts]:
https://github.com/enso-org/enso/blob/develop/app/gui/docs/product/shortcuts.md#debug
@ -946,6 +947,7 @@
[9637]: https://github.com/enso-org/enso/pull/9637
[9641]: https://github.com/enso-org/enso/pull/9641
[9663]: https://github.com/enso-org/enso/pull/9663
[9672]: https://github.com/enso-org/enso/pull/9672
#### Enso Compiler

View File

@ -18,6 +18,7 @@ from project.Widget_Helpers import make_number_format_selector
polyglot java import java.lang.ArithmeticException
polyglot java import java.lang.NumberFormatException
polyglot java import java.lang.Integer as Java_Integer
polyglot java import java.math.BigDecimal
polyglot java import java.text.DecimalFormat
polyglot java import java.text.DecimalFormatSymbols
@ -794,6 +795,61 @@ type Decimal
message = "Outside representable Float range (approximately (-1.8E308, 1.8E308))"
Warning.attach (Out_Of_Range.Error self message) f
## GROUP Rounding
ICON math
Round to a specified number of decimal places.
By default, rounding uses "symmetric round-half-up", also known as
"half-up." If use_bankers=True, then it uses "round-half-even", also
known as "banker's rounding".
Arguments:
- decimal_places: The number of decimal places to round to. Can be
negative, which results in rounding to positive integer powers of 10.
Must be between Java `Integer.MIN_VALUE` and `Integer.MAX_VALUE`
(-2147483648 and 2147483647) (inclusive).
- use_bankers: Rounds mid-point to nearest even number.
! Error Conditions
If `decimal_places` is outside the range `Integer.MIN_VALUE` and
`Integer.MAX_VALUE` (inclusive), an `Illegal_Argument` error is thrown.
? Negative decimal place counts
Rounding to `n` digits can be thought of as "rounding to the nearest
multiple of 10^(-n)". For negative decimal counts, this results in
rounding to the nearest positive integer power of 10.
> Example
Round to the nearest integer.
Decimal.new "3.3" . round
# => Decimal.new "3"
> Example
Round to two decimal places.
Decimal.new "3.1415" . round 2
# => Decimal.new "3.14"
> Example
Round a very large number.
Decimal.new "1234.5678E-50" . round 53
# => Decimal.new "1234.568E-50"
> Example
Use Banker's Rounding.
Decimal.new "2.5" . round use_bankers=True
# => 2
round : Integer -> Boolean -> Decimal
round self (decimal_places:Integer=0) (use_bankers:Boolean=False) -> Decimal =
out_of_range = decimal_places > Java_Integer.MAX_VALUE || decimal_places < Java_Integer.MIN_VALUE
if out_of_range.not then Decimal.Value (Decimal_Utils.round self.big_decimal decimal_places use_bankers) else
message = "round decimal_places must be between "+Java_Integer.MIN_VALUE.to_text+" and "+Java_Integer.MAX_VALUE.to_text+" (inclusive), but was "+decimal_places.to_text
Error.throw (Out_Of_Range.Error decimal_places message)
## GROUP Conversions
ICON convert
Converts a `Decimal` to a string, using the Java `DecimalFormat` formatter.

View File

@ -655,9 +655,9 @@ type Float
ICON math
Round to a specified number of decimal places.
By default, rounding uses "asymmetric round-half-up", also known as
"round towards positive infinity." If use_bankers=True, then it uses
"round-half-even", also known as "banker's rounding".
By default, rounding uses "symmetric round-half-up", also known as
"half-up." If use_bankers=True, then it uses "round-half-even", also
known as "banker's rounding".
If `decimal_places` > 0, `round` returns a `Float`; otherwise, it
returns an `Integer`.
@ -1017,9 +1017,9 @@ type Integer
For integers, rounding to 0 or more decimal places simply returns the
argument. For negative decimal places, see below.
By default, rounding uses "asymmetric round-half-up", also known as
"round towards positive infinity." If use_bankers=True, then it uses
"round-half-even", also known as "banker's rounding".
By default, rounding uses "symmetric round-half-up", also known as
"half-up." If use_bankers=True, then it uses "round-half-even", also
known as "banker's rounding".
Arguments:
- decimal_places: The number of decimal places to round to. Can be

View File

@ -3,6 +3,7 @@ package org.enso.base.numeric;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.MathContext;
import java.math.RoundingMode;
/** Utils for the Enso Decmial type. */
public class Decimal_Utils {
@ -91,4 +92,12 @@ public class Decimal_Utils {
return bd.toBigIntegerExact().hashCode();
}
}
public static BigDecimal round(BigDecimal bd, int decimalPlaces, boolean useBankers) {
var roundingMode = useBankers ? RoundingMode.HALF_EVEN : RoundingMode.HALF_UP;
// `stripTrailingZeros` is necessary because rounding can produce results
// that have extra trailing zeros, which take up space and slow down later
// calculations.
return bd.setScale(decimalPlaces, roundingMode).stripTrailingZeros();
}
}

View File

@ -7,6 +7,10 @@ from Standard.Base.Errors.Common import Loss_Of_Numeric_Precision, Out_Of_Range
from Standard.Test import all
import project.Data.Round_Spec
polyglot java import java.lang.Integer as Java_Integer
Decimal.should_have_rep self rep = self.internal_representation . should_equal rep
add_specs suite_builder =
@ -729,6 +733,36 @@ add_specs suite_builder =
Decimal.new "-12.345E97" . signum . should_equal -1
Decimal.new "0" . signum . should_equal 0
suite_builder.group "rounding" group_builder->
do_round n dp=0 use_bankers=False = Decimal.new n . round dp use_bankers
Round_Spec.add_specs group_builder do_round
group_builder.specify "Large values" <|
Decimal.new "1234.5678E-50" . round 53 . should_equal (Decimal.new "1234.568E-50")
Decimal.new "1234.5678E-50" . round 52 . should_equal (Decimal.new "1234.57E-50")
Decimal.new "1234.5678E-100" . round 101 . should_equal (Decimal.new "1234.6E-100")
Decimal.new "1234.5678E-100" . round 103 . should_equal (Decimal.new "1234.568E-100")
Decimal.new "-1234.5678E-50" . round 53 . should_equal (Decimal.new "-1234.568E-50")
Decimal.new "-1234.5678E-50" . round 52 . should_equal (Decimal.new "-1234.57E-50")
Decimal.new "-1234.5678E-100" . round 101 . should_equal (Decimal.new "-1234.6E-100")
Decimal.new "-1234.5678E-100" . round 103 . should_equal (Decimal.new "-1234.568E-100")
group_builder.specify "Strips trailing zeros" <|
Decimal.new "1234.5678" . round 5 . internal_representation . should_equal [12345678, 8, 4]
group_builder.specify "Decimal places out of range" <|
too_big_positive = Java_Integer.MAX_VALUE + 1
too_big_negative = Java_Integer.MIN_VALUE - 1
Decimal.new "3.1" . round too_big_positive . should_fail_with (Out_Of_Range.Error too_big_positive "round decimal_places must be between "+Java_Integer.MIN_VALUE.to_text+" and "+Java_Integer.MAX_VALUE.to_text+" (inclusive), but was "+too_big_positive.to_text)
Decimal.new "3.1" . round too_big_negative . should_fail_with (Out_Of_Range.Error too_big_negative "round decimal_places must be between "+Java_Integer.MIN_VALUE.to_text+" and "+Java_Integer.MAX_VALUE.to_text+" (inclusive), but was "+too_big_negative.to_text)
group_builder.specify "Examples" <|
Decimal.new "3.3" . round . should_equal (Decimal.new "3")
Decimal.new "3.1415" . round 2 . should_equal (Decimal.new "3.14")
Decimal.new "1234.5678E-50" . round 53 . should_equal (Decimal.new "1234.568E-50")
Decimal.new "2.5" . round use_bankers=True . should_equal 2
suite_builder.group "min/max" group_builder->
group_builder.specify "should calculate min and max correctly" <|
Decimal.new "12" . min (Decimal.new "13") . should_equal (Decimal.new "12")

View File

@ -61,8 +61,6 @@ add_specs suite_builder =
very_negative = -99223372036854775808
suite_builder.group "Integers" group_builder->
Round_Spec.add_specs group_builder (.round)
group_builder.specify "should be of unbound size when multiplied" <|
1.up_to 101 . fold 1 (*) . should_equal hundred_factorial
@ -605,6 +603,13 @@ add_specs suite_builder =
(922337203685477580712345 - 922337203685477580700000) . round . should_equal 12345
((99999999999998 * 1000).div 1000) . round . should_equal 99999999999998
suite_builder.group "Rounding" group_builder->
Round_Spec.add_specs group_builder (.round)
group_builder.specify "Decimal places out of range" <|
3.1 . round 16 . should_fail_with Illegal_Argument
3.1 . round -16 . should_fail_with Illegal_Argument
suite_builder.group "Float.truncate" group_builder->
group_builder.specify "Correctly converts to Integer" <|

View File

@ -178,10 +178,6 @@ add_specs group_builder round_fun =
round_fun -1.22222222222235 13 use_bankers=True . should_equal -1.2222222222224
round_fun -1.222222222222235 14 use_bankers=True . should_equal -1.22222222222224
group_builder.specify "Decimal places out of range" <|
round_fun 3.1 16 . should_fail_with Illegal_Argument
round_fun 3.1 -16 . should_fail_with Illegal_Argument
group_builder.specify "Floating point imperfect representation counter-examples" <|
round_fun 1.225 2 use_bankers=True . should_equal 1.22 # Actual result 1.23
round_fun 37.785 2 . should_equal 37.79

View File

@ -181,6 +181,10 @@ add_specs suite_builder setup =
Round_Spec.add_specs group_builder do_round
group_builder.specify "Decimal places out of range" <|
3.1 . round 16 . should_fail_with Illegal_Argument
3.1 . round -16 . should_fail_with Illegal_Argument
## Runs the provided callback with a few combinations of columns, where some
of them are made Mixed (but still contain only the original values).
If the backend does not support mixed columns, the callback is run only