From 0268cbb55b6e654dc9c67ee1953323b618c18ca3 Mon Sep 17 00:00:00 2001 From: GregoryTravis Date: Tue, 16 Jul 2024 14:37:25 -0400 Subject: [PATCH] Various Decimal usability tweaks (#10517) --- CHANGELOG.md | 2 + .../Base/0.0.0-dev/src/Data/Decimal.enso | 133 ++++++++++++------ .../0.0.0-dev/src/Data/Json/Extensions.enso | 2 +- .../lib/Standard/Base/0.0.0-dev/src/Main.enso | 1 + test/Base_Tests/src/Data/Decimal_Spec.enso | 55 ++++++-- test/Base_Tests/src/Data/Json_Spec.enso | 2 +- 6 files changed, 143 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c52cda6066c4..b9a15a596815 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,12 +33,14 @@ - [Rename `Map` to `Dictionary` and `Set` to `Hashset`.][10474] - [Compare two objects with `Ordering.compare` and define comparator with `Comparable.new`][10468] +- [Added `dec` construction function for creating `Decimal`s.][10517] [10434]: https://github.com/enso-org/enso/pull/10434 [10445]: https://github.com/enso-org/enso/pull/10445 [10466]: https://github.com/enso-org/enso/pull/10466 [10467]: https://github.com/enso-org/enso/pull/10467 [10474]: https://github.com/enso-org/enso/pull/10474 +[10517]: https://github.com/enso-org/enso/pull/10517 # Enso 2024.2 diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Decimal.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Decimal.enso index 21fc646945ca..70a19be554e2 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Decimal.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Decimal.enso @@ -4,6 +4,7 @@ import project.Data.Numeric.Internal.Decimal_Internal import project.Data.Numeric.Math_Context.Math_Context import project.Data.Numeric.Rounding_Mode.Rounding_Mode import project.Data.Text.Text +import project.Data.Vector.Vector import project.Error.Error import project.Errors.Illegal_Argument.Illegal_Argument import project.Nothing.Nothing @@ -89,7 +90,7 @@ type Decimal Value (big_decimal : BigDecimal) ## ICON input_number - Construct a `Decimal` from a string or integer. + Construct a `Decimal` from a `Text`, `Integer` or `Float`. Arguments: - x: The `Text`, `Integer`, or `Float` to construct a `Decimal` from. @@ -111,14 +112,24 @@ type Decimal if a `Math_Context` value is explicitly passed. ^ Example - Create a `Decimal` from a string. + Create a `Decimal` from a `Text`. c = Decimal.new "12.345" + + ^ Example + Create a `Decimal` from an `Integer`. + + c = Decimal.new 12345 + + ^ Example + Create a `Decimal` from a `Float`. + + c = Decimal.new 12.345 new : Text | Integer | Float -> Math_Context | Nothing -> Decimal ! Arithmetic_Error | Number_Parse_Error new (x : Text | Integer | Float) (mc : Math_Context | Nothing = Nothing) -> Decimal ! Arithmetic_Error | Number_Parse_Error = handle_java_exception <| case x of - _ : Text -> Decimal.from_string x mc + _ : Text -> Decimal.from_text x mc _ : Integer -> Decimal.from_integer x mc _ : Float -> Decimal.from_float x mc @@ -145,11 +156,11 @@ type Decimal if a `Math_Context` value is explicitly passed. ^ Example - Create a `Decimal` from a string. + Create a `Decimal` from a `Text`. - d = Decimal.from_string "12.345" - from_string : Text -> Math_Context | Nothing -> Decimal ! Number_Parse_Error - from_string (s : Text) (mc : Math_Context | Nothing = Nothing) -> Decimal ! Number_Parse_Error = + d = Decimal.from_text "12.345" + from_text : Text -> Math_Context | Nothing -> Decimal ! Number_Parse_Error + from_text (s : Text) (mc : Math_Context | Nothing = Nothing) -> Decimal ! Number_Parse_Error = handle_java_exception <| handle_number_format_exception <| case mc of _ : Math_Context -> Decimal.Value <| handle_precision_loss s <| Decimal_Utils.fromString s mc.math_context @@ -172,7 +183,7 @@ type Decimal if a `Math_Context` value is explicitly passed. ^ Example - Create a `Decimal` from an integer. + Create a `Decimal` from an `Integer`. d = Decimal.from_integer 12 from_integer : Integer -> Math_Context | Nothing -> Decimal @@ -207,7 +218,7 @@ type Decimal - If `f` is NaN or +/-Inf, an Illegal_Argument error is thrown. ^ Example - Create a `Decimal` from a float. + Create a `Decimal` from a `Float`. d = Decimal.from_integer 12.345 from_float : Float -> Math_Context | Nothing -> Decimal ! Arithmetic_Error | Illegal_Argument @@ -343,7 +354,7 @@ type Decimal c = a + b # => Decimal.new 30.55 + : Decimal -> Decimal - + self (that : Decimal) = self.add that + + self (that : Decimal) -> Decimal ! Arithmetic_Error = self.add that ## ALIAS minus GROUP Operators @@ -402,7 +413,7 @@ type Decimal c = a - b # => Decimal.new 10.11 - : Decimal -> Decimal - - self (that : Decimal) = self.subtract that + - self (that : Decimal) -> Decimal ! Arithmetic_Error = self.subtract that ## ALIAS times GROUP Operators @@ -460,7 +471,7 @@ type Decimal c = a * b # => Decimal.new 207.7726 * : Decimal -> Decimal - * self (that : Decimal) = self.multiply that + * self (that : Decimal) -> Decimal ! Arithmetic_Error = self.multiply that ## GROUP Operators ICON math @@ -524,7 +535,7 @@ type Decimal c = a / b # => Decimal.new 45.67 / : Decimal -> Decimal - / self (that : Decimal) = self.divide that + / self (that : Decimal) -> Decimal ! Arithmetic_Error = self.divide that ## ALIAS modulo, modulus GROUP Operators @@ -554,7 +565,7 @@ type Decimal remainder = Decimal.new -5 . remainder 3 # => -2 remainder : Decimal -> Decimal - remainder self (that : Decimal) = + remainder self (that : Decimal) -> Decimal = handle_java_exception <| Decimal.Value (self.big_decimal.remainder that.big_decimal) @@ -586,7 +597,7 @@ type Decimal remainder = Decimal.new -5 % 3 # => -2 % : Decimal -> Decimal - % self (that : Decimal) = self.remainder that + % self (that : Decimal) -> Decimal = self.remainder that ## GROUP Math ICON math @@ -640,7 +651,7 @@ type Decimal Decimal.new "2.25" . pow (Decimal.new "5") # => 57.6650390625 pow : Integer -> Decimal - pow self exp:Integer = + pow self exp:Integer -> Decimal = ## If `exp` is an integer that does not fit in a Java Integer, UnsuppUnsupported_Argument_Types is raised, so we convert that to an Arithmetic_Error. @@ -667,7 +678,7 @@ type Decimal Decimal.new "2.25" ^ Decimal.new "5" # => 57.6650390625 ^ : Integer -> Decimal - ^ self exp:Integer = self.pow exp + ^ self exp:Integer -> Decimal = self.pow exp ## GROUP Operators ICON operators @@ -679,7 +690,7 @@ type Decimal 5.1.negate # => Decimal.new -5.1 negate : Decimal - negate self = Decimal.Value self.big_decimal.negate + negate self -> Decimal = Decimal.Value self.big_decimal.negate ## GROUP Math ICON math @@ -755,9 +766,11 @@ type Decimal # => 2345 to_integer : Integer to_integer self = - i = self.big_decimal.toBigInteger - if self == i then i else - Warning.attach (Loss_Of_Numeric_Precision.Warning self i) i + as_biginteger = self.big_decimal.toBigInteger + back_to_decimal = BigDecimal.new as_biginteger + are_equal = (self.big_decimal.compareTo back_to_decimal) == 0 + if are_equal then as_biginteger else + Warning.attach (Loss_Of_Numeric_Precision.Warning self as_biginteger) as_biginteger ## GROUP Conversions ICON convert @@ -791,8 +804,8 @@ type Decimal d = Decimal.new "23.45" d.to_float # => 23.45 - to_float : Integer - to_float self = + to_float : Float + to_float self -> Float = f = self.big_decimal.doubleValue if f.is_finite then attach_loss_of_numeric_precision self f else message = "Outside representable Float range (approximately (-1.8E308, 1.8E308))" @@ -855,7 +868,7 @@ type Decimal ## GROUP Rounding ICON math - Computes the nearest integer equal to or above this number. + Computes the nearest `Integer` equal to or above this number. > Example Compute the ceiling of 12.34. @@ -873,7 +886,7 @@ type Decimal ## GROUP Rounding ICON math - Computes the nearest integer equal to or below this number. + Computes the nearest `Integer` equal to or below this number. > Example Compute the floor of 12.34. @@ -893,8 +906,8 @@ type Decimal GROUP Rounding ICON math - Truncate a number to an integer to by dropping the fractional part. This - is equivalent to "round-toward-zero". + Truncate a number to an `Integer` to by dropping the fractional part. + This is equivalent to "round-toward-zero". > Example Compute the truncation of 12.34 @@ -942,7 +955,7 @@ type Decimal @format make_number_format_selector @locale Locale.default_widget format : Text -> Locale -> Text - format self format:Text="" locale:Locale=Locale.default = + format self format:Text="" locale:Locale=Locale.default -> Text = symbols = DecimalFormatSymbols.new locale.java_locale formatter = DecimalFormat.new format symbols formatter.format self.big_decimal @@ -986,50 +999,90 @@ type Decimal Decimal.parse "123.456.789,87654" locale=Locale.italy # => 123456789.87654 parse : Text -> Locale | Nothing -> Decimal ! Number_Parse_Error - parse text locale:(Locale | Nothing)=Nothing = case locale of - Nothing -> Decimal.from_string text + parse text locale:(Locale | Nothing)=Nothing -> Decimal ! Number_Parse_Error = case locale of + Nothing -> Decimal.from_text text Locale.Value java_locale -> Panic.catch ParseException ((NumberFormat.getInstance java_locale).parse text) _-> Error.throw (Number_Parse_Error.Error text) ## PRIVATE precision : Integer - precision self = self.big_decimal.precision + precision self -> Integer = self.big_decimal.precision ## PRIVATE scale : Integer - scale self = self.big_decimal.scale + scale self -> Integer = self.big_decimal.scale ## PRIVATE with_scale : Integer -> Decimal - private with_scale self new_scale:Integer = + private with_scale self new_scale:Integer -> Decimal = if self.scale == new_scale then self else Decimal.Value (self.big_decimal.setScale new_scale) ## PRIVATE unscaled_value : Integer - unscaled_value self = self.big_decimal.unscaledValue + unscaled_value self -> Integer = self.big_decimal.unscaledValue ## PRIVATE - internal_representation : [Integer] - internal_representation self = [self.unscaled_value, self.precision, self.scale] + internal_representation : Vector Integer + internal_representation self -> Vector Integer = [self.unscaled_value, self.precision, self.scale] ## PRIVATE Note: the underlying Java `BigDecimal` implementation is not affected by locale. to_text : Text - to_text self = self.big_decimal.toString + to_text self -> Text = self.big_decimal.toString ## PRIVATE Note: the underlying Java `BigDecimal` implementation is not affected by locale. to_display_text : Text - to_display_text self = self.big_decimal.toString + to_display_text self -> Text = self.big_decimal.toString ## PRIVATE Note: the underlying Java `BigDecimal` implementation is not affected by locale. to_text_without_scientific_notation : Text - to_text_without_scientific_notation self = self.big_decimal.toPlainString + to_text_without_scientific_notation self -> Text = self.big_decimal.toPlainString + +## ICON input_number + Construct a `Decimal` from a `Text`, `Integer` or `Float`. + + Arguments: + - x: The `Text`, `Integer`, or `Float` to construct a `Decimal` from. + - mc: The `Math_Context` to use to specify precision and `Rounding_Mode`. + If a `Math_Context` is used, there is a possibility of a loss of + precision. + + ? Number Format + + The textual format for a Decimal is defined at + https://docs.oracle.com/javase/8/docs/api/java/math/BigDecimal.html#BigDecimal-java.lang.String-. + + ! Error Conditions + + - If the `Text` argument is incorrectly formatted, a `Number_Parse_Error` + is thrown. + - If the construction of the Decimal results in a loss of precision, a + `Loss_Of_Numeric_Precision` warning is attached. This can only happen + if a `Math_Context` value is explicitly passed. + + ^ Example + Create a `Decimal` from a `Text`. + + c = dec "12.345" + + ^ Example + Create a `Decimal` from an `Integer`. + + c = dec 12345 + + ^ Example + Create a `Decimal` from a `Float`. + + c = dec 12.345 +dec : Text | Integer | Float -> Math_Context | Nothing -> Decimal ! Arithmetic_Error | Number_Parse_Error +dec (x : Text | Integer | Float) (mc : Math_Context | Nothing = Nothing) -> Decimal ! Arithmetic_Error | Number_Parse_Error = + Decimal.new x mc ## PRIVATE handle_number_format_exception ~action = @@ -1065,7 +1118,7 @@ Comparable.from (that : Decimal) = Comparable.new that Decimal_Comparator Comparable.from (that : Number) = Comparable.new that Decimal_Comparator ## PRIVATE -Decimal.from (that : Text) = Decimal.from_string that +Decimal.from (that : Text) = Decimal.from_text that ## PRIVATE Decimal.from (that : Integer) = Decimal.new that diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Json/Extensions.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Json/Extensions.enso index 53aa4bd0a584..a7a00279a04a 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Json/Extensions.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Json/Extensions.enso @@ -80,7 +80,7 @@ Decimal.from (that:JS_Object) = case that.get "type" == "Decimal" && ["value", "scale", "precision"].all that.contains_key of True -> math_context = Math_Context.new (that.at "precision") - raw_value = Decimal.from_string (that.at "value") math_context + raw_value = Decimal.from_text (that.at "value") math_context raw_value.with_scale (that.at "scale") False -> Error.throw (Illegal_Argument.Error "Invalid JS_Object for Decimal.") diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Main.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Main.enso index 440fb021face..1ea217cb6cfc 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Main.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Main.enso @@ -2,6 +2,7 @@ export project.Any.Any export project.Data export project.Data.Array.Array export project.Data.Decimal.Decimal +export project.Data.Decimal.dec export project.Data.Dictionary.Dictionary export project.Data.Filter_Condition.Filter_Action export project.Data.Filter_Condition.Filter_Condition diff --git a/test/Base_Tests/src/Data/Decimal_Spec.enso b/test/Base_Tests/src/Data/Decimal_Spec.enso index 94881245b51f..842f36233a04 100644 --- a/test/Base_Tests/src/Data/Decimal_Spec.enso +++ b/test/Base_Tests/src/Data/Decimal_Spec.enso @@ -15,9 +15,17 @@ Decimal.should_have_rep self rep = self.internal_representation . should_equal r add_specs suite_builder = suite_builder.group "(Decimal_Spec) construction" group_builder-> + group_builder.specify "should be able to construct a Decimal with dec" <| + dec "123.45" . should_have_rep [12345, 5, 2] + dec 123.45 . should_have_rep [12345, 5, 2] + dec 12345 . should_have_rep [12345, 5, 0] + + mc = Math_Context.new 4 + dec "123.45" mc . should_have_rep [1235, 4, 1] + group_builder.specify "should be able to construct a Decimal from a string" <| Decimal.new "123.45" . should_have_rep [12345, 5, 2] - Decimal.from_string "123.45" . should_have_rep [12345, 5, 2] + Decimal.from_text "123.45" . should_have_rep [12345, 5, 2] group_builder.specify "should throw Number_Parse_Error on a badly-formatted string" <| Decimal.new "ee" . should_fail_with Number_Parse_Error @@ -35,7 +43,7 @@ add_specs suite_builder = group_builder.specify "should be able to construct a Decimal from a long string" <| Decimal.new "495782984723948723947239938732974241.2345" . should_have_rep [4957829847239487239472399387329742412345, 40, 4] - Decimal.from_string "495782984723948723947239938732974241.2345" . should_have_rep [4957829847239487239472399387329742412345, 40, 4] + Decimal.from_text "495782984723948723947239938732974241.2345" . should_have_rep [4957829847239487239472399387329742412345, 40, 4] group_builder.specify "should be able to construct a Decimal from a small integer" <| Decimal.new 1234500 . should_have_rep [1234500, 7, 0] @@ -97,17 +105,17 @@ add_specs suite_builder = mc4 = Math_Context.new 4 mc5 = Math_Context.new 5 - Problems.not_expect_warning (Decimal.new "123.25") + Problems.assume_no_problems (Decimal.new "123.25") Problems.expect_warning Loss_Of_Numeric_Precision (Decimal.new "123.25" mc4) - Problems.not_expect_warning Loss_Of_Numeric_Precision (Decimal.new "123.25" mc5) + Problems.assume_no_problems (Decimal.new "123.25" mc5) Problems.expect_warning Loss_Of_Numeric_Precision (Decimal.new 123.25) Problems.expect_warning Loss_Of_Numeric_Precision (Decimal.new 123.25 mc4) Problems.expect_warning Loss_Of_Numeric_Precision (Decimal.new 123.25 mc5) - Problems.not_expect_warning (Decimal.new 12325) + Problems.assume_no_problems (Decimal.new 12325) Problems.expect_warning Loss_Of_Numeric_Precision (Decimal.new 12325 mc4) - Problems.not_expect_warning Loss_Of_Numeric_Precision (Decimal.new 12325 mc5) + Problems.assume_no_problems (Decimal.new 12325 mc5) group_builder.specify "should throw Illegal_Argument for NaN/Inf" <| Decimal.new Number.nan . should_fail_with Illegal_Argument @@ -116,12 +124,15 @@ add_specs suite_builder = group_builder.specify "should be convertible via .from" <| Decimal.from "123.45" . should_equal (Decimal.new "123.45") - Decimal.from "123.45" . should_equal (Decimal.from_string "123.45") + Decimal.from "123.45" . should_equal (Decimal.from_text "123.45") Decimal.from 123.45 . should_equal (Decimal.new 123.45) Decimal.from 123.45 . should_equal (Decimal.from_float 123.45) Decimal.from 12345 . should_equal (Decimal.new 12345) Decimal.from 12345 . should_equal (Decimal.from_integer 12345) + group_builder.specify "should attach a Loss_Of_Numeric_Precision warning when converting float to decimal with .from" <| + Problems.expect_only_warning Loss_Of_Numeric_Precision (Decimal.from 123.45) + group_builder.specify "constructor should respect Math_Context " <| Decimal.new 12000 (Math_Context.new 0) . should_equal 12000 Decimal.new 12000 (Math_Context.new 1) . should_equal 10000 @@ -200,7 +211,7 @@ add_specs suite_builder = (Ordering.hash x0) . should_equal (Ordering.hash x1) - group_builder.specify "should compare correctly to Integer and Float" <| + group_builder.specify "should compare correctly to Integer and Float" pending="https://github.com/enso-org/enso/issues/10163" <| values = [] + [[0.1, 0.1]] + [["0.1", 0.1]] @@ -301,7 +312,7 @@ add_specs suite_builder = values.map pr-> v = pr.at 0 - d = Decimal.new v . to_float + d = Decimal.new v . remove_warnings expected = pr.at 1 . to_float d . should_equal expected @@ -336,6 +347,12 @@ add_specs suite_builder = (d <= d) . should_be_true (d >= d) . should_be_true + if v.is_a Float then + Problems.expect_only_warning Loss_Of_Numeric_Precision (d <= v) + Problems.expect_only_warning Loss_Of_Numeric_Precision (d >= v) + Problems.expect_only_warning Loss_Of_Numeric_Precision (v <= d) + Problems.expect_only_warning Loss_Of_Numeric_Precision (v >= d) + if greater.is_infinite.not then (d < greater) . should_be_true (d <= greater) . should_be_true @@ -501,6 +518,17 @@ add_specs suite_builder = (3 * Decimal.new 4.0) . should_equal 12 (3 / Decimal.new 4.0) . should_equal 0.75 + group_builder.specify "Decimal mixed arithmetic should result in Decimal" <| + (Decimal.new 1 + 2) . should_be_a Decimal + (Decimal.new 1 - 2) . should_be_a Decimal + (Decimal.new 3 * 4) . should_be_a Decimal + (Decimal.new 3 / 4) . should_be_a Decimal + + (1 + Decimal.new 2) . should_be_a Decimal + (1 - Decimal.new 2) . should_be_a Decimal + (3 * Decimal.new 4) . should_be_a Decimal + (3 / Decimal.new 4) . should_be_a Decimal + group_builder.specify "should get an aritmetic error when a result is a nonterminating decimal expansion" <| (Decimal.new 1 / Decimal.new 3) . should_fail_with Arithmetic_Error @@ -518,6 +546,10 @@ add_specs suite_builder = nd2 = -d2 nd2 . should_equal 5 + group_builder.specify "Mixed Decimal/Integer arithmetic should not attach warnings" <| + Problems.assume_no_problems (Decimal.new 1 + 2) + Problems.assume_no_problems (1 + Decimal.new 2) + suite_builder.group "(Decimal_Spec) conversions" group_builder-> group_builder.specify "should convert correctly to and from Integer" <| a = Decimal.new "12000" @@ -587,6 +619,9 @@ add_specs suite_builder = f.should_be_a Float f.should_equal 56.34 + group_builder.specify "should attach a Loss_Of_Numeric_Precision warning when converting decimal to float with .from" <| + Problems.expect_only_warning Loss_Of_Numeric_Precision (Float.from (Decimal.new "56.34")) + group_builder.specify "Decimal.to_float cannot compare correctly with the original Decimal" <| huge_a = Decimal.new "3.4E300" huge_a_float = Float.from huge_a @@ -603,7 +638,7 @@ add_specs suite_builder = group_builder.specify "should attach a warning on loss of precision in integer conversion" <| Problems.expect_only_warning Loss_Of_Numeric_Precision (Decimal.new "125.2" . to_integer) - Problems.not_expect_warning Loss_Of_Numeric_Precision (Decimal.new "125" . to_integer) + Problems.assume_no_problems (Decimal.new "125" . to_integer) huge_int = Decimal.new "125E90" huge_fraction = Decimal.new "125E90" + Decimal.new "0.2" Problems.expect_only_warning Loss_Of_Numeric_Precision (huge_fraction . to_integer) diff --git a/test/Base_Tests/src/Data/Json_Spec.enso b/test/Base_Tests/src/Data/Json_Spec.enso index 2dfd4b1c2b6c..ea4dc6d740a4 100644 --- a/test/Base_Tests/src/Data/Json_Spec.enso +++ b/test/Base_Tests/src/Data/Json_Spec.enso @@ -159,7 +159,7 @@ add_specs suite_builder = large_number.to_json . parse_json . should_equal large_number group_builder.specify "should serialise decimals and parse back" <| - decimal = Decimal.from_string "1234567890123456789012345678901234567890.1234567890123456789012345678901234567890" + decimal = Decimal.from_text "1234567890123456789012345678901234567890.1234567890123456789012345678901234567890" decimal.to_json . should_equal '{"type":"Decimal","value":"1234567890123456789012345678901234567890.1234567890123456789012345678901234567890","scale":40,"precision":80}' decimal.to_json . parse_json . should_equal decimal decimal.to_json . parse_json . scale . should_equal 40