Skip to content

Fix unexpected rounding signs on OPT26- #13365

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

Merged
merged 2 commits into from
Feb 23, 2024
Merged
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
5 changes: 4 additions & 1 deletion .formatter.exs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@
assert_same: 2,

# Errors tests
assert_eval_raise: 3
assert_eval_raise: 3,

# Float tests
float_assert: 1
],
normalize_bitstring_modifiers: false
]
11 changes: 8 additions & 3 deletions lib/elixir/lib/float.ex
Original file line number Diff line number Diff line change
Expand Up @@ -374,8 +374,8 @@ defmodule Float do
case rounding do
:ceil when sign === 0 -> 1 / power_of_10(precision)
:floor when sign === 1 -> -1 / power_of_10(precision)
:ceil when sign === 1 -> -0.0
:half_up when sign === 1 -> -0.0
:ceil when sign === 1 -> minus_zero()
:half_up when sign === 1 -> minus_zero()
_ -> 0.0
end

Expand Down Expand Up @@ -406,7 +406,7 @@ defmodule Float do

cond do
num == 0 and sign == 1 ->
-0.0
minus_zero()

num == 0 ->
0.0
Expand All @@ -422,6 +422,11 @@ defmodule Float do
end
end

# TODO remove once we require Erlang/OTP 27+
# This function tricks the compiler to avoid this bug in previous versions:
# https://github.com/elixir-lang/elixir/blob/main/lib/elixir/lib/float.ex#L408-L412
defp minus_zero, do: -0.0

defp decompose(significant, initial) do
decompose(significant, 1, 0, initial)
end
Expand Down
155 changes: 83 additions & 72 deletions lib/elixir/test/elixir/float_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ defmodule FloatTest do

doctest Float

# TODO remove and replace by assert once we require Erlang/OTP 27+
# We can't easily distinguish between -0.0 and +0.0 on previous version
defmacrop float_assert({:===, _, [left, right]}) do
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I went with this approach because it gives us:

  • useful assertions
  • useful error messages on failure
  • can easily be removed since it is not here to stay

quote do
# note: these are pure functions so no need to use bind_quoted
# we favor a useful error message instead
assert unquote(left) === unquote(right)
assert to_string(unquote(left)) === to_string(unquote(right))
end
end

test "parse/1" do
assert Float.parse("12") === {12.0, ""}
assert Float.parse("-12") === {-12.0, ""}
Expand Down Expand Up @@ -45,153 +56,153 @@ defmodule FloatTest do
end

test "floor/1" do
assert Float.floor(12.524235) === 12.0
assert Float.floor(-12.5) === -13.0
assert Float.floor(-12.524235) === -13.0
assert Float.floor(7.5e3) === 7500.0
assert Float.floor(7.5432e3) === 7543.0
assert Float.floor(7.5e-3) === 0.0
assert Float.floor(-12.32453e4) === -123_246.0
assert Float.floor(-12.32453e-10) === -1.0
assert Float.floor(0.32453e-10) === 0.0
assert Float.floor(-0.32453e-10) === -1.0
assert Float.floor(1.32453e-10) === 0.0
float_assert Float.floor(12.524235) === 12.0
float_assert Float.floor(-12.5) === -13.0
float_assert Float.floor(-12.524235) === -13.0
float_assert Float.floor(7.5e3) === 7500.0
float_assert Float.floor(7.5432e3) === 7543.0
float_assert Float.floor(7.5e-3) === 0.0
float_assert Float.floor(-12.32453e4) === -123_246.0
float_assert Float.floor(-12.32453e-10) === -1.0
float_assert Float.floor(0.32453e-10) === 0.0
float_assert Float.floor(-0.32453e-10) === -1.0
float_assert Float.floor(1.32453e-10) === 0.0
end

describe "floor/2" do
test "with 0.0" do
for precision <- 0..15 do
assert Float.floor(0.0, precision) === 0.0
assert Float.floor(-0.0, precision) === -0.0
float_assert Float.floor(0.0, precision) === 0.0
float_assert Float.floor(-0.0, precision) === -0.0
end
end

