Skip to content

Commit c9347ed

Browse files
authored
Document protocol implementations (#220)
Also, improve error message on Decimal.to_float for inf/nan.
1 parent 8843f5b commit c9347ed

File tree

3 files changed

+71
-10
lines changed

3 files changed

+71
-10
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
elixir: 1.8.2
2020
otp: 20.3.x
2121
- pair:
22-
elixir: 1.17.x
22+
elixir: 1.18.x
2323
otp: 27.x
2424
lint: lint
2525
steps:

lib/decimal.ex

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,45 @@ defmodule Decimal do
4040
according to the specification, return a number that "underflows" 0 is returned
4141
instead of Etiny. This may happen when dividing a number with infinity.
4242
Additionally, overflow, underflow and clamped may never be signalled.
43+
44+
## Protocol Implementations
45+
46+
`Decimal` implements the following protocols:
47+
48+
### `Inspect`
49+
50+
iex> inspect(Decimal.new("1.00"))
51+
"Decimal.new(\\"1.00\\")"
52+
53+
### `String.Chars`
54+
55+
iex> to_string(Decimal.new("1.00"))
56+
"1.00"
57+
58+
### `JSON.Encoder`
59+
60+
_(If running Elixir 1.18+.)_
61+
62+
By default, decimals are encoded as strings to preserve precision:
63+
64+
iex> JSON.encode!(Decimal.new("1.00"))
65+
"\\"1.00\\""
66+
67+
To change that, pass a custom encoder to `JSON.encode!/2`. The following encodes
68+
decimals as floats:
69+
70+
iex> encoder = fn
71+
...> %Decimal{} = decimal, _encoder ->
72+
...> decimal |> Decimal.to_float() |> :json.encode_float()
73+
...>
74+
...> other, encoder ->
75+
...> JSON.protocol_encode(other, encoder)
76+
...> end
77+
...>
78+
iex> JSON.encode!(%{x: Decimal.new("1.00")}, encoder)
79+
"{\\"x\\":1.0}"
80+
81+
Note: `Decimal.to_float/1` crashes on infinite and NaN decimals.
4382
"""
4483

4584
import Bitwise
@@ -1588,7 +1627,7 @@ defmodule Decimal do
15881627
@doc """
15891628
Returns the decimal represented as an integer.
15901629
1591-
Fails when loss of precision will occur.
1630+
Raises when loss of precision will occur.
15921631
15931632
## Examples
15941633
@@ -1623,28 +1662,30 @@ defmodule Decimal do
16231662
@doc """
16241663
Returns the decimal converted to a float.
16251664
1626-
The returned float may have lower precision than the decimal. Fails if
1627-
the decimal cannot be converted to a float.
1665+
The returned float may have lower precision than the decimal.
1666+
1667+
Raises if the decimal cannot be converted to a float.
16281668
16291669
## Examples
16301670
16311671
iex> Decimal.to_float(Decimal.new("1.5"))
16321672
1.5
1673+
16331674
iex> Decimal.to_float(Decimal.new("-1.79769313486231581e308"))
16341675
** (Decimal.Error) : negative number smaller than DBL_MAX: Decimal.new("-1.79769313486231581E+308")
16351676
1636-
16371677
iex> Decimal.to_float(Decimal.new("-1.79769313486231581e308"))
16381678
** (Decimal.Error) : negative number smaller than DBL_MAX: Decimal.new("-1.79769313486231581E+308")
16391679
1640-
16411680
iex> Decimal.to_float(Decimal.new("2.22507385850720139e-308"))
16421681
** (Decimal.Error) : number smaller than DBL_MIN: Decimal.new("2.22507385850720139E-308")
16431682
1644-
16451683
iex> Decimal.to_float(Decimal.new("-2.22507385850720139e-308"))
16461684
** (Decimal.Error): negative number bigger than DBL_MIN: Decimal.new(\"-2.22507385850720139E-308\")
16471685
1686+
iex> Decimal.to_float(Decimal.new("inf"))
1687+
** (ArgumentError) Decimal.new("Infinity") cannot be converted to float
1688+
16481689
"""
16491690
@spec to_float(t) :: float
16501691
def to_float(%Decimal{coef: coef} = decimal) when is_integer(coef) do
@@ -1669,6 +1710,10 @@ defmodule Decimal do
16691710
end
16701711
end
16711712

1713+
def to_float(%Decimal{} = decimal) do
1714+
raise ArgumentError, "#{inspect(decimal)} cannot be converted to float"
1715+
end
1716+
16721717
@doc """
16731718
Returns the scale of the decimal.
16741719

test/decimal_test.exs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@ defmodule DecimalTest do
77

88
require Decimal
99

10-
doctest Decimal
10+
elixir_json_available? = Version.match?(System.version(), ">= 1.18.0-rc")
11+
12+
if elixir_json_available? do
13+
doctest Decimal
14+
else
15+
doctest Decimal, except: [:moduledoc]
16+
end
1117

1218
test "parse/1" do
1319
assert Decimal.parse("123") == {d(1, 123, 0), ""}
@@ -684,7 +690,7 @@ defmodule DecimalTest do
684690
assert Decimal.to_float(~d"2251799813685248") === 2_251_799_813_685_248.0
685691
assert Decimal.to_float(~d"9007199254740992") === 9_007_199_254_740_992.0
686692

687-
assert_raise FunctionClauseError, fn ->
693+
assert_raise ArgumentError, fn ->
688694
Decimal.to_float(d(1, :NaN, 0))
689695
end
690696
end)
@@ -983,9 +989,19 @@ defmodule DecimalTest do
983989
end
984990
end
985991

986-
if Version.match?(System.version(), ">= 1.18.0-rc") do
992+
if elixir_json_available? do
987993
test "JSON.Encoder implementation" do
988994
assert JSON.encode!(%{x: Decimal.new("1.0")}) == "{\"x\":\"1.0\"}"
995+
996+
encoder = fn
997+
%Decimal{} = decimal, _encode ->
998+
decimal |> Decimal.to_float() |> :json.encode_float()
999+
1000+
other, encode ->
1001+
JSON.protocol_encode(other, encode)
1002+
end
1003+
1004+
assert JSON.encode!(%{x: Decimal.new("1.0")}, encoder) == "{\"x\":1.0}"
9891005
end
9901006
end
9911007
end

0 commit comments

Comments
 (0)