From 120ef06a44fec470e36c93e0c9dc328c8fac1c44 Mon Sep 17 00:00:00 2001 From: Sijawusz Pur Rahnama Date: Fri, 19 Mar 2021 13:20:47 +0100 Subject: [PATCH 1/5] Add BigDecimal#round(digits, mode) overload --- spec/std/big/big_decimal_spec.cr | 42 ++++++++++++++++++++++++++++++++ src/big/big_decimal.cr | 40 ++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/spec/std/big/big_decimal_spec.cr b/spec/std/big/big_decimal_spec.cr index 26d35657c00c..409804907c13 100644 --- a/spec/std/big/big_decimal_spec.cr +++ b/spec/std/big/big_decimal_spec.cr @@ -193,6 +193,48 @@ describe BigDecimal do (-BigDecimal.new(3)).should eq(BigDecimal.new(-3)) end + describe "#round" do + {% for sign in %w[+ -] %} + context "(with {{sign.id}} sign)" do + it "returns self if digits are equal or more than significant digits" do + BigDecimal.new("{{sign.id}}2.5").round(1) + .should eq(BigDecimal.new(BigInt.new({{sign.id}}25), 1)) + BigDecimal.new("{{sign.id}}000.5").round(1) + .should eq(BigDecimal.new(BigInt.new({{sign.id}}5), 1)) + BigDecimal.new("{{sign.id}}2.00").round(10) + .should eq(BigDecimal.new(BigInt.new({{sign.id}}20), 1)) + end + + it "rounds :to_zero" do + BigDecimal.new("{{sign.id}}979797799999666.9").round(0, mode: :to_zero) + .should eq(BigDecimal.new(BigInt.new({{sign.id}}979797799999666))) + end + + it "rounds :ties_away" do + BigDecimal.new("{{sign.id}}979797799999666.5").round(0, mode: :ties_away) + .should eq(BigDecimal.new(BigInt.new({{sign.id}}979797799999667))) + end + + it "rounds :ties_even" do + BigDecimal.new("{{sign.id}}979797799999666.5").round(0, mode: :ties_even) + .should eq(BigDecimal.new(BigInt.new({{sign.id}}979797799999666))) + BigDecimal.new("{{sign.id}}979797799999666.5").round(0, mode: :ties_even) + .should eq(BigDecimal.new(BigInt.new({{sign.id}}979797799999666))) + end + + it "rounds :to_positive" do + BigDecimal.new("{{sign.id}}2.5").round(0, mode: :to_positive) + .should eq(BigDecimal.new(BigInt.new({{sign.id}}{{sign == "+" ? 3 : 2}}))) + end + + it "rounds :to_negative" do + BigDecimal.new("{{sign.id}}2.5").round(0, mode: :to_negative) + .should eq(BigDecimal.new(BigInt.new({{sign.id}}{{sign == "-" ? 3 : 2}}))) + end + end + {% end %} + end + it "performs arithmetic with other number types" do (1.to_big_d + 2).should eq(BigDecimal.new("3.0")) (2 + 1.to_big_d).should eq(BigDecimal.new("3.0")) diff --git a/src/big/big_decimal.cr b/src/big/big_decimal.cr index a98c5c7b37c1..a98173dd947f 100644 --- a/src/big/big_decimal.cr +++ b/src/big/big_decimal.cr @@ -237,6 +237,46 @@ struct BigDecimal < Number end end + # Rounds to the nearest integer (by default), returning the result as a `BigDecimal`. + # + # ``` + # BigDecimal.new("3.14159").round # => 3 + # BigDecimal.new("8.7").round # => 9 + # BigDecimal.new("-9.9").round # => -10 + # ``` + # + # If *digits* is specified and positive, the fractional part of the result + # has no more than that many digits. + # + # The value of the optional *mode* argument can be used to determine how + # rounding is performed. + def round(digits : Int = 0, *, mode : RoundingMode = :ties_even) : BigDecimal + return self if @scale <= digits + + n_digits = @value.abs.digits.reverse + negative = @value < 0 + scale = @scale.to_i - digits + + value = n_digits[0...-scale].join.to_big_i + value = self.class.new(value, digits) + value *= -1 if negative + + msd = n_digits[-scale] + return value if msd.zero? + + round_up = + case mode + in .to_zero? then false + in .ties_away? then msd >= 5 + in .ties_even? then msd == 5 ? n_digits[-2].odd? : msd > 5 + in .to_positive? then !negative + in .to_negative? then negative + end + + value += (negative ? -1 : 1) if round_up + value + end + def <=>(other : Int | Float | BigRational) self <=> BigDecimal.new(other) end From 8bd6d32a14b59fc3ae3b49f7f1811aed1e8ec433 Mon Sep 17 00:00:00 2001 From: Sijawusz Pur Rahnama Date: Fri, 19 Mar 2021 13:21:52 +0100 Subject: [PATCH 2/5] Add BigDecimal.(save_)rounding_mode --- spec/std/big/big_decimal_spec.cr | 36 ++++++++++++++++++++++++++++++++ src/big/big_decimal.cr | 29 ++++++++++++++++++++++++- 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/spec/std/big/big_decimal_spec.cr b/spec/std/big/big_decimal_spec.cr index 409804907c13..1ad918f77efb 100644 --- a/spec/std/big/big_decimal_spec.cr +++ b/spec/std/big/big_decimal_spec.cr @@ -235,6 +235,42 @@ describe BigDecimal do {% end %} end + describe ".rounding_mode" do + it "defaults to :ties_even" do + BigDecimal.rounding_mode.should eq(Number::RoundingMode::TIES_EVEN) + end + + it "allows to be set" do + begin + BigDecimal.rounding_mode = :to_zero + BigDecimal.rounding_mode.should eq(Number::RoundingMode::TO_ZERO) + ensure + BigDecimal.rounding_mode = nil + end + end + + it "acts as default value for mode argument in #round" do + BigDecimal.rounding_mode = :to_positive + BigDecimal.new("2.5").round.should eq(BigDecimal.new(BigInt.new(3))) + BigDecimal.rounding_mode = nil + BigDecimal.new("2.5").round.should eq(BigDecimal.new(BigInt.new(2))) + end + end + + describe ".save_rounding_mode" do + it "properly retains and resets rounding mode state" do + begin + BigDecimal.rounding_mode = :to_zero + BigDecimal.save_rounding_mode do + BigDecimal.rounding_mode = :ties_even + end + BigDecimal.rounding_mode.should eq(Number::RoundingMode::TO_ZERO) + ensure + BigDecimal.rounding_mode = nil + end + end + end + it "performs arithmetic with other number types" do (1.to_big_d + 2).should eq(BigDecimal.new("3.0")) (2 + 1.to_big_d).should eq(BigDecimal.new("3.0")) diff --git a/src/big/big_decimal.cr b/src/big/big_decimal.cr index a98173dd947f..1784c32b5e20 100644 --- a/src/big/big_decimal.cr +++ b/src/big/big_decimal.cr @@ -237,6 +237,33 @@ struct BigDecimal < Number end end + # Determines what happens when a result must be rounded in order to fit + # in the appropriate number of significant digits. + # + # NOTE: Defaults to `RoundingMode::TIES_EVEN` if `nil`. + class_property rounding_mode : RoundingMode? { RoundingMode::TIES_EVEN } + + # Executes the provided block, preserving the rounding mode: + # + # ``` + # BigDecimal.rounding_mode = :to_zero + # + # BigDecimal.save_rounding_mode do + # BigDecimal.rounding_mode = :ties_away + # BigDecimal.rounding_mode # => Number::RoundingMode::TIES_AWAY + # end + # + # BigDecimal.rounding_mode # => Number::RoundingMode::TO_ZERO + # ``` + def self.save_rounding_mode + prev_rounding_mode = @@rounding_mode + begin + yield + ensure + @@rounding_mode = prev_rounding_mode + end + end + # Rounds to the nearest integer (by default), returning the result as a `BigDecimal`. # # ``` @@ -250,7 +277,7 @@ struct BigDecimal < Number # # The value of the optional *mode* argument can be used to determine how # rounding is performed. - def round(digits : Int = 0, *, mode : RoundingMode = :ties_even) : BigDecimal + def round(digits : Int = 0, *, mode : RoundingMode = BigDecimal.rounding_mode) : BigDecimal return self if @scale <= digits n_digits = @value.abs.digits.reverse From e1245835f4b98abea7b1cdce4907247eb4bae14d Mon Sep 17 00:00:00 2001 From: Sijawusz Pur Rahnama Date: Thu, 29 Nov 2018 20:53:33 +0100 Subject: [PATCH 3/5] Add BigDecimal.with_rounding_mode --- spec/std/big/big_decimal_spec.cr | 14 ++++++++++++++ src/big/big_decimal.cr | 22 ++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/spec/std/big/big_decimal_spec.cr b/spec/std/big/big_decimal_spec.cr index 1ad918f77efb..c352a782026b 100644 --- a/spec/std/big/big_decimal_spec.cr +++ b/spec/std/big/big_decimal_spec.cr @@ -271,6 +271,20 @@ describe BigDecimal do end end + describe ".with_rounding_mode" do + it "sets given mode as .rounding_mode within yielded block" do + begin + BigDecimal.rounding_mode = :to_zero + BigDecimal.with_rounding_mode(:ties_even) do + BigDecimal.rounding_mode.should eq(Number::RoundingMode::TIES_EVEN) + end + BigDecimal.rounding_mode.should eq(Number::RoundingMode::TO_ZERO) + ensure + BigDecimal.rounding_mode = nil + end + end + end + it "performs arithmetic with other number types" do (1.to_big_d + 2).should eq(BigDecimal.new("3.0")) (2 + 1.to_big_d).should eq(BigDecimal.new("3.0")) diff --git a/src/big/big_decimal.cr b/src/big/big_decimal.cr index 1784c32b5e20..18bca8163f33 100644 --- a/src/big/big_decimal.cr +++ b/src/big/big_decimal.cr @@ -264,6 +264,28 @@ struct BigDecimal < Number end end + # Executes the provided block, preserving the rounding mode; *mode* argument + # is set as `BigDecimal.rounding_mode` inside the block. + # + # ``` + # BigDecimal.rounding_mode = :to_zero + # + # # *mode* argument is set as `BigDecimal.rounding_mode` inside the block + # BigDecimal.with_rounding_mode(:ties_away) do + # BigDecimal.rounding_mode # => Number::RoundingMode::TIES_AWAY + # end + # + # BigDecimal.rounding_mode # => Number::RoundingMode::TO_ZERO + # ``` + # + # NOTE: Uses `save_rounding_mode` internally. + def self.with_rounding_mode(mode : RoundingMode?) + save_rounding_mode do + @@rounding_mode = mode + yield + end + end + # Rounds to the nearest integer (by default), returning the result as a `BigDecimal`. # # ``` From 34483fe8988c3b09c032e1d2f050e3025ba39e01 Mon Sep 17 00:00:00 2001 From: Sijawusz Pur Rahnama Date: Fri, 19 Mar 2021 13:22:51 +0100 Subject: [PATCH 4/5] BigDecimal docs tweak --- src/big/big_decimal.cr | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/big/big_decimal.cr b/src/big/big_decimal.cr index 18bca8163f33..c718b01effbd 100644 --- a/src/big/big_decimal.cr +++ b/src/big/big_decimal.cr @@ -289,9 +289,9 @@ struct BigDecimal < Number # Rounds to the nearest integer (by default), returning the result as a `BigDecimal`. # # ``` - # BigDecimal.new("3.14159").round # => 3 - # BigDecimal.new("8.7").round # => 9 - # BigDecimal.new("-9.9").round # => -10 + # BigDecimal.new("3.14159").round(2) # => 3.14 + # BigDecimal.new("8.7").round # => 9 + # BigDecimal.new("-9.9").round # => -10 # ``` # # If *digits* is specified and positive, the fractional part of the result @@ -652,6 +652,7 @@ struct Int include Comparable(BigDecimal) # Converts `self` to `BigDecimal`. + # # ``` # require "big" # 12123415151254124124.to_big_d @@ -688,6 +689,7 @@ struct Float # # NOTE: Floats are fundamentally less precise than BigDecimals, # which makes conversion to them risky. + # # ``` # require "big" # 1212341515125412412412421.0.to_big_d @@ -712,6 +714,7 @@ end class String # Converts `self` to `BigDecimal`. + # # ``` # require "big" # "1212341515125412412412421".to_big_d From e942306f6e4e74021103138fcb7c9d2652e79f8c Mon Sep 17 00:00:00 2001 From: Sijawusz Pur Rahnama Date: Fri, 19 Mar 2021 13:27:02 +0100 Subject: [PATCH 5/5] Add support for negative *digits* value in BigDecimal#round --- spec/std/big/big_decimal_spec.cr | 4 ++++ src/big/big_decimal.cr | 37 ++++++++++++++++++++++++-------- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/spec/std/big/big_decimal_spec.cr b/spec/std/big/big_decimal_spec.cr index c352a782026b..3929e07530fb 100644 --- a/spec/std/big/big_decimal_spec.cr +++ b/spec/std/big/big_decimal_spec.cr @@ -203,6 +203,8 @@ describe BigDecimal do .should eq(BigDecimal.new(BigInt.new({{sign.id}}5), 1)) BigDecimal.new("{{sign.id}}2.00").round(10) .should eq(BigDecimal.new(BigInt.new({{sign.id}}20), 1)) + BigDecimal.new("{{sign.id}}0.00").round(-10) + .should eq(BigDecimal.new(BigInt.zero)) end it "rounds :to_zero" do @@ -220,6 +222,8 @@ describe BigDecimal do .should eq(BigDecimal.new(BigInt.new({{sign.id}}979797799999666))) BigDecimal.new("{{sign.id}}979797799999666.5").round(0, mode: :ties_even) .should eq(BigDecimal.new(BigInt.new({{sign.id}}979797799999666))) + BigDecimal.new("{{sign.id}}123456078.999").round(-3, mode: :ties_even) + .should eq(BigDecimal.new(BigInt.new({{sign.id}}123456000))) end it "rounds :to_positive" do diff --git a/src/big/big_decimal.cr b/src/big/big_decimal.cr index c718b01effbd..d6fae227d2d1 100644 --- a/src/big/big_decimal.cr +++ b/src/big/big_decimal.cr @@ -289,14 +289,18 @@ struct BigDecimal < Number # Rounds to the nearest integer (by default), returning the result as a `BigDecimal`. # # ``` - # BigDecimal.new("3.14159").round(2) # => 3.14 - # BigDecimal.new("8.7").round # => 9 - # BigDecimal.new("-9.9").round # => -10 + # BigDecimal.new("3.14159").round(2) # => 3.14 + # BigDecimal.new("8.7").round # => 9 + # BigDecimal.new("-9.9").round # => -10 + # BigDecimal.new("13345.234").round(-2) # => 13300 # ``` # # If *digits* is specified and positive, the fractional part of the result # has no more than that many digits. # + # If *digits* is specified and negative, at least that many digits to the left of + # the decimal point will be `0` in the result. + # # The value of the optional *mode* argument can be used to determine how # rounding is performed. def round(digits : Int = 0, *, mode : RoundingMode = BigDecimal.rounding_mode) : BigDecimal @@ -306,23 +310,38 @@ struct BigDecimal < Number negative = @value < 0 scale = @scale.to_i - digits - value = n_digits[0...-scale].join.to_big_i - value = self.class.new(value, digits) - value *= -1 if negative + # 123.456.to_big_d.round(-10) # => 0.1e11 + if digits < 0 && n_digits.size <= scale + value = self.class.new(n_digits.first, 1) + value *= -1 if negative + value = value.round(0, mode: mode) + value *= power_ten_to(digits.abs) + return value + end + value = n_digits[0...-scale].join.presence.try(&.to_big_i) || 0 + value = self.class.new(value, digits < 0 ? 0 : digits) + + # 10001.123.to_big_d.round(-3) # => 10000 msd = n_digits[-scale] - return value if msd.zero? + if msd == 0 + value *= -1 if negative + value *= power_ten_to(digits.abs) if digits < 0 + return value + end round_up = case mode in .to_zero? then false in .ties_away? then msd >= 5 - in .ties_even? then msd == 5 ? n_digits[-2].odd? : msd > 5 + in .ties_even? then msd == 5 ? n_digits[-2]?.try(&.odd?) : msd > 5 in .to_positive? then !negative in .to_negative? then negative end - value += (negative ? -1 : 1) if round_up + value += 1 if round_up + value *= -1 if negative + value *= power_ten_to(digits.abs) if digits < 0 value end