Current section
Files
Jump to
Current section
Files
lib/quickbeam/vm/runtime/number.ex
defmodule QuickBEAM.VM.Runtime.Number do
@moduledoc "JS `Number` built-in: prototype methods (`toFixed`, `toString`, etc.) and static properties (`MAX_SAFE_INTEGER`, etc.)."
use QuickBEAM.VM.Builtin
alias QuickBEAM.VM.Runtime
alias QuickBEAM.VM.Runtime.GlobalNumeric
# ── Number.prototype ──
proto "toString" do
to_string_with_radix(unwrap_number(this), args)
end
proto "toFixed" do
to_fixed(unwrap_number(this), args)
end
proto "valueOf" do
unwrap_number(this)
end
proto "toExponential" do
to_exponential(unwrap_number(this), args)
end
proto "toPrecision" do
to_precision(unwrap_number(this), args)
end
defp unwrap_number({:obj, ref}) do
case QuickBEAM.VM.Heap.get_obj(ref, %{}) do
%{"__wrapped_number__" => value} -> value
_ -> :nan
end
end
defp unwrap_number(value), do: value
# ── Number static ──
static "isNaN" do
hd(args) == :nan
end
static "isFinite" do
is_number(hd(args))
end
static "isInteger" do
is_integer(hd(args)) or (is_float(hd(args)) and hd(args) == Float.floor(hd(args)))
end
static "parseInt" do
GlobalNumeric.parse_int(args, nil)
end
static "parseFloat" do
GlobalNumeric.parse_float(args, nil)
end
static_val("NaN", :nan)
static_val("POSITIVE_INFINITY", :infinity)
static_val("NEGATIVE_INFINITY", :neg_infinity)
static_val("MAX_SAFE_INTEGER", 9_007_199_254_740_991)
static_val("MIN_SAFE_INTEGER", -9_007_199_254_740_991)
static_val("EPSILON", 2.220446049250313e-16)
# credo:disable-for-next-line Credo.Check.Readability.LargeNumbers
static_val("MAX_VALUE", 1.7976931348623157e+308)
static_val("MIN_VALUE", 5.0e-324)
# ── toString(radix) ──
defp to_string_with_radix(n, [radix | _]) when is_number(n) do
r = Runtime.to_int(radix)
cond do
r == 10 ->
Runtime.stringify(n)
r >= 2 and r <= 36 and n == trunc(n) ->
Integer.to_string(trunc(n), r) |> String.downcase()
r >= 2 and r <= 36 ->
format_float_with_runtime(n * 1.0, r) || float_to_radix(n * 1.0, r)
true ->
Runtime.stringify(n)
end
end
defp to_string_with_radix(n, _), do: Runtime.stringify(n)
defp format_float_with_runtime(n, radix) do
case QuickBEAM.VM.Heap.get_ctx() do
%{runtime_pid: runtime_pid} when runtime_pid != nil ->
literal = :erlang.float_to_binary(n, [:short])
case QuickBEAM.Runtime.eval(runtime_pid, "(#{literal}).toString(#{radix})") do
{:ok, value} when is_binary(value) -> value
_ -> nil
end
_ ->
nil
end
end
defp float_to_radix(n, radix) do
{sign, n} = if n < 0, do: {"-", -n}, else: {"", n}
int_part = trunc(n)
frac_part = n - int_part
int_str =
if int_part == 0, do: "0", else: Integer.to_string(int_part, radix) |> String.downcase()
if frac_part == 0.0 do
sign <> int_str
else
precision = ceil(53 * :math.log(2) / :math.log(radix))
digits = frac_digits_list(frac_part, radix, precision + 3)
digits = round_and_trim(digits, precision, radix, frac_part)
chars = Enum.map(digits, &String.at("0123456789abcdefghijklmnopqrstuvwxyz", &1))
sign <> int_str <> "." <> Enum.join(chars)
end
end
defp frac_digits_list(_frac, _radix, 0), do: []
defp frac_digits_list(frac, radix, remaining) do
prod = frac * radix
digit = trunc(prod)
rest = prod - digit
if rest == 0.0 do
[digit]
else
[digit | frac_digits_list(rest, radix, remaining - 1)]
end
end
defp round_and_trim(digits, precision, radix, original_frac) do
truncated = Enum.take(digits, precision) |> trim_trailing_zeros()
rounded = round_radix_digits(digits, precision, radix) |> trim_trailing_zeros()
if truncated == rounded do
truncated
else
trunc_rt = digits_to_float_precise(truncated, radix)
round_rt = digits_to_float_precise(rounded, radix)
trunc_exact = trunc_rt == original_frac
round_exact = round_rt == original_frac
cond do
trunc_exact and not round_exact ->
truncated
round_exact and not trunc_exact ->
rounded
true ->
trunc_err = abs(trunc_rt - original_frac)
round_err = abs(round_rt - original_frac)
if round_err < trunc_err, do: rounded, else: truncated
end
end
end
defp digits_to_float_precise(digits, radix) do
{num, denom} =
Enum.reduce(Enum.with_index(digits), {0, 1}, fn {d, i}, {n, _} ->
power = round(:math.pow(radix, i + 1))
{n * radix + d, power}
end)
num / denom
end
defp round_radix_digits(digits, precision, _radix) when length(digits) <= precision do
digits
end
defp round_radix_digits(digits, precision, radix) do
{keep, tail} = Enum.split(digits, precision)
should_round_up =
case tail do
[d | _] when d >= div(radix, 2) + 1 ->
true
[d | rest] when d == div(radix, 2) ->
Enum.any?(rest, &(&1 > 0)) or rem(List.last(keep, 0), 2) == 1
_ ->
false
end
if should_round_up do
propagate_carry(keep, radix)
else
keep
end
end
defp propagate_carry(digits, radix) do
{result, carry} =
digits
|> Enum.reverse()
|> Enum.map_reduce(1, fn d, carry ->
sum = d + carry
{rem(sum, radix), div(sum, radix)}
end)
if carry > 0, do: [carry | result], else: result
end
defp trim_trailing_zeros(digits) do
digits
|> Enum.reverse()
|> Enum.drop_while(&(&1 == 0))
|> Enum.reverse()
end
# ── toFixed(digits) ──
defp to_fixed(:nan, _), do: "NaN"
defp to_fixed(:infinity, _), do: "Infinity"
defp to_fixed(:neg_infinity, _), do: "-Infinity"
defp to_fixed(n, [digits | _]) when is_number(n) do
:erlang.float_to_binary(n * 1.0, decimals: max(0, Runtime.to_int(digits)))
end
defp to_fixed(n, _), do: Runtime.stringify(n)
# ── toExponential(digits) ──
defp to_exponential(n, [digits | _]) when is_number(n) do
d = Runtime.to_int(digits)
f = js_round_significant(abs(n * 1.0), d + 1)
sign = if n < 0, do: "-", else: ""
sign <> (:erlang.float_to_binary(f, [{:scientific, d}]) |> strip_exponent_zeros())
end
defp to_exponential(n, _), do: Runtime.stringify(n)
defp strip_exponent_zeros(s) do
case String.split(s, "e") do
[mantissa, exp_str] -> mantissa <> "e" <> format_exponent(String.to_integer(exp_str))
_ -> s
end
end
# ── toPrecision(precision) ──
defp to_precision(n, [prec | _]) when is_number(n) do
p = max(1, Runtime.to_int(prec))
f = n * 1.0
if f == 0.0 do
zero_precision(n < 0, p)
else
format_precision(f, p)
end
end
defp to_precision(n, _), do: Runtime.stringify(n)
defp zero_precision(negative?, p) do
prefix = if negative?, do: "-", else: ""
prefix <> "0" <> if(p > 1, do: "." <> String.duplicate("0", p - 1), else: "")
end
defp format_precision(f, p) do
exp = trunc(:math.floor(:math.log10(abs(f))))
sign = if f < 0, do: "-", else: ""
f = js_round_significant(abs(f), p)
if exp >= p or exp < -6 do
sci = :erlang.float_to_binary(f, [{:scientific, p - 1}])
case String.split(sci, "e") do
[mantissa, exp_str] ->
sign <> mantissa <> "e" <> format_exponent(String.to_integer(exp_str))
_ ->
Runtime.stringify(f)
end
else
sign <> :erlang.float_to_binary(f, decimals: p - exp - 1)
end
end
defp js_round_significant(f, p) do
if f == 0.0, do: 0.0, else: do_js_round_sig(f, p)
end
defp do_js_round_sig(f, p) do
exp = :math.floor(:math.log10(f))
factor = :math.pow(10, p - 1 - exp)
scaled = f * factor
rounded = :erlang.trunc(scaled + 0.5)
rounded / factor
end
defp format_exponent(exp) when exp >= 0, do: "+" <> Integer.to_string(exp)
defp format_exponent(exp), do: Integer.to_string(exp)
end