-
-
Notifications
You must be signed in to change notification settings - Fork 206
/
decimal.ex
150 lines (124 loc) · 3.16 KB
/
decimal.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
defmodule Ash.Type.Decimal do
@constraints [
max: [
type: {:custom, __MODULE__, :decimal, []},
doc: "Enforces a maximum on the value"
],
min: [
type: {:custom, __MODULE__, :decimal, []},
doc: "Enforces a minimum on the value"
]
]
@moduledoc """
Represents a decimal.
A builtin type that can be referenced via `:decimal`
### Constraints
#{Spark.OptionsHelpers.docs(@constraints)}
"""
require Decimal
use Ash.Type
@impl true
def generator(constraints) do
params =
constraints
|> Keyword.take([:min, :max])
|> Enum.map(fn {key, value} ->
if Decimal.is_decimal(value) do
{key, Decimal.to_float(value)}
else
{key, value}
end
end)
params
|> StreamData.float()
|> StreamData.map(&Decimal.from_float/1)
# A second pass filter to account for inaccuracies in the above float -> decimal
|> StreamData.filter(fn value ->
less_than_max =
if constraints[:max] do
Decimal.lt?(value, constraints[:max]) || Decimal.eq?(value, constraints[:max])
else
true
end
greater_than_min =
if constraints[:min] do
Decimal.gt?(value, constraints[:min]) || Decimal.eq?(value, constraints[:min])
else
true
end
less_than_max && greater_than_min
end)
end
@impl true
def storage_type, do: :decimal
@impl true
def constraints, do: @constraints
@doc false
def decimal(value) do
case cast_input(value, []) do
{:ok, decimal} ->
{:ok, decimal}
:error ->
{:error, "cannot be casted to decimal"}
end
end
@impl true
def apply_constraints(nil, _), do: :ok
def apply_constraints(value, constraints) do
errors =
Enum.reduce(constraints, [], fn
{:max, max}, errors ->
if Decimal.compare(value, max) == :gt do
[[message: "must be less than or equal to %{max}", max: max] | errors]
else
errors
end
{:min, min}, errors ->
if Decimal.compare(value, min) == :lt do
[[message: "must be more than or equal to %{min}", min: min] | errors]
else
errors
end
end)
case errors do
[] -> :ok
errors -> {:error, errors}
end
end
@impl true
def cast_input(value, _) when is_binary(value) do
case Decimal.parse(value) do
{decimal, ""} ->
{:ok, decimal}
_ ->
:error
end
end
@impl true
def cast_input(value, _) do
Ecto.Type.cast(:decimal, value)
end
@impl true
def cast_stored(value, _) when is_binary(value) do
case Decimal.parse(value) do
{decimal, ""} ->
{:ok, decimal}
_ ->
:error
end
end
@impl true
def cast_stored(nil, _), do: {:ok, nil}
def cast_stored(value, _) do
Ecto.Type.load(:decimal, value)
end
@impl true
@spec dump_to_native(any, any) :: :error | {:ok, any}
def dump_to_native(nil, _), do: {:ok, nil}
def dump_to_native(value, _) do
Ecto.Type.dump(:decimal, value)
end
@doc false
def new(%Decimal{} = v), do: v
def new(v), do: Decimal.new(v)
end