From aa3dae5e11a17f419e3f6dab899542219b756698 Mon Sep 17 00:00:00 2001 From: "karlo.smid" Date: Thu, 2 May 2024 13:02:19 +0200 Subject: [PATCH 1/4] feat: check dbl_min_max on Decimal.new method --- lib/decimal.ex | 62 ++++++++++++++++++++++++++++++++++++------ lib/decimal/context.ex | 2 +- test/decimal_test.exs | 42 +++++++++++++++++++++++++++- 3 files changed, 96 insertions(+), 10 deletions(-) diff --git a/lib/decimal.ex b/lib/decimal.ex index dfbb67d..93c930a 100644 --- a/lib/decimal.ex +++ b/lib/decimal.ex @@ -1257,20 +1257,32 @@ defmodule Decimal do iex> Decimal.new("3.14") Decimal.new("3.14") + iex> Decimal.new("1.79769313486231581e308") + ** (Decimal.Error) : number bigger than DBL_MAX: Decimal.new("1.79769313486231581E+308") + + iex> Decimal.new("2.22507385850720139e-308") + ** (Decimal.Error) : number smaller than DBL_MIN: Decimal.new("2.22507385850720139E-308") + """ @spec new(decimal) :: t def new(%Decimal{sign: sign, coef: coef, exp: exp} = num) when sign in [1, -1] and ((is_integer(coef) and coef >= 0) or coef in [:NaN, :inf]) and - is_integer(exp), - do: num + is_integer(exp) do + check_dbl_min_max(num) + end - def new(int) when is_integer(int), - do: %Decimal{sign: if(int < 0, do: -1, else: 1), coef: Kernel.abs(int)} + def new(int) when is_integer(int) do + num = %Decimal{sign: if(int < 0, do: -1, else: 1), coef: Kernel.abs(int)} + check_dbl_min_max(num) + end def new(binary) when is_binary(binary) do case parse(binary) do - {decimal, ""} -> decimal - _ -> raise Error, reason: "number parsing syntax: #{inspect(binary)}" + {decimal, ""} -> + check_dbl_min_max(decimal) + + _ -> + raise Error, reason: "number parsing syntax: #{inspect(binary)}" end end @@ -1290,8 +1302,10 @@ defmodule Decimal do @spec new(sign :: 1 | -1, coef :: non_neg_integer | :NaN | :inf, exp :: integer) :: t def new(sign, coef, exp) when sign in [1, -1] and ((is_integer(coef) and coef >= 0) or coef in [:NaN, :inf]) and - is_integer(exp), - do: %Decimal{sign: sign, coef: coef, exp: exp} + is_integer(exp) do + num = %Decimal{sign: sign, coef: coef, exp: exp} + check_dbl_min_max(num) + end @doc """ Creates a new decimal number from a floating point number. @@ -2014,6 +2028,38 @@ defmodule Decimal do defp fix_float_exp([], result), do: :lists.reverse(result) + defp check_dbl_min_max(%Decimal{coef: :inf} = infinity), do: infinity + + defp check_dbl_min_max(%Decimal{sign: 1} = num) do + cond do + Decimal.gt?(num, dbl_max(1)) -> + raise Error, reason: "number bigger than DBL_MAX: #{inspect(num)}" + + Decimal.gt?(num, zero(1)) and Decimal.lt?(num, dbl_min(1)) -> + raise Error, reason: "number smaller than DBL_MIN: #{inspect(num)}" + + true -> + num + end + end + + defp check_dbl_min_max(num) do + cond do + Decimal.lt?(num, dbl_max(-1)) -> + raise Error, reason: "negative number smaller than DBL_MAX: #{inspect(num)}" + + Decimal.lt?(num, zero(-1)) and Decimal.gt?(num, dbl_min(-1)) -> + raise Error, reason: "negative number bigger than DBL_MIN: #{inspect(num)}" + + true -> + num + end + end + + def dbl_min(sign), do: %Decimal{sign: sign, coef: 22_250_738_585_072_014, exp: -324} + def zero(sign), do: %Decimal{sign: sign, coef: 0, exp: 0} + def dbl_max(sign), do: %Decimal{sign: sign, coef: 17_976_931_348_623_158, exp: 292} + if Version.compare(System.version(), "1.3.0") == :lt do defp integer_to_charlist(string), do: Integer.to_char_list(string) else diff --git a/lib/decimal/context.ex b/lib/decimal/context.ex index 0201eb2..c1c85be 100644 --- a/lib/decimal/context.ex +++ b/lib/decimal/context.ex @@ -84,7 +84,7 @@ defmodule Decimal.Context do Runs function with given context. """ doc_since("1.9.0") - @spec with(t(), (() -> x)) :: x when x: var + @spec with(t(), (-> x)) :: x when x: var def with(%Context{} = context, fun) when is_function(fun, 0) do old = Process.put(@context_key, context) diff --git a/test/decimal_test.exs b/test/decimal_test.exs index 1651bc6..b575e91 100644 --- a/test/decimal_test.exs +++ b/test/decimal_test.exs @@ -248,7 +248,9 @@ defmodule DecimalTest do assert Decimal.compare("Inf", "Inf") == :eq - assert Decimal.compare(~d"5e10000000000", ~d"0") == :gt + assert_raise Decimal.Error, + ": number bigger than DBL_MAX: Decimal.new(\"5E+10000000000\")", + fn -> Decimal.compare(~d"5e10000000000", ~d"0") end assert_raise Error, fn -> Decimal.compare(~d"nan", ~d"0") @@ -906,4 +908,42 @@ defmodule DecimalTest do Decimal.sqrt(Decimal.new(d(3, 1, -1))) end end + + test "max_min_dbl" do + assert Decimal.new("1.7976931348623158e308") == Decimal.dbl_max(1) + assert Decimal.new("-1.7976931348623158e308") == Decimal.dbl_max(-1) + + assert_raise Decimal.Error, + ": number bigger than DBL_MAX: Decimal.new(\"1.79769313486231581E+308\")", + fn -> Decimal.new("1.79769313486231581e308") end + + assert_raise Decimal.Error, + ": negative number smaller than DBL_MAX: Decimal.new(\"-1.79769313486231581E+308\")", + fn -> Decimal.new("-1.79769313486231581e308") end + + assert Decimal.new("1.79769313486231579e308") == Decimal.new("1.79769313486231579E+308") + assert Decimal.new("-1.79769313486231579e308") == Decimal.new("-1.79769313486231579E+308") + + assert Decimal.new("2.2250738585072014e-308") == Decimal.dbl_min(1) + assert Decimal.new("-2.2250738585072014e-308") == Decimal.dbl_min(-1) + + assert_raise Decimal.Error, + ": number smaller than DBL_MIN: Decimal.new(\"2.22507385850720139E-308\")", + fn -> Decimal.new("2.22507385850720139e-308") end + + assert_raise Decimal.Error, + ": negative number bigger than DBL_MIN: Decimal.new(\"-2.22507385850720139E-308\")", + fn -> Decimal.new("-2.22507385850720139e-308") end + + assert Decimal.new("2.22507385850720141e-308") == Decimal.new("2.22507385850720141E-308") + assert Decimal.new("-2.22507385850720141e-308") == Decimal.new("-2.22507385850720141E-308") + + assert_raise Decimal.Error, + ": number bigger than DBL_MAX: Decimal.new(\"9.999999999999999999E+1000000000000000000000017\")", + fn -> Decimal.new("9999999999999999999e999999999999999999999999") end + + assert_raise Decimal.Error, + ": number smaller than DBL_MIN: Decimal.new(\"9.9999999999999E-999999999999999999999986\")", + fn -> Decimal.new("99999999999999e-999999999999999999999999") end + end end From e3031fe4982e58999c7d0f4cc562a60f79d9070a Mon Sep 17 00:00:00 2001 From: "karlo.smid" Date: Fri, 10 May 2024 07:55:27 +0200 Subject: [PATCH 2/4] refactor: check dbl_min_max only on to_float function --- lib/decimal.ex | 51 ++++++++++++++++++++++++------------------- test/decimal_test.exs | 42 ++++++++++++++++++++--------------- test/test_helper.exs | 18 +++++++++++++++ 3 files changed, 71 insertions(+), 40 deletions(-) diff --git a/lib/decimal.ex b/lib/decimal.ex index 93c930a..5ec34da 100644 --- a/lib/decimal.ex +++ b/lib/decimal.ex @@ -1258,31 +1258,25 @@ defmodule Decimal do Decimal.new("3.14") iex> Decimal.new("1.79769313486231581e308") - ** (Decimal.Error) : number bigger than DBL_MAX: Decimal.new("1.79769313486231581E+308") + Decimal.new("1.79769313486231581e308") iex> Decimal.new("2.22507385850720139e-308") - ** (Decimal.Error) : number smaller than DBL_MIN: Decimal.new("2.22507385850720139E-308") + Decimal.new("2.22507385850720139e-308") """ @spec new(decimal) :: t def new(%Decimal{sign: sign, coef: coef, exp: exp} = num) when sign in [1, -1] and ((is_integer(coef) and coef >= 0) or coef in [:NaN, :inf]) and - is_integer(exp) do - check_dbl_min_max(num) - end + is_integer(exp), + do: num - def new(int) when is_integer(int) do - num = %Decimal{sign: if(int < 0, do: -1, else: 1), coef: Kernel.abs(int)} - check_dbl_min_max(num) - end + def new(int) when is_integer(int), + do: %Decimal{sign: if(int < 0, do: -1, else: 1), coef: Kernel.abs(int)} def new(binary) when is_binary(binary) do case parse(binary) do - {decimal, ""} -> - check_dbl_min_max(decimal) - - _ -> - raise Error, reason: "number parsing syntax: #{inspect(binary)}" + {decimal, ""} -> decimal + _ -> raise Error, reason: "number parsing syntax: #{inspect(binary)}" end end @@ -1302,10 +1296,8 @@ defmodule Decimal do @spec new(sign :: 1 | -1, coef :: non_neg_integer | :NaN | :inf, exp :: integer) :: t def new(sign, coef, exp) when sign in [1, -1] and ((is_integer(coef) and coef >= 0) or coef in [:NaN, :inf]) and - is_integer(exp) do - num = %Decimal{sign: sign, coef: coef, exp: exp} - check_dbl_min_max(num) - end + is_integer(exp), + do: %Decimal{sign: sign, coef: coef, exp: exp} @doc """ Creates a new decimal number from a floating point number. @@ -1574,10 +1566,25 @@ defmodule Decimal do iex> Decimal.to_float(Decimal.new("1.5")) 1.5 + iex> Decimal.to_float(Decimal.new("-1.79769313486231581e308")) + ** (Decimal.Error) : negative number smaller than DBL_MAX: Decimal.new("-1.79769313486231581E+308") + + + iex> Decimal.to_float(Decimal.new("-1.79769313486231581e308")) + ** (Decimal.Error) : negative number smaller than DBL_MAX: Decimal.new("-1.79769313486231581E+308") + + + iex> Decimal.to_float(Decimal.new("2.22507385850720139e-308")) + ** (Decimal.Error) : number smaller than DBL_MIN: Decimal.new("2.22507385850720139E-308") + + + iex> Decimal.to_float(Decimal.new("-2.22507385850720139e-308")) + ** (Decimal.Error): negative number bigger than DBL_MIN: Decimal.new(\"-2.22507385850720139E-308\") """ @spec to_float(t) :: float - def to_float(%Decimal{sign: sign, coef: coef, exp: exp}) when is_integer(coef) do + def to_float(%Decimal{coef: coef} = decimal) when is_integer(coef) do + %Decimal{sign: sign, coef: coef, exp: exp} = check_dbl_min_max(decimal) # Convert back to float without loss # http://www.exploringbinary.com/correct-decimal-to-floating-point-using-big-integers/ {num, den} = ratio(coef, exp) @@ -2056,9 +2063,9 @@ defmodule Decimal do end end - def dbl_min(sign), do: %Decimal{sign: sign, coef: 22_250_738_585_072_014, exp: -324} - def zero(sign), do: %Decimal{sign: sign, coef: 0, exp: 0} - def dbl_max(sign), do: %Decimal{sign: sign, coef: 17_976_931_348_623_158, exp: 292} + defp dbl_min(sign), do: %Decimal{sign: sign, coef: 22_250_738_585_072_014, exp: -324} + defp zero(sign), do: %Decimal{sign: sign, coef: 0, exp: 0} + defp dbl_max(sign), do: %Decimal{sign: sign, coef: 17_976_931_348_623_158, exp: 292} if Version.compare(System.version(), "1.3.0") == :lt do defp integer_to_charlist(string), do: Integer.to_char_list(string) diff --git a/test/decimal_test.exs b/test/decimal_test.exs index b575e91..32fc1e2 100644 --- a/test/decimal_test.exs +++ b/test/decimal_test.exs @@ -248,9 +248,7 @@ defmodule DecimalTest do assert Decimal.compare("Inf", "Inf") == :eq - assert_raise Decimal.Error, - ": number bigger than DBL_MAX: Decimal.new(\"5E+10000000000\")", - fn -> Decimal.compare(~d"5e10000000000", ~d"0") end + assert Decimal.compare(~d"5e10000000000", ~d"0") == :gt assert_raise Error, fn -> Decimal.compare(~d"nan", ~d"0") @@ -909,41 +907,49 @@ defmodule DecimalTest do end end - test "max_min_dbl" do - assert Decimal.new("1.7976931348623158e308") == Decimal.dbl_max(1) - assert Decimal.new("-1.7976931348623158e308") == Decimal.dbl_max(-1) + test "test max_min_dbl in to_float" do + assert Decimal.to_float(dbl_max(1)) == 1.7976931348623158e308 + assert Decimal.to_float(dbl_max(-1)) == -1.7976931348623158e308 assert_raise Decimal.Error, ": number bigger than DBL_MAX: Decimal.new(\"1.79769313486231581E+308\")", - fn -> Decimal.new("1.79769313486231581e308") end + fn -> Decimal.to_float(Decimal.new("1.79769313486231581e308")) end assert_raise Decimal.Error, ": negative number smaller than DBL_MAX: Decimal.new(\"-1.79769313486231581E+308\")", - fn -> Decimal.new("-1.79769313486231581e308") end + fn -> Decimal.to_float(Decimal.new("-1.79769313486231581e308")) end + + assert Decimal.to_float(Decimal.new("1.79769313486231579e308")) == 1.79769313486231579e308 - assert Decimal.new("1.79769313486231579e308") == Decimal.new("1.79769313486231579E+308") - assert Decimal.new("-1.79769313486231579e308") == Decimal.new("-1.79769313486231579E+308") + assert Decimal.to_float(Decimal.new("-1.79769313486231579e308")) == + -1.79769313486231579e+308 - assert Decimal.new("2.2250738585072014e-308") == Decimal.dbl_min(1) - assert Decimal.new("-2.2250738585072014e-308") == Decimal.dbl_min(-1) + assert Decimal.to_float(Decimal.new("2.2250738585072014e-308")) == 2.2250738585072014e-308 + assert Decimal.to_float(Decimal.new("-2.2250738585072014e-308")) == -2.2250738585072014e-308 assert_raise Decimal.Error, ": number smaller than DBL_MIN: Decimal.new(\"2.22507385850720139E-308\")", - fn -> Decimal.new("2.22507385850720139e-308") end + fn -> Decimal.to_float(Decimal.new("2.22507385850720139e-308")) end assert_raise Decimal.Error, ": negative number bigger than DBL_MIN: Decimal.new(\"-2.22507385850720139E-308\")", - fn -> Decimal.new("-2.22507385850720139e-308") end + fn -> Decimal.to_float(Decimal.new("-2.22507385850720139e-308")) end + + assert Decimal.to_float(Decimal.new("2.22507385850720141e-308")) == 2.22507385850720141e-308 - assert Decimal.new("2.22507385850720141e-308") == Decimal.new("2.22507385850720141E-308") - assert Decimal.new("-2.22507385850720141e-308") == Decimal.new("-2.22507385850720141E-308") + assert Decimal.to_float(Decimal.new("-2.22507385850720141e-308")) == + -2.22507385850720141e-308 assert_raise Decimal.Error, ": number bigger than DBL_MAX: Decimal.new(\"9.999999999999999999E+1000000000000000000000017\")", - fn -> Decimal.new("9999999999999999999e999999999999999999999999") end + fn -> + Decimal.to_float(Decimal.new("9999999999999999999e999999999999999999999999")) + end assert_raise Decimal.Error, ": number smaller than DBL_MIN: Decimal.new(\"9.9999999999999E-999999999999999999999986\")", - fn -> Decimal.new("99999999999999e-999999999999999999999999") end + fn -> + Decimal.to_float(Decimal.new("99999999999999e-999999999999999999999999")) + end end end diff --git a/test/test_helper.exs b/test/test_helper.exs index b215dcb..0cad62b 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -12,4 +12,22 @@ defmodule TestMacros do Decimal.new(unquote(str)) end end + + defmacro dbl_min(sign) do + quote do + %Decimal{sign: unquote(sign), coef: 22_250_738_585_072_014, exp: -324} + end + end + + defmacro zero(sign) do + quote do + %Decimal{sign: unquote(sign), coef: 0, exp: 0} + end + end + + defmacro dbl_max(sign) do + quote do + %Decimal{sign: unquote(sign), coef: 17_976_931_348_623_158, exp: 292} + end + end end From 760e56e88117a3ecc649c7fc46dbfd66ea6d1b91 Mon Sep 17 00:00:00 2001 From: "karlo.smid" Date: Tue, 14 May 2024 13:57:16 +0200 Subject: [PATCH 3/4] refactor: use dbl_min and dbl_max test macros in test --- test/decimal_test.exs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/decimal_test.exs b/test/decimal_test.exs index 32fc1e2..b5f26a0 100644 --- a/test/decimal_test.exs +++ b/test/decimal_test.exs @@ -919,13 +919,13 @@ defmodule DecimalTest do ": negative number smaller than DBL_MAX: Decimal.new(\"-1.79769313486231581E+308\")", fn -> Decimal.to_float(Decimal.new("-1.79769313486231581e308")) end - assert Decimal.to_float(Decimal.new("1.79769313486231579e308")) == 1.79769313486231579e308 + assert Decimal.to_float(dbl_max(1)) == 1.79769313486231579e308 - assert Decimal.to_float(Decimal.new("-1.79769313486231579e308")) == + assert Decimal.to_float(dbl_max(-1)) == -1.79769313486231579e+308 - assert Decimal.to_float(Decimal.new("2.2250738585072014e-308")) == 2.2250738585072014e-308 - assert Decimal.to_float(Decimal.new("-2.2250738585072014e-308")) == -2.2250738585072014e-308 + assert Decimal.to_float(dbl_min(1)) == 2.2250738585072014e-308 + assert Decimal.to_float(dbl_min(-1)) == -2.2250738585072014e-308 assert_raise Decimal.Error, ": number smaller than DBL_MIN: Decimal.new(\"2.22507385850720139E-308\")", From fafde555fbe2e5ece6ed4c5ba97889d5f6c7c171 Mon Sep 17 00:00:00 2001 From: Wojtek Mach Date: Tue, 11 Feb 2025 13:02:29 +0100 Subject: [PATCH 4/4] Update decimal_test.exs --- test/decimal_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/decimal_test.exs b/test/decimal_test.exs index 6b24c38..8a2ecb6 100644 --- a/test/decimal_test.exs +++ b/test/decimal_test.exs @@ -988,4 +988,4 @@ defmodule DecimalTest do assert JSON.encode!(%{x: Decimal.new("1.0")}) == "{\"x\":\"1.0\"}" end end -end \ No newline at end of file +end