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 Number.rounding_mode + .{with,save}_rounding_mode #11097

Closed
wants to merge 1 commit into from
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
64 changes: 64 additions & 0 deletions spec/std/number_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,70 @@ describe "Number" do
end
end

describe ".save_rounding_mode" do
it "saves `.rounding_mode` and sets it back on block exit" do
prev_rounding_mode = Number.rounding_mode
begin
Number.rounding_mode = :ties_away
Number.save_rounding_mode do
Number.rounding_mode = :ties_even
Number.rounding_mode.should eq(Number::RoundingMode::TIES_EVEN)
end
Number.rounding_mode.should eq(Number::RoundingMode::TIES_AWAY)
ensure
Number.rounding_mode = prev_rounding_mode
end
end
end

describe ".with_rounding_mode" do
it "sets `.rounding_mode` to the given value and sets it back on block exit" do
prev_rounding_mode = Number.rounding_mode
begin
Number.rounding_mode = :ties_away
Number.with_rounding_mode(:ties_even) do
Number.rounding_mode.should eq(Number::RoundingMode::TIES_EVEN)
Number.rounding_mode = :to_zero
end
Number.rounding_mode.should eq(Number::RoundingMode::TIES_AWAY)
ensure
Number.rounding_mode = prev_rounding_mode
end
end
end

describe ".rounding_mode" do
it "is used as a default :mode argument for #round" do
prev_rounding_mode = Number.rounding_mode
begin
Number.rounding_mode = :to_zero
Number.with_rounding_mode(:ties_even) do
2.5.round.should eq(2.0)
3.5.round.should eq(4.0)
2.25.round(1).should eq(2.2)
3.35.round(1).should eq(3.4)
end
ensure
Number.rounding_mode = prev_rounding_mode
end
end

it "is not used when :mode argument for #round is given" do
prev_rounding_mode = Number.rounding_mode
begin
Number.rounding_mode = :to_zero
Number.with_rounding_mode(:ties_even) do
2.5.round(mode: :ties_away).should eq(3.0)
3.5.round(mode: :ties_away).should eq(4.0)
2.25.round(1, mode: :ties_away).should eq(2.3)
3.35.round(1, mode: :ties_away).should eq(3.4)
end
ensure
Number.rounding_mode = prev_rounding_mode
end
end
end

describe "#round_even" do
-2.5.round_even.should eq -2.0
-1.5.round_even.should eq -2.0
Expand Down
2 changes: 1 addition & 1 deletion src/big/big_decimal.cr
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ struct BigDecimal < Number
BigDecimal.new(mantissa, 0)
end

def round(digits : Number, base = 10, *, mode : RoundingMode = :ties_even) : BigDecimal
def round(digits : Number, base = 10, *, mode : RoundingMode = Number.rounding_mode) : BigDecimal
return self if (base == 10 && @scale <= digits) || zero?

# the following is same as the overload in `Number` except `base.to_f`
Expand Down
103 changes: 76 additions & 27 deletions src/number.cr
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,36 @@ struct Number

alias Primitive = Int::Primitive | Float::Primitive

# Specifies rounding behaviour for numerical operations capable of discarding
# precision.
enum RoundingMode
# Rounds towards the nearest integer. If both neighboring integers are equidistant,
# rounds towards the even neighbor (Banker's rounding).
TIES_EVEN

# Rounds towards the nearest integer. If both neighboring integers are equidistant,
# rounds away from zero.
TIES_AWAY

# Rounds towards zero (truncate).
TO_ZERO

# Rounds towards positive infinity (ceil).
TO_POSITIVE

# Rounds towards negative infinity (floor).
TO_NEGATIVE
end

# Determines what happens when a result must be rounded in order to fit
# in the appropriate number of significant digits when *mode* argument
# is not explicitly given (see `#round`).
#
# The default is `RoundingMode::TIES_EVEN` which rounds to the nearest integer,
# with ties# (fractional value of `0.5`) being rounded to the even neighbor
# (Banker's rounding).
class_property rounding_mode : RoundingMode = :ties_even

# Returns the value zero in the respective type.
#
# ```
Expand Down Expand Up @@ -250,19 +280,60 @@ struct Number
self.class.new(value)
end

# Executes the provided block, preserving the rounding mode:
#
# ```
# Number.rounding_mode = :to_zero
#
# Number.save_rounding_mode do
# Number.rounding_mode = :ties_away
# Number.rounding_mode # => Number::RoundingMode::TIES_AWAY
# end
#
# Number.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 `Number.rounding_mode` inside the block.
#
# ```
# Number.rounding_mode = :to_zero
#
# Number.with_rounding_mode(:ties_away) do
# Number.rounding_mode # => Number::RoundingMode::TIES_AWAY
# end
#
# Number.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 this number to a given precision.
#
# Rounds to the specified number of *digits* after the decimal place,
# (or before if negative), in base *base*.
#
# The rounding *mode* controls the direction of the rounding. The default is
# `RoundingMode::TIES_EVEN` which rounds to the nearest integer, with ties
# (fractional value of `0.5`) being rounded to the even neighbor (Banker's rounding).
# controlled by the `Number.rounding_mode` property.
#
# ```
# -1763.116.round(2) # => -1763.12
# ```
def round(digits : Number, base = 10, *, mode : RoundingMode = :ties_even)
def round(digits : Number, base = 10, *, mode : RoundingMode = Number.rounding_mode)
if digits < 0
multiplier = base.to_f ** digits.abs
shifted = self / multiplier
Expand All @@ -282,33 +353,11 @@ struct Number
self.class.new result
end

# Specifies rounding behaviour for numerical operations capable of discarding
# precision.
enum RoundingMode
# Rounds towards the nearest integer. If both neighboring integers are equidistant,
# rounds towards the even neighbor (Banker's rounding).
TIES_EVEN

# Rounds towards the nearest integer. If both neighboring integers are equidistant,
# rounds away from zero.
TIES_AWAY

# Rounds towards zero (truncate).
TO_ZERO

# Rounds towards positive infinity (ceil).
TO_POSITIVE

# Rounds towards negative infinity (floor).
TO_NEGATIVE
end

# Rounds `self` to an integer value using rounding *mode*.
#
# The rounding *mode* controls the direction of the rounding. The default is
# `RoundingMode::TIES_EVEN` which rounds to the nearest integer, with ties
# (fractional value of `0.5`) being rounded to the even neighbor (Banker's rounding).
def round(mode : RoundingMode = :ties_even) : self
# controlled by the `Number.rounding_mode` property.
def round(mode : RoundingMode = Number.rounding_mode) : self
case mode
in .to_zero?
trunc
Expand Down