Skip to content
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
77 changes: 55 additions & 22 deletions lib/elixir/lib/float.ex
Original file line number Diff line number Diff line change
Expand Up @@ -167,40 +167,73 @@ defmodule Float do
parse_unsigned(binary)
end

defp parse_unsigned(<<digit, rest::binary>>) when digit in ?0..?9,
do: parse_unsigned(rest, false, false, [digit])
defp parse_unsigned(<<digit, rest::binary>> = binary) when digit in ?0..?9,
do: parse_mantissa(binary, rest, false)

defp parse_unsigned(binary) when is_binary(binary), do: :error

defp parse_unsigned(<<digit, rest::binary>>, dot?, e?, acc) when digit in ?0..?9,
do: parse_unsigned(rest, dot?, e?, [digit | acc])
defp parse_mantissa(binary, <<digit, rest::binary>>, dot?) when digit in ?0..?9,
do: parse_mantissa(binary, rest, dot?)

defp parse_unsigned(<<?., digit, rest::binary>>, false, false, acc) when digit in ?0..?9,
do: parse_unsigned(rest, true, false, [digit, ?. | acc])
defp parse_mantissa(binary, <<?., digit, rest::binary>>, false) when digit in ?0..?9,
do: parse_mantissa(binary, rest, true)

defp parse_unsigned(<<exp_marker, digit, rest::binary>>, dot?, false, acc)
defp parse_mantissa(binary, <<exp_marker, digit, rest::binary>> = tail, dot?)
when exp_marker in ~c"eE" and digit in ?0..?9,
do: parse_unsigned(rest, true, true, [digit, ?e | add_dot(acc, dot?)])
do: parse_exponent(binary, byte_size(binary) - byte_size(tail), rest, dot?)

defp parse_unsigned(<<exp_marker, sign, digit, rest::binary>>, dot?, false, acc)
defp parse_mantissa(binary, <<exp_marker, sign, digit, rest::binary>> = tail, dot?)
when exp_marker in ~c"eE" and sign in ~c"-+" and digit in ?0..?9,
do: parse_unsigned(rest, true, true, [digit, sign, ?e | add_dot(acc, dot?)])

# :erlang.binary_to_float/1 can raise an ArgumentError if the e exponent is too big. For example,
# "1.0e400". Because of this, we rescue the ArgumentError here and return an error.
defp parse_unsigned(rest, dot?, _, acc) do
acc
|> add_dot(dot?)
|> :lists.reverse()
|> :erlang.list_to_float()
do: parse_exponent(binary, byte_size(binary) - byte_size(tail), rest, dot?)

defp parse_mantissa(binary, rest, dot?), do: finish_mantissa(binary, rest, dot?)

defp parse_exponent(binary, exp_pos, <<digit, rest::binary>>, dot?) when digit in ?0..?9,
do: parse_exponent(binary, exp_pos, rest, dot?)

defp parse_exponent(binary, exp_pos, rest, dot?),
do: finish_exponent(binary, exp_pos, rest, dot?)

defp finish_mantissa(binary, rest, _dot? = true) do
{:erlang.binary_to_float(consumed(binary, rest)), rest}
rescue
ArgumentError -> :error
end

# Bare integer: * 1.0 casts to the nearest float without building a new binary,
# and raises ArithmeticError on overflow (for example a 400-digit integer).
defp finish_mantissa(binary, rest, _dot? = false) do
{:erlang.binary_to_integer(consumed(binary, rest)) * 1.0, rest}
rescue
ArithmeticError -> :error
end

# binary_to_float/1 raises ArgumentError when the exponent is too big, e.g. "1.0e400".
defp finish_exponent(binary, _exp_pos, rest, _dot? = true) do
{:erlang.binary_to_float(consumed(binary, rest)), rest}
rescue
ArgumentError -> :error
end

# No decimal point, so ".0" is spliced in before the exponent (at exp_pos) to
# form a valid float literal.
defp finish_exponent(binary, exp_pos, rest, _dot? = false) do
len = byte_size(binary) - byte_size(rest)

literal =
IO.iodata_to_binary([
:binary.part(binary, 0, exp_pos),
".0",
:binary.part(binary, exp_pos, len - exp_pos)
])

{:erlang.binary_to_float(literal), rest}
rescue
ArgumentError -> :error
else
float -> {float, rest}
end

defp add_dot(acc, true), do: acc
defp add_dot(acc, false), do: [?0, ?. | acc]
defp consumed(binary, ""), do: binary
defp consumed(binary, rest), do: :binary.part(binary, 0, byte_size(binary) - byte_size(rest))

@doc """
Rounds a float to the largest float less than or equal to `number`.
Expand Down
13 changes: 13 additions & 0 deletions lib/elixir/test/elixir/float_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,19 @@ defmodule FloatTest do
assert Float.parse(String.duplicate("9", 310) <> ".0") === :error
assert Float.parse("-" <> String.duplicate("9", 310) <> ".0") === :error
assert Float.parse(String.duplicate("9", 310) <> ".0foo") === :error

# Integer without a decimal point (parsed as an integer and cast to float)
assert Float.parse("123456789012345678901234567890") === {1.2345678901234568e29, ""}
assert Float.parse(String.duplicate("9", 300)) === {1.0e300, ""}
assert Float.parse(String.duplicate("9", 310)) === :error
assert Float.parse("-" <> String.duplicate("9", 310)) === :error
assert Float.parse(String.duplicate("9", 310) <> "foo") === :error

# Exponent notation without a decimal point, followed by trailing characters
assert Float.parse("1e10foo") === {1.0e10, "foo"}
assert Float.parse("8e1xy") === {80.0, "xy"}
assert Float.parse("5E+2q") === {500.0, "q"}
assert Float.parse("8E4 49") === {80000.0, " 49"}
end

test "floor/1" do
Expand Down
Loading