diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5e369ab..302d3df 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: elixir: 1.8.2 otp: 20.3.x - pair: - elixir: 1.17.x + elixir: 1.18.x otp: 27.x lint: lint steps: diff --git a/lib/decimal.ex b/lib/decimal.ex index afb0e5f..b6dcc9b 100644 --- a/lib/decimal.ex +++ b/lib/decimal.ex @@ -40,6 +40,45 @@ defmodule Decimal do according to the specification, return a number that "underflows" 0 is returned instead of Etiny. This may happen when dividing a number with infinity. Additionally, overflow, underflow and clamped may never be signalled. + + ## Protocol Implementations + + `Decimal` implements the following protocols: + + ### `Inspect` + + iex> inspect(Decimal.new("1.00")) + "Decimal.new(\\"1.00\\")" + + ### `String.Chars` + + iex> to_string(Decimal.new("1.00")) + "1.00" + + ### `JSON.Encoder` + + _(If running Elixir 1.18+.)_ + + By default, decimals are encoded as strings to preserve precision: + + iex> JSON.encode!(Decimal.new("1.00")) + "\\"1.00\\"" + + To change that, pass a custom encoder to `JSON.encode!/2`. The following encodes + decimals as floats: + + iex> encoder = fn + ...> %Decimal{} = decimal, _encoder -> + ...> decimal |> Decimal.to_float() |> :json.encode_float() + ...> + ...> other, encoder -> + ...> JSON.protocol_encode(other, encoder) + ...> end + ...> + iex> JSON.encode!(%{x: Decimal.new("1.00")}, encoder) + "{\\"x\\":1.0}" + + Note: `Decimal.to_float/1` crashes on infinite and NaN decimals. """ import Bitwise @@ -1588,7 +1627,7 @@ defmodule Decimal do @doc """ Returns the decimal represented as an integer. - Fails when loss of precision will occur. + Raises when loss of precision will occur. ## Examples @@ -1623,28 +1662,30 @@ defmodule Decimal do @doc """ Returns the decimal converted to a float. - The returned float may have lower precision than the decimal. Fails if - the decimal cannot be converted to a float. + The returned float may have lower precision than the decimal. + + Raises if the decimal cannot be converted to a float. ## Examples 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\") + iex> Decimal.to_float(Decimal.new("inf")) + ** (ArgumentError) Decimal.new("Infinity") cannot be converted to float + """ @spec to_float(t) :: float def to_float(%Decimal{coef: coef} = decimal) when is_integer(coef) do @@ -1669,6 +1710,10 @@ defmodule Decimal do end end + def to_float(%Decimal{} = decimal) do + raise ArgumentError, "#{inspect(decimal)} cannot be converted to float" + end + @doc """ Returns the scale of the decimal. diff --git a/test/decimal_test.exs b/test/decimal_test.exs index 8a2ecb6..bd9b46f 100644 --- a/test/decimal_test.exs +++ b/test/decimal_test.exs @@ -7,7 +7,13 @@ defmodule DecimalTest do require Decimal - doctest Decimal + elixir_json_available? = Version.match?(System.version(), ">= 1.18.0-rc") + + if elixir_json_available? do + doctest Decimal + else + doctest Decimal, except: [:moduledoc] + end test "parse/1" do assert Decimal.parse("123") == {d(1, 123, 0), ""} @@ -684,7 +690,7 @@ defmodule DecimalTest do assert Decimal.to_float(~d"2251799813685248") === 2_251_799_813_685_248.0 assert Decimal.to_float(~d"9007199254740992") === 9_007_199_254_740_992.0 - assert_raise FunctionClauseError, fn -> + assert_raise ArgumentError, fn -> Decimal.to_float(d(1, :NaN, 0)) end end) @@ -983,9 +989,19 @@ defmodule DecimalTest do end end - if Version.match?(System.version(), ">= 1.18.0-rc") do + if elixir_json_available? do test "JSON.Encoder implementation" do assert JSON.encode!(%{x: Decimal.new("1.0")}) == "{\"x\":\"1.0\"}" + + encoder = fn + %Decimal{} = decimal, _encode -> + decimal |> Decimal.to_float() |> :json.encode_float() + + other, encode -> + JSON.protocol_encode(other, encode) + end + + assert JSON.encode!(%{x: Decimal.new("1.0")}, encoder) == "{\"x\":1.0}" end end end