-
Notifications
You must be signed in to change notification settings - Fork 13
/
conversion.ex
377 lines (281 loc) · 10 KB
/
conversion.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
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
defmodule Cldr.Unit.Conversion do
@moduledoc """
Unit conversion functions for the units defined
in `CLDR`.
"""
@enforce_keys [:base_unit]
defstruct factor: nil,
offset: nil,
base_unit: nil,
special: nil
@type factor :: integer | float
@type offset :: integer | float
@type t :: %{
factor: factor(),
base_unit: [atom(), ...],
offset: offset(),
special: atom() | nil
}
alias Cldr.Unit
alias Cldr.Unit.BaseUnit
import Kernel, except: [div: 2]
@decimal_1 Decimal.new(1)
@doc """
Returns the conversion that calculates
the base unit into another unit or
and error.
"""
def conversion_for(unit_1, unit_2) do
with {:ok, base_unit_1, _conversion_1} <- base_unit_and_conversion(unit_1),
{:ok, base_unit_2, conversion_2} <- base_unit_and_conversion(unit_2) do
conversion_for(unit_1, unit_2, base_unit_1, base_unit_2, conversion_2)
end
end
# Base units match so are compatible
defp conversion_for(_unit_1, _unit_2, base_unit, base_unit, conversion_2) do
{:ok, conversion_2, :forward}
end
# Its invertable so see if that's convertible. Note that
# there is no difference in the conversion for an inverted
# conversion. Its only a hint so that in convert_from_base/2
# we know to divide, not multiple the value.
defp conversion_for(unit_1, unit_2, base_unit_1, _base_unit_2, {numerator_2, denominator_2}) do
inverted_conversion = {denominator_2, numerator_2}
with {:ok, base_unit_2} <- BaseUnit.canonical_base_unit(inverted_conversion) do
if base_unit_1 == base_unit_2 do
{:ok, {numerator_2, denominator_2}, :inverted}
else
{:error, Unit.incompatible_units_error(unit_1, unit_2)}
end
end
end
# If the base units don't match, try comparing the unit categories
# instead.
defp conversion_for(unit_1, unit_2, _base_unit_1, _base_unit_2, conversion_2) do
with {:ok, category_1} <- Cldr.Unit.unit_category(unit_1),
{:ok, category_2} <- Cldr.Unit.unit_category(unit_2) do
if category_1 == category_2 do
{:ok, conversion_2, :forward}
else
{:error, Unit.incompatible_units_error(unit_1, unit_2)}
end
end
end
@doc """
Returns the base unit and the base unit
conversionfor a given unit.
## Argument
* `unit` is either a `t:Cldr.Unit`, an `atom` or
a `t:String`
## Returns
* `{:ok, base_unit, conversion}` or
* `{:error, {exception, reason}}`
## Example
iex> Cldr.Unit.Conversion.base_unit_and_conversion :square_kilometer
{:ok, :square_meter,
[
{"square_kilometer",
%Cldr.Unit.Conversion{
factor: 1000000,
offset: 0,
base_unit: [:square, :meter]
}}
]}
iex> Cldr.Unit.Conversion.base_unit_and_conversion :square_table
{:error, {Cldr.UnknownUnitError, "Unknown unit was detected at \\"table\\""}}
"""
def base_unit_and_conversion(%Unit{base_conversion: conversion}) do
{:ok, base_unit} = BaseUnit.canonical_base_unit(conversion)
{:ok, base_unit, conversion}
end
def base_unit_and_conversion(unit_name) when is_atom(unit_name) or is_binary(unit_name) do
with {:ok, _unit, conversion} <- Cldr.Unit.validate_unit(unit_name),
{:ok, base_unit} <- BaseUnit.canonical_base_unit(conversion) do
{:ok, base_unit, conversion}
end
end
@doc """
Convert one unit into another unit of the same
unit type (length, volume, mass, ...)
## Arguments
* `unit` is any unit returned by `Cldr.Unit.new/2`.
* `to_unit` is any unit name returned by `Cldr.Unit.known_units/0`.
## Returns
* a `Unit.t` of the unit type `to_unit` or
* `{:error, {exception, message}}`
## Examples
iex> Cldr.Unit.convert Cldr.Unit.new!(:mile, 1), :foot
{:ok, Cldr.Unit.new!(:foot, 5280)}
iex> Cldr.Unit.convert Cldr.Unit.new!(:mile, 1), :gallon
{:error, {Cldr.Unit.IncompatibleUnitsError,
"Operations can only be performed between units with the same base unit. Received :mile and :gallon"}}
"""
@spec convert(Unit.t(), Unit.unit()) :: {:ok, Unit.t()} | {:error, {module(), String.t()}}
def convert(%Unit{value: value, base_conversion: from_conversion} = unit, to_unit) do
with {:ok, to_conversion, maybe_inverted} <- conversion_for(unit, to_unit) do
converted_value = convert(value, from_conversion, to_conversion, maybe_inverted)
Unit.new(to_unit, converted_value, usage: unit.usage, format_options: unit.format_options)
end
end
defp convert(value, from, to, maybe_inverted) when is_number(value) or is_map(value) do
value
|> convert_to_base(from)
|> maybe_invert_value(maybe_inverted)
|> convert_from_base(to)
end
def maybe_invert_value(value, :inverted) do
div(1, value)
end
def maybe_invert_value(value, _) do
value
end
# Special handling for Beaufort
defp convert_to_base(value, {_, %__MODULE__{special: :beaufort}}) do
mult(0.836, pow(value, 1.5))
end
# All conversions are ultimately a list of
# 2-tuples of the unit and conversion struct
defp convert_to_base(value, {_, %__MODULE__{} = from}) do
%{factor: from_factor, offset: from_offset} = from
from_factor
|> mult(value)
|> add(from_offset)
end
# A per module is a 2-tuple of the numerator and
# denominator. Both are lists of conversion tuples.
defp convert_to_base(value, {numerator, denominator}) do
convert_to_base(@decimal_1, numerator)
|> div(convert_to_base(@decimal_1, denominator))
|> mult(value)
end
# We recurse over the list of conversions
# and accumulate the value as we go
defp convert_to_base(value, []) do
value
end
defp convert_to_base(value, [first | rest]) do
convert_to_base(value, first) |> convert_to_base(rest)
end
# But if we meet a shape of data we don't
# understand then its a raisable error
defp convert_to_base(_value, conversion) do
raise ArgumentError, "Conversion not recognised: #{inspect(conversion)}"
end
# Special handling for Beaufort
defp convert_from_base(value, {_, %__MODULE__{special: :beaufort}}) do
# B = (S/0.836)^(2/3)
divided = div(value, 0.836)
pow(divided, 2 / 3)
|> Cldr.Math.round(2)
end
defp convert_from_base(value, {_, %__MODULE__{} = to}) do
%{factor: to_factor, offset: to_offset} = to
value
|> sub(to_offset)
|> div(to_factor)
end
defp convert_from_base(value, {numerator, denominator}) do
convert_from_base(@decimal_1, numerator)
|> div(convert_from_base(@decimal_1, denominator))
|> mult(value)
end
defp convert_from_base(value, []) do
value
end
defp convert_from_base(value, [first | rest]) do
convert_from_base(value, first) |> convert_from_base(rest)
end
defp convert_from_base(_value, conversion) do
raise ArgumentError, "Conversion not recognised: #{inspect(conversion)}"
end
@doc """
Convert one unit into another unit of the same
unit type (length, volume, mass, ...) and raises
on a unit type mismatch.
## Arguments
* `unit` is any unit returned by `Cldr.Unit.new/2`.
* `to_unit` is any unit name returned by `Cldr.Unit.known_units/0`.
## Returns
* a `Unit.t` of the unit type `to_unit` or
* raises an exception
## Examples
iex> Cldr.Unit.Conversion.convert!(Cldr.Unit.new!(:celsius, 0), :fahrenheit)
...> |> Cldr.Unit.round
Cldr.Unit.new!(:fahrenheit, 32)
iex> Cldr.Unit.Conversion.convert!(Cldr.Unit.new!(:fahrenheit, 32), :celsius)
...> |> Cldr.Unit.round
Cldr.Unit.new!(:celsius, 0)
Cldr.Unit.Conversion.convert Cldr.Unit.new!(:mile, 1), :gallon
** (Cldr.Unit.IncompatibleUnitsError) Operations can only be performed between units of the same type. Received :mile and :gallon
"""
@spec convert!(Unit.t(), Unit.unit()) :: Unit.t() | no_return()
def convert!(%Unit{} = unit, to_unit) do
case convert(unit, to_unit) do
{:error, {exception, reason}} -> raise exception, reason
{:ok, unit} -> unit
end
end
@doc """
Convert a unit into its base unit.
For example, the base unit for `length`
is `meter`. The base unit is an
intermediary unit used in all
conversions.
## Arguments
* `unit` is any unit returned by `Cldr.Unit.new/2`
## Returns
* `unit` converted to its base unit as a `t:Unit.t()` or
* `{;error, {exception, reason}}` as an error
## Example
iex> unit = Cldr.Unit.new!(:kilometer, 10)
iex> Cldr.Unit.Conversion.convert_to_base_unit unit
{:ok, Cldr.Unit.new!(:meter, 10000)}
"""
def convert_to_base_unit(%Unit{} = unit) do
with {:ok, base_unit} <- Unit.base_unit(unit) do
convert(unit, base_unit)
end
end
def convert_to_base_unit(unit) when is_atom(unit) do
unit
|> Unit.new!("1.0")
|> convert_to_base_unit()
end
def convert_to_base_unit([unit | _rest]) when is_atom(unit) do
convert_to_base_unit(unit)
end
@doc """
Convert a unit into its base unit and
raises on error.
For example, the base unit for `length`
is `meter`. The base unit is an
intermediary unit used in all
conversions.
## Arguments
* `unit` is any unit returned by `Cldr.Unit.new/2`.
## Returns
* `unit` converted to its base unit as a `t:Unit.t()` or
* raises an exception
## Example
iex> unit = Cldr.Unit.new!(:kilometer, 10)
iex> Cldr.Unit.Conversion.convert_to_base_unit! unit
Cldr.Unit.new!(:meter, 10000)
"""
def convert_to_base_unit!(%Unit{} = unit) do
case convert_to_base_unit(unit) do
{:error, {exception, reason}} -> raise exception, reason
{:ok, unit} -> unit
end
end
# Math Helpers
@doc false
def add(v1, v2), do: Cldr.Math.add(v1, v2) |> Cldr.Math.maybe_integer()
@doc false
def sub(v1, v2), do: Cldr.Math.sub(v1, v2) |> Cldr.Math.maybe_integer()
@doc false
def mult(v1, v2), do: Cldr.Math.mult(v1, v2) |> Cldr.Math.maybe_integer()
@doc false
def div(v1, v2), do: Cldr.Math.div(v1, v2) |> Cldr.Math.maybe_integer()
@doc false
def pow(v1, v2), do: Cldr.Math.pow(v1, v2) |> Cldr.Math.maybe_integer()
end