diff --git a/lib/elixir/lib/float.ex b/lib/elixir/lib/float.ex index 3c530b978f1..8a39e301f09 100644 --- a/lib/elixir/lib/float.ex +++ b/lib/elixir/lib/float.ex @@ -167,40 +167,73 @@ defmodule Float do parse_unsigned(binary) end - defp parse_unsigned(<>) when digit in ?0..?9, - do: parse_unsigned(rest, false, false, [digit]) + defp parse_unsigned(<> = 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(<>, dot?, e?, acc) when digit in ?0..?9, - do: parse_unsigned(rest, dot?, e?, [digit | acc]) + defp parse_mantissa(binary, <>, dot?) when digit in ?0..?9, + do: parse_mantissa(binary, rest, dot?) - defp parse_unsigned(<>, false, false, acc) when digit in ?0..?9, - do: parse_unsigned(rest, true, false, [digit, ?. | acc]) + defp parse_mantissa(binary, <>, false) when digit in ?0..?9, + do: parse_mantissa(binary, rest, true) - defp parse_unsigned(<>, dot?, false, acc) + defp parse_mantissa(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(<>, dot?, false, acc) + defp parse_mantissa(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, <>, 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`. diff --git a/lib/elixir/test/elixir/float_test.exs b/lib/elixir/test/elixir/float_test.exs index 607accbd4b4..e43d705002f 100644 --- a/lib/elixir/test/elixir/float_test.exs +++ b/lib/elixir/test/elixir/float_test.exs @@ -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