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

Implement rounding mode for Number#round #10413

Merged
Merged
Show file tree
Hide file tree
Changes from 4 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
191 changes: 191 additions & 0 deletions spec/std/number_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,155 @@ describe "Number" do
6543210987654321.0.round(-15).should eq(7000000000000000.0)
end

describe "rounding modes" do
it "to_zero" do
-1.5.round(:to_zero).should eq -1.0
-1.0.round(:to_zero).should eq -1.0
-0.9.round(:to_zero).should eq 0.0
-0.5.round(:to_zero).should eq 0.0
-0.1.round(:to_zero).should eq 0.0
0.0.round(:to_zero).should eq 0.0
0.1.round(:to_zero).should eq 0.0
0.5.round(:to_zero).should eq 0.0
0.9.round(:to_zero).should eq 0.0
1.0.round(:to_zero).should eq 1.0
1.5.round(:to_zero).should eq 1.0
end

it "to_positive" do
-1.5.round(:to_positive).should eq -1.0
-1.0.round(:to_positive).should eq -1.0
-0.9.round(:to_positive).should eq 0.0
-0.5.round(:to_positive).should eq 0.0
-0.1.round(:to_positive).should eq 0.0
0.0.round(:to_positive).should eq 0.0
0.1.round(:to_positive).should eq 1.0
0.5.round(:to_positive).should eq 1.0
0.9.round(:to_positive).should eq 1.0
1.0.round(:to_positive).should eq 1.0
1.5.round(:to_positive).should eq 2.0
end

it "to_negative" do
-1.5.round(:to_negative).should eq -2.0
-1.0.round(:to_negative).should eq -1.0
-0.9.round(:to_negative).should eq -1.0
-0.5.round(:to_negative).should eq -1.0
-0.1.round(:to_negative).should eq -1.0
0.0.round(:to_negative).should eq 0.0
0.1.round(:to_negative).should eq 0.0
0.5.round(:to_negative).should eq 0.0
0.9.round(:to_negative).should eq 0.0
1.0.round(:to_negative).should eq 1.0
1.5.round(:to_negative).should eq 1.0
end

it "ties_even" do
-2.5.round(:ties_even).should eq -2.0
-1.5.round(:ties_even).should eq -2.0
-1.0.round(:ties_even).should eq -1.0
-0.9.round(:ties_even).should eq -1.0
-0.5.round(:ties_even).should eq 0.0
-0.1.round(:ties_even).should eq 0.0
0.0.round(:ties_even).should eq 0.0
0.1.round(:ties_even).should eq 0.0
0.5.round(:ties_even).should eq 0.0
0.9.round(:ties_even).should eq 1.0
1.0.round(:ties_even).should eq 1.0
1.5.round(:ties_even).should eq 2.0
2.5.round(:ties_even).should eq 2.0
end

it "ties_away" do
-2.5.round(:ties_away).should eq -3.0
-1.5.round(:ties_away).should eq -2.0
-1.0.round(:ties_away).should eq -1.0
-0.9.round(:ties_away).should eq -1.0
-0.5.round(:ties_away).should eq -1.0
-0.1.round(:ties_away).should eq 0.0
0.0.round(:ties_away).should eq 0.0
0.1.round(:ties_away).should eq 0.0
0.5.round(:ties_away).should eq 1.0
0.9.round(:ties_away).should eq 1.0
1.0.round(:ties_away).should eq 1.0
1.5.round(:ties_away).should eq 2.0
2.5.round(:ties_away).should eq 3.0
end

it "default (=ties_away)" do
-2.5.round.should eq -3.0
-1.5.round.should eq -2.0
-1.0.round.should eq -1.0
-0.9.round.should eq -1.0
-0.5.round.should eq -1.0
-0.1.round.should eq 0.0
0.0.round.should eq 0.0
0.1.round.should eq 0.0
0.5.round.should eq 1.0
0.9.round.should eq 1.0
1.0.round.should eq 1.0
1.5.round.should eq 2.0
2.5.round.should eq 3.0
end
end

describe "with digits" do
it "to_zero" do
12.345.round(-1, mode: :to_zero).should eq 10
12.345.round(0, mode: :to_zero).should eq 12
12.345.round(1, mode: :to_zero).should eq 12.3
12.345.round(2, mode: :to_zero).should eq 12.34
-12.345.round(-1, mode: :to_zero).should eq -10
-12.345.round(0, mode: :to_zero).should eq -12
-12.345.round(1, mode: :to_zero).should eq -12.3
-12.345.round(2, mode: :to_zero).should eq -12.34
end