test "floor/2 with precision" do
assert Float.floor(12.524235, 0) === 12.0
assert Float.floor(-12.524235, 0) === -13.0
float_assert Float.floor(12.524235, 0) === 12.0
float_assert Float.floor(-12.524235, 0) === -13.0

assert Float.floor(12.52, 2) === 12.51
assert Float.floor(-12.52, 2) === -12.52
float_assert Float.floor(12.52, 2) === 12.51
float_assert Float.floor(-12.52, 2) === -12.52

assert Float.floor(12.524235, 2) === 12.52
assert Float.floor(-12.524235, 3) === -12.525
float_assert Float.floor(12.524235, 2) === 12.52
float_assert Float.floor(-12.524235, 3) === -12.525

assert Float.floor(12.32453e-20, 2) === 0.0
assert Float.floor(-12.32453e-20, 2) === -0.01
float_assert Float.floor(12.32453e-20, 2) === 0.0
float_assert Float.floor(-12.32453e-20, 2) === -0.01

assert_raise ArgumentError, "precision 16 is out of valid range of 0..15", fn ->
Float.floor(1.1, 16)
end
end

test "with subnormal floats" do
assert Float.floor(-5.0e-324, 0) === -1.0
assert Float.floor(-5.0e-324, 1) === -0.1
assert Float.floor(-5.0e-324, 2) === -0.01
assert Float.floor(-5.0e-324, 15) === -0.000000000000001
float_assert Float.floor(-5.0e-324, 0) === -1.0
float_assert Float.floor(-5.0e-324, 1) === -0.1
float_assert Float.floor(-5.0e-324, 2) === -0.01
float_assert Float.floor(-5.0e-324, 15) === -0.000000000000001

for precision <- 0..15 do
assert Float.floor(5.0e-324, precision) === 0.0
float_assert Float.floor(5.0e-324, precision) === 0.0
end
end
end

test "ceil/1" do
assert Float.ceil(12.524235) === 13.0
assert Float.ceil(-12.5) === -12.0
assert Float.ceil(-12.524235) === -12.0
assert Float.ceil(7.5e3) === 7500.0
assert Float.ceil(7.5432e3) === 7544.0
assert Float.ceil(7.5e-3) === 1.0
assert Float.ceil(-12.32453e4) === -123_245.0
assert Float.ceil(-12.32453e-10) === -0.0
assert Float.ceil(0.32453e-10) === 1.0
assert Float.ceil(-0.32453e-10) === -0.0
assert Float.ceil(1.32453e-10) === 1.0
assert Float.ceil(0.0) === 0.0
float_assert Float.ceil(12.524235) === 13.0
float_assert Float.ceil(-12.5) === -12.0
float_assert Float.ceil(-12.524235) === -12.0
float_assert Float.ceil(7.5e3) === 7500.0
float_assert Float.ceil(7.5432e3) === 7544.0
float_assert Float.ceil(7.5e-3) === 1.0
float_assert Float.ceil(-12.32453e4) === -123_245.0
float_assert Float.ceil(-12.32453e-10) === -0.0
float_assert Float.ceil(0.32453e-10) === 1.0
float_assert Float.ceil(-0.32453e-10) === -0.0
float_assert Float.ceil(1.32453e-10) === 1.0
float_assert Float.ceil(0.0) === 0.0
end

describe "ceil/2" do
test "with 0.0" do
for precision <- 0..15 do
assert Float.ceil(0.0, precision) === 0.0
assert Float.ceil(-0.0, precision) === -0.0
float_assert Float.ceil(0.0, precision) === 0.0
float_assert Float.ceil(-0.0, precision) === -0.0
end
end

test "with regular floats" do
assert Float.ceil(12.524235, 0) === 13.0
assert Float.ceil(-12.524235, 0) === -12.0
float_assert Float.ceil(12.524235, 0) === 13.0
float_assert Float.ceil(-12.524235, 0) === -12.0

assert Float.ceil(12.52, 2) === 12.52
assert Float.ceil(-12.52, 2) === -12.51
float_assert Float.ceil(12.52, 2) === 12.52
float_assert Float.ceil(-12.52, 2) === -12.51

