Current section

Files

Jump to
ex_cldr_units lib cldr unit conversion.ex
Raw

lib/cldr/unit/conversion.ex

defmodule Cldr.Unit.Conversion do
@moduledoc """
Unit conversion functions for the units defined
in `Cldr`.
"""
@enforce_keys [:factor, :offset, :base_unit]
defstruct factor: 1,
offset: 0,
base_unit: nil
@type factor :: integer | float | Ratio.t()
@type offset :: integer | float
@type t :: %{
factor: factor(),
base_unit: [atom(), ...],
offset: offset()
}
alias Cldr.Unit
import Unit, only: [incompatible_units_error: 2]
defmodule Options do
defstruct usage: nil, locale: nil, backend: nil, territory: nil
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.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 category and base unit. Received :mile and :gallon"}}
"""
@spec convert(Unit.t(), Unit.unit()) :: {:ok, Unit.t()} | {:error, {module(), String.t()}}
def convert(%Unit{} = unit, to_unit) do
%{unit: from_unit, value: value, base_conversion: from_conversion} = unit
with {:ok, to_unit, to_conversion} <- Unit.validate_unit(to_unit),
{:ok, converted} <- convert(value, from_conversion, to_conversion) do
Unit.new(to_unit, converted, usage: unit.usage, format_options: unit.format_options)
else
{:error, {Cldr.Unit.IncompatibleUnitsError, _}} ->
{:error, incompatible_units_error(from_unit, to_unit)}
end
end
defp convert(value, from, to) when is_number(value) or is_map(value) do
use Ratio
with {:ok, from, to} <- compatible(from, to) do
value
|> Ratio.new()
|> convert_to_base(from)
|> convert_from_base(to)
|> wrap_ok
end
end
def convert_to_base(value, %__MODULE__{} = from) do
use Ratio
%{factor: from_factor, offset: from_offset} = from
value * from_factor + from_offset
end
def convert_to_base(value, [{_, [{_, from}]}]) do
convert_to_base(value, from)
end
# A known translation with a "per" conversion
def convert_to_base(value, [{_, {_, _} = from}]) do
convert_to_base(value, from)
end
def convert_to_base(value, {_, %__MODULE__{} = from}) do
convert_to_base(value, from)
end
def convert_to_base(value, {numerator, denominator}) do
use Ratio
convert_to_base(1.0, numerator) / convert_to_base(1.0, denominator) * value
end
def convert_to_base(value, []) do
value
end
def convert_to_base(value, [numerator | rest]) do
convert_to_base(value, numerator) |> convert_to_base(rest)
end
def convert_to_base(_value, conversion) do
raise ArgumentError, "Conversion not recognised: #{inspect(conversion)}"
end
def convert_from_base(value, %__MODULE__{} = to) do
use Ratio
%{factor: to_factor, offset: to_offset} = to
(value - to_offset) / to_factor
end
def convert_from_base(value, [{_, [{_, to}]}]) do
convert_from_base(value, to)
end
# A known translation with a "per" conversion
def convert_from_base(value, [{_, {_, _} = to}]) do
convert_from_base(value, to)
end
def convert_from_base(value, {_, %__MODULE__{} = to}) do
convert_from_base(value, to)
end
def convert_from_base(value, {numerator, denominator}) do
use Ratio
convert_from_base(1.0, numerator) / convert_from_base(1.0, denominator) * value
end
def convert_from_base(value, []) do
value
end
def convert_from_base(value, [numerator | rest]) do
convert_from_base(value, numerator) |> convert_from_base(rest)
end
defp compatible(from, to) do
with {:ok, base_unit_from} <- Unit.base_unit(from),
{:ok, base_unit_to} <- Unit.base_unit(to),
true <- to_string(base_unit_from) == to_string(base_unit_to) do
{:ok, from, to}
else
_ -> {:error, incompatible_units_error(from, to)}
end
end
defp wrap_ok(unit) do
{:ok, unit}
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.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<:fahrenheit, 32.0>
iex> Cldr.Unit.Conversion.convert!(Cldr.Unit.new!(:fahrenheit, 32), :celsius)
...> |> Cldr.Unit.round
#Cldr.Unit<:celsius, 0.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<: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
end