Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add BigDecimal#round(digits, mode) overload #7126

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions spec/std/big/big_decimal_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,102 @@ 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))
BigDecimal.new("{{sign.id}}0.00").round(-10)
.should eq(BigDecimal.new(BigInt.zero))
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)))
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
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

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

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"))
Expand Down
111 changes: 111 additions & 0 deletions src/big/big_decimal.cr
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,114 @@ 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:
#
Sija marked this conversation as resolved.
Show resolved Hide resolved
# ```
# 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

# 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`.
#
# ```
# 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
return self if @scale <= digits

n_digits = @value.abs.digits.reverse
negative = @value < 0
scale = @scale.to_i - digits

# 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]
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]?.try(&.odd?) : msd > 5
in .to_positive? then !negative
in .to_negative? then negative
end

value += 1 if round_up
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This addition is incorrect, since value is already the BigDecimal itself, not the mantissa:

BigDecimal.new("123.456789").round(2, mode: :ties_away) # => 124.45

value *= -1 if negative
value *= power_ten_to(digits.abs) if digits < 0
value
end

def <=>(other : Int | Float | BigRational)
self <=> BigDecimal.new(other)
end
Expand Down Expand Up @@ -563,6 +671,7 @@ struct Int
include Comparable(BigDecimal)

# Converts `self` to `BigDecimal`.
#
# ```
# require "big"
# 12123415151254124124.to_big_d
Expand Down Expand Up @@ -599,6 +708,7 @@ struct Float
#
# NOTE: Floats are fundamentally less precise than BigDecimals,
# which makes conversion to them risky.
#
# ```
# require "big"
# 1212341515125412412412421.0.to_big_d
Expand All @@ -623,6 +733,7 @@ end

class String
# Converts `self` to `BigDecimal`.
#
# ```
# require "big"
# "1212341515125412412412421".to_big_d
Expand Down