assert Float.ceil(12.524235, 2) === 12.53
assert Float.ceil(-12.524235, 3) === -12.524
float_assert Float.ceil(12.524235, 2) === 12.53
float_assert Float.ceil(-12.524235, 3) === -12.524

assert Float.ceil(12.32453e-20, 2) === 0.01
assert Float.ceil(-12.32453e-20, 2) === -0.0
float_assert Float.ceil(12.32453e-20, 2) === 0.01
float_assert Float.ceil(-12.32453e-20, 2) === -0.0

assert Float.ceil(0.0, 2) === 0.0
float_assert Float.ceil(0.0, 2) === 0.0

assert_raise ArgumentError, "precision 16 is out of valid range of 0..15", fn ->
Float.ceil(1.1, 16)
end
end

test "with small floats rounded up to -0.0" do
assert Float.ceil(-0.1, 0) === -0.0
assert Float.ceil(-0.01, 1) === -0.0
float_assert Float.ceil(-0.1, 0) === -0.0
float_assert Float.ceil(-0.01, 1) === -0.0
end

test "with subnormal floats" do
assert Float.ceil(5.0e-324, 0) === 1.0
assert Float.ceil(5.0e-324, 1) === 0.1
assert Float.ceil(5.0e-324, 2) === 0.01
assert Float.ceil(5.0e-324, 15) === 0.000000000000001
float_assert Float.ceil(5.0e-324, 0) === 1.0
float_assert Float.ceil(5.0e-324, 1) === 0.1
float_assert Float.ceil(5.0e-324, 2) === 0.01
float_assert Float.ceil(5.0e-324, 15) === 0.000000000000001

for precision <- 0..15 do
assert Float.ceil(-5.0e-324, precision) === -0.0
float_assert Float.ceil(-5.0e-324, precision) === -0.0
end
end
end

describe "round/2" do
test "with 0.0" do
for precision <- 0..15 do
assert Float.round(0.0, precision) === 0.0
assert Float.round(-0.0, precision) === -0.0
float_assert Float.round(0.0, precision) === 0.0
float_assert Float.round(-0.0, precision) === -0.0
end
end

test "with regular floats" do
assert Float.round(5.5675, 3) === 5.567
assert Float.round(-5.5674, 3) === -5.567
assert Float.round(5.5, 3) === 5.5
assert Float.round(5.5e-10, 10) === 5.0e-10
assert Float.round(5.5e-10, 8) === 0.0
assert Float.round(5.0, 0) === 5.0
float_assert Float.round(5.5675, 3) === 5.567
float_assert Float.round(-5.5674, 3) === -5.567
float_assert Float.round(5.5, 3) === 5.5
float_assert Float.round(5.5e-10, 10) === 5.0e-10
float_assert Float.round(5.5e-10, 8) === 0.0
float_assert Float.round(5.0, 0) === 5.0

assert_raise ArgumentError, "precision 16 is out of valid range of 0..15", fn ->
Float.round(1.1, 16)
end
end

test "with small floats rounded to +0.0 / -0.0" do
assert Float.round(0.01, 0) === 0.0
assert Float.round(0.01, 1) === 0.0
float_assert Float.round(0.01, 0) === 0.0
float_assert Float.round(0.01, 1) === 0.0

assert Float.round(-0.01, 0) === -0.0
assert Float.round(-0.01, 1) === -0.0
float_assert Float.round(-0.01, 0) === -0.0
float_assert Float.round(-0.01, 1) === -0.0

assert Float.round(-0.49999, 0) === -0.0
assert Float.round(-0.049999, 1) === -0.0
float_assert Float.round(-0.49999, 0) === -0.0
float_assert Float.round(-0.049999, 1) === -0.0
end

test "with subnormal floats" do
for precision <- 0..15 do
assert Float.round(5.0e-324, precision) === 0.0
assert Float.round(-5.0e-324, precision) === -0.0
float_assert Float.round(5.0e-324, precision) === 0.0
float_assert Float.round(-5.0e-324, precision) === -0.0
end
end
end
Expand Down