it "to_positive" do
12.345.round(-1, mode: :to_positive).should eq 20
12.345.round(0, mode: :to_positive).should eq 13
12.345.round(1, mode: :to_positive).should eq 12.4
12.345.round(2, mode: :to_positive).should eq 12.35
-12.345.round(-1, mode: :to_positive).should eq -10
-12.345.round(0, mode: :to_positive).should eq -12
-12.345.round(1, mode: :to_positive).should eq -12.3
-12.345.round(2, mode: :to_positive).should eq -12.34
end

it "to_negative" do
12.345.round(-1, mode: :to_negative).should eq 10
12.345.round(0, mode: :to_negative).should eq 12
12.345.round(1, mode: :to_negative).should eq 12.3
12.345.round(2, mode: :to_negative).should eq 12.34
-12.345.round(-1, mode: :to_negative).should eq -20
-12.345.round(0, mode: :to_negative).should eq -13
-12.345.round(1, mode: :to_negative).should eq -12.4
-12.345.round(2, mode: :to_negative).should eq -12.35
end

it "ties_away" do
13.825.round(-1, mode: :ties_away).should eq 10
13.825.round(0, mode: :ties_away).should eq 14
13.825.round(1, mode: :ties_away).should eq 13.8
13.825.round(2, mode: :ties_away).should eq 13.83
-13.825.round(-1, mode: :ties_away).should eq -10
-13.825.round(0, mode: :ties_away).should eq -14
-13.825.round(1, mode: :ties_away).should eq -13.8
-13.825.round(2, mode: :ties_away).should eq -13.83
end

it "ties_even" do
15.255.round(-1, mode: :ties_even).should eq 20
15.255.round(0, mode: :ties_even).should eq 15
15.255.round(1, mode: :ties_even).should eq 15.3
15.255.round(2, mode: :ties_even).should eq 15.26
-15.255.round(-1, mode: :ties_even).should eq -20
-15.255.round(0, mode: :ties_even).should eq -15
-15.255.round(1, mode: :ties_even).should eq -15.3
-15.255.round(2, mode: :ties_even).should eq -15.26
end
end

describe "base" do
it "2" do
-1763.116.round(2, base: 2).should eq(-1763.0)
Expand All @@ -132,6 +281,48 @@ describe "Number" do
end
end

describe "#round_even" do
-2.5.round_even.should eq -2.0
-1.5.round_even.should eq -2.0
-1.0.round_even.should eq -1.0
-0.9.round_even.should eq -1.0
-0.5.round_even.should eq -0.0
-0.1.round_even.should eq 0.0
0.0.round_even.should eq 0.0
0.1.round_even.should eq 0.0
0.5.round_even.should eq 0.0
0.9.round_even.should eq 1.0
1.0.round_even.should eq 1.0
1.5.round_even.should eq 2.0
2.5.round_even.should eq 2.0

1.round_even.should eq 1
1.round_even.should be_a(Int32)
1_u8.round_even.should be_a(UInt8)
1_f32.round_even.should be_a(Float32)
end

describe "#round_away" do
-2.5.round_away.should eq -3.0
-1.5.round_away.should eq -2.0
-1.0.round_away.should eq -1.0
-0.9.round_away.should eq -1.0
-0.5.round_away.should eq -1.0
-0.1.round_away.should eq 0.0
0.0.round_away.should eq 0.0
0.1.round_away.should eq 0.0
0.5.round_away.should eq 1.0
0.9.round_away.should eq 1.0
1.0.round_away.should eq 1.0
1.5.round_away.should eq 2.0
2.5.round_away.should eq 3.0

1.round_away.should eq 1
1.round_away.should be_a(Int32)
1_u8.round_away.should be_a(UInt8)
1_f32.round_away.should be_a(Float32)
end

it "gives the absolute value" do
123.abs.should eq(123)
-123.abs.should eq(123)
Expand Down
32 changes: 30 additions & 2 deletions src/float.cr
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,21 @@ struct Float32
LibM.floor_f32(self)
end

def round
# Rounds towards the nearest integer. If both neighboring integers are equidistant,
# rounds towards the even neighbor (Banker's rounding).
def round_even : self
# LLVM 11 introduced llvm.roundeven.* intrinsics which may replace rint in
# the future.
{% if compare_versions(Crystal::LLVM_VERSION, "11.0.0") >= 0 %}
LibM.roundeven_f32(self)
{% else %}
LibM.rint_f32(self)
{% end %}
end

# Rounds towards the nearest integer. If both neighboring integers are equidistant,
# rounds away from zero.
def round_away
LibM.round_f32(self)
end

Expand Down Expand Up @@ -238,7 +252,21 @@ struct Float64
LibM.floor_f64(self)
end

def round
# Rounds towards the nearest integer. If both neighboring integers are equidistant,
# rounds towards the even neighbor (Banker's rounding).
def round_even : self
# LLVM 11 introduced llvm.roundeven.* intrinsics which may replace rint in
# the future.
{% if compare_versions(Crystal::LLVM_VERSION, "11.0.0") >= 0 %}
LibM.roundeven_f64(self)
{% else %}
LibM.rint_f64(self)
{% end %}
end

# Rounds towards the nearest integer. If both neighboring integers are equidistant,
# rounds away from zero.
def round_away
LibM.round_f64(self)
end

Expand Down
14 changes: 12 additions & 2 deletions src/int.cr
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,10 @@ struct Int
self >= 0 ? self : -self
end

def round(mode : RoundingMode) : self
self
end

def ceil
self
end
Expand All @@ -249,11 +253,17 @@ struct Int
self
end

def round
def trunc
self
end

# Returns `self`.
def round_even : self
self
end

def trunc
# Returns `self`.
def round_away
self
end

Expand Down
6 changes: 6 additions & 0 deletions src/math/libm.cr
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ lib LibM
{% end %}
fun round_f32 = "llvm.round.f32"(value : Float32) : Float32
fun round_f64 = "llvm.round.f64"(value : Float64) : Float64
{% if compare_versions(Crystal::LLVM_VERSION, "11.0.0") >= 0 %}
fun roundeven_f32 = "llvm.roundeven.f32"(value : Float32) : Float32
fun roundeven_f64 = "llvm.roundeven.f64"(value : Float64) : Float64
{% end %}
fun rint_f32 = "llvm.rint.f32"(value : Float32) : Float32
fun rint_f64 = "llvm.rint.f64"(value : Float64) : Float64
fun sin_f32 = "llvm.sin.f32"(value : Float32) : Float32
fun sin_f64 = "llvm.sin.f64"(value : Float64) : Float64
fun sqrt_f32 = "llvm.sqrt.f32"(value : Float32) : Float32
Expand Down
71 changes: 64 additions & 7 deletions src/number.cr
Original file line number Diff line number Diff line change
Expand Up @@ -389,19 +389,76 @@ struct Number
self.class.new((x / y).round * y)
end

# Rounds this number to a given precision in decimal *digits*.
# 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_AWAY` which rounds to the nearest integer, with ties
# (fractional value of `0.5`) being rounded away from zero.
#
# ```
# -1763.116.round(2) # => -1763.12
# ```
def round(digits = 0, base = 10)
x = self.to_f
def round(digits : Number = 0, base = 10, mode : RoundingMode = :ties_away)
if digits < 0
multiplier = base.to_f ** digits.abs
shifted = self / multiplier
else
multiplier = base.to_f ** digits
shifted = self * multiplier
end

rounded = shifted.round(mode)

if digits < 0
y = base.to_f ** digits.abs
self.class.new((x / y).round * y)
result = rounded * multiplier
else
y = base.to_f ** digits
self.class.new((x * y).round / y)
result = rounded / multiplier
end

self.class.new result
end

# Specifies rounding behaviour for numerical operations capable of discarding
# precision.
enum RoundingMode
Copy link
Contributor

@Sija Sija Feb 21, 2021

Choose a reason for hiding this comment

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

I'd move it to the beginning of the file for a better discoverability.

Copy link
Member Author

Choose a reason for hiding this comment

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

What difference does it make? I think it should stay in proximity to the methods that use it.

Copy link
Member Author

Choose a reason for hiding this comment

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

We could even consider extracting all features related to rounding into a separate file.

Copy link
Contributor

Choose a reason for hiding this comment

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

We could even consider extracting all features related to rounding into a separate file.

Sounds even better.

Copy link
Member Author

Choose a reason for hiding this comment

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

Let's leave that for a follow-up.

# 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_AWAY` which rounds to the nearest integer, with ties
# (fractional value of `0.5`) being rounded away from zero.
def round(mode : RoundingMode = :ties_away) : self
case mode
in .to_zero?
trunc
in .to_positive?
ceil
in .to_negative?
floor
in .ties_away?
round_away
in .ties_even?
round_even
end
end

Expand Down