Packages
ex_cldr_units
3.16.2
3.20.5
3.20.4
3.20.3
3.20.2
3.20.1
3.20.0
3.19.2
3.19.1
3.19.0
3.18.1
3.18.0
3.17.2
3.17.1
3.17.0
3.16.5
3.16.4
3.16.3
3.16.2
3.16.1
3.16.0
3.15.0
3.14.0
3.13.3
3.13.2
3.13.1
3.13.0
3.12.2
3.12.1
3.12.0
3.11.0
3.10.0
3.9.2
3.9.1
3.9.0
3.8.0
3.8.0-rc.2
3.8.0-rc.1
3.8.0-rc.0
3.7.1
3.7.0
3.6.0
3.5.3
3.5.2
3.5.1
3.5.0
3.5.0-rc.1
3.5.0-rc.0
3.4.0
3.4.0-rc.0
3.3.1
3.3.0
3.3.0-rc.0
3.2.1
3.2.0
3.1.2
3.1.1
3.1.0
3.0.1
3.0.0
3.0.0-rc.0
2.8.1
2.8.0
2.7.0
2.6.1
2.6.0
2.5.3
2.5.2
2.5.1
2.5.0
2.4.0
2.3.3
2.3.2
retired
2.3.1
2.3.0
2.2.0
2.1.0
2.0.0
1.3.0
1.2.2
1.2.1
1.2.0
1.1.1
1.1.0
retired
1.0.1
retired
1.0.0
retired
1.0.0-rc.0
retired
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.1
0.3.0
0.2.1
0.2.0
0.1.3
0.1.2
0.1.1
0.1.0
Unit formatting (volume, area, length, ...), conversion and arithmetic functions based upon the Common Locale Data Repository (CLDR).
Current section
Files
Jump to
Current section
Files
lib/cldr/unit/math.ex
defmodule Cldr.Unit.Math do
@moduledoc """
Simple arithmetic functions for the `t.Cldr.Unit.t/0` type.
"""
alias Cldr.Unit
alias Cldr.Unit.Parser
alias Cldr.Unit.Conversion
import Kernel, except: [div: 2, round: 1, trunc: 1]
import Unit, only: [incompatible_units_error: 2]
@doc false
defguard is_per_unit(base_conversion)
when is_tuple(base_conversion) and
tuple_size(base_conversion) == 2
@doc false
defguard is_simple_unit(base_conversion) when is_list(base_conversion)
@doc """
Adds two compatible `t:Cldr.Unit.t/0` types
## Arguments
* `unit_1` and `unit_2` are compatible Units
returned by `Cldr.Unit.new/2`.
## Returns
* A `t:Cldr.Unit.t/0` of the same type as `unit_1` with a value
that is the sum of `unit_1` and the potentially converted
`unit_2`, or
* `{:error, {IncompatibleUnitError, message}}`.
## Examples
iex> Cldr.Unit.Math.add Cldr.Unit.new!(:foot, 1), Cldr.Unit.new!(:foot, 1)
Cldr.Unit.new!(:foot, 2)
iex> Cldr.Unit.Math.add Cldr.Unit.new!(:foot, 1), Cldr.Unit.new!(:mile, 1)
Cldr.Unit.new!(:foot, 5281)
iex> Cldr.Unit.Math.add Cldr.Unit.new!(:foot, 1), Cldr.Unit.new!(:gallon, 1)
{:error, {Cldr.Unit.IncompatibleUnitsError,
"Operations can only be performed between units with the same base unit. Received :foot and :gallon"}}
"""
@spec add(Unit.t(), Unit.t()) :: Unit.t() | {:error, {module(), String.t()}}
def add(%Unit{unit: unit, value: value_1} = unit_1, %Unit{unit: unit, value: value_2}) do
%{unit_1 | value: Conversion.add(value_1, value_2)}
end
def add(%Unit{unit: unit_category_1} = unit_1, %Unit{unit: unit_category_2} = unit_2) do
if Unit.compatible?(unit_category_1, unit_category_2) do
add(unit_1, Conversion.convert!(unit_2, unit_category_1))
else
{:error, incompatible_units_error(unit_1, unit_2)}
end
end
@doc """
Adds two compatible `t:Cldr.Unit.t/0` types
and raises on error.
## Arguments
* `unit_1` and `unit_2` are compatible Units
returned by `Cldr.Unit.new/2`.
## Returns
* A `t:Cldr.Unit.t/0` of the same type as `unit_1` with a value
that is the sum of `unit_1` and the potentially converted
`unit_2` or
* Raises an exception.
"""
@spec add!(Unit.t(), Unit.t()) :: Unit.t() | no_return()
def add!(unit_1, unit_2) do
case add(unit_1, unit_2) do
{:error, {exception, reason}} -> raise exception, reason
unit -> unit
end
end
@doc """
Subtracts two compatible `t:Cldr.Unit.t/0` types.
## Arguments
* `unit_1` and `unit_2` are compatible Units
returned by `Cldr.Unit.new/2`.
## Returns
* A `t:Cldr.Unit.t/0` of the same type as `unit_1` with a value
that is the difference between `unit_1` and the potentially
converted `unit_2`, or
* `{:error, {IncompatibleUnitError, message}}`.
## Examples
iex> Cldr.Unit.sub Cldr.Unit.new!(:kilogram, 5), Cldr.Unit.new!(:pound, 1)
Cldr.Unit.new!(:kilogram, "4.54640763")
iex> Cldr.Unit.sub Cldr.Unit.new!(:pint, 5), Cldr.Unit.new!(:liter, 1)
Cldr.Unit.new!(:pint, "2.886623581134812676960800627")
iex> Cldr.Unit.sub Cldr.Unit.new!(:pint, 5), Cldr.Unit.new!(:pint, 1)
Cldr.Unit.new!(:pint, 4)
"""
@spec sub(Unit.t(), Unit.t()) :: Unit.t() | {:error, {module(), String.t()}}
def sub(%Unit{unit: unit, value: value_1} = unit_1, %Unit{unit: unit, value: value_2}) do
%{unit_1 | value: Conversion.sub(value_1, value_2)}
end
def sub(%Unit{unit: unit_category_1} = unit_1, %Unit{unit: unit_category_2} = unit_2) do
if Unit.compatible?(unit_category_1, unit_category_2) do
sub(unit_1, Conversion.convert!(unit_2, unit_category_1))
else
{:error, incompatible_units_error(unit_1, unit_2)}
end
end
@doc """
Subtracts two compatible `t:Cldr.Unit.t/0` types
and raises on error.
## Arguments
* `unit_1` and `unit_2` are compatible Units
returned by `Cldr.Unit.new/2`.
## Returns
* A `t:Cldr.Unit.t/0` of the same type as `unit_1` with a value
that is the difference between `unit_1` and the potentially
converted `unit_2` or
* Raises an exception.
"""
@spec sub!(Unit.t(), Unit.t()) :: Unit.t() | no_return()
def sub!(unit_1, unit_2) do
case sub(unit_1, unit_2) do
{:error, {exception, reason}} -> raise exception, reason
unit -> unit
end
end
@doc """
Multiplies two `t:Cldr.Unit.t/0` types. Any two
units can be multiplied together.
## Arguments
* `unit_1` and `unit_2` are Units
returned by `Cldr.Unit.new/2`.
## Returns
* A `t:Cldr.Unit.t/0` of a type that is the product
of `unit_1` and `unit_2` with a value
that is the product of `unit_1` and `unit_2`'s
values.
## Examples
iex> Cldr.Unit.mult Cldr.Unit.new!(:kilogram, 5), Cldr.Unit.new!(:pound, 1)
Cldr.Unit.new!(:kilogram, "2.26796185")
iex> Cldr.Unit.mult Cldr.Unit.new!(:pint, 5), Cldr.Unit.new!(:liter, 1)
Cldr.Unit.new!(:pint, "10.56688209432593661519599687")
iex> Cldr.Unit.mult Cldr.Unit.new!(:pint, 5), Cldr.Unit.new!(:pint, 1)
Cldr.Unit.new!(:pint, 5)
"""
@spec mult(Unit.t(), Unit.t()) :: Unit.t()
def mult(%Unit{unit: unit, value: value_1}, %Unit{unit: unit, value: value_2}) do
Unit.new!(unit, Conversion.mult(value_1, value_2))
end
def mult(%Unit{unit: unit_category_1} = unit_1, %Unit{unit: unit_category_2} = unit_2) do
if Unit.compatible?(unit_category_1, unit_category_2) do
{:ok, converted} = Conversion.convert(unit_2, unit_category_1)
mult(unit_1, converted)
else
product(unit_1, unit_2)
end
end
@doc """
Multiplies two compatible `t:Cldr.Unit.t/0` types
and raises on error.
## Options
* `unit_1` and `unit_2` are compatible Units
returned by `Cldr.Unit.new/2`.
## Returns
* A `t:Cldr.Unit.t/0` of the same type as `unit_1` with a value
that is the product of `unit_1` and the potentially
converted `unit_2` or
* Raises an exception.
"""
@spec mult!(Unit.t(), Unit.t()) :: Unit.t()
def mult!(unit_1, unit_2) do
mult(unit_1, unit_2)
end
@doc """
Divides one `t:Cldr.Unit.t/0` type into another.
Any unit can be divided by another.
## Options
* `unit_1` and `unit_2` are Units
returned by `Cldr.Unit.new/2`
## Returns
* A `t:Cldr.Unit.t/0` of a type that is the dividend
of `unit_1` and `unit_2` with a value
that is the dividend of `unit_1` and `unit_2`'s
values.
## Examples
iex> Cldr.Unit.Math.div Cldr.Unit.new!(:kilogram, 5), Cldr.Unit.new!(:pound, 1)
Cldr.Unit.new!(:kilogram, "11.02311310924387903614869007")
iex> Cldr.Unit.Math.div Cldr.Unit.new!(:pint, 5), Cldr.Unit.new!(:liter, 1)
Cldr.Unit.new!(:pint, "2.365882365000000000000000000")
iex> Cldr.Unit.Math.div Cldr.Unit.new!(:pint, 5), Cldr.Unit.new!(:pint, 1)
Cldr.Unit.new!(:pint, 5)
"""
@spec div(Unit.t(), Unit.t()) :: Unit.t()
def div(%Unit{unit: unit, value: value_1}, %Unit{unit: unit, value: value_2}) do
Unit.new!(unit, Conversion.div(value_1, value_2))
end
def div(%Unit{unit: unit_category_1} = unit_1, %Unit{unit: unit_category_2} = unit_2) do
if Unit.compatible?(unit_category_1, unit_category_2) do
div(unit_1, Conversion.convert!(unit_2, unit_category_1))
else
product(unit_1, invert(unit_2))
end
end
@doc """
Divides one `t:Cldr.Unit.t/0` type into another.
Any unit can be divided by another.
## Arguments
* `unit_1` and `unit_2` are compatible Units
returned by `Cldr.Unit.new/2`
## Returns
* A `t:Cldr.Unit.t/0` of the same type as `unit_1` with a value
that is the dividend of `unit_1` and the potentially
converted `unit_2` or
* Raises an exception.
"""
@spec div!(Unit.t(), Unit.t()) :: Unit.t()
def div!(unit_1, unit_2) do
div(unit_1, unit_2)
end
@doc """
Rounds the value of a unit.
## Arguments
* `unit` is any unit returned by `Cldr.Unit.new/2`
* `places` is the number of decimal places to round to.
The default is `0`.
* `mode` is the rounding mode to be applied. The default
is `:half_up`.
## Returns
* A `%Unit{}` of the same type as `unit` with a value
that is rounded to the specified number of decimal places.
## Rounding modes
Directed roundings:
* `:down` - Round towards 0 (truncate), eg 10.9 rounds to 10.0
* `:up` - Round away from 0, eg 10.1 rounds to 11.0. (Non IEEE algorithm)
* `:ceiling` - Round toward +∞ - Also known as rounding up or ceiling
* `:floor` - Round toward -∞ - Also known as rounding down or floor
Round to nearest:
* `:half_even` - Round to nearest value, but in a tiebreak, round towards the
nearest value with an even (zero) least significant bit, which occurs 50%
of the time. This is the default for IEEE binary floating-point and the recommended
value for decimal.
* `:half_up` - Round to nearest value, but in a tiebreak, round away from 0.
This is the default algorithm for Erlang's Kernel.round/2
* `:half_down` - Round to nearest value, but in a tiebreak, round towards 0
(Non IEEE algorithm)
## Examples
iex> Cldr.Unit.round Cldr.Unit.new!(:yard, 1031.61), 1
Cldr.Unit.new!(:yard, "1031.6")
iex> Cldr.Unit.round Cldr.Unit.new!(:yard, 1031.61), 2
Cldr.Unit.new!(:yard, "1031.61")
iex> Cldr.Unit.round Cldr.Unit.new!(:yard, 1031.61), 1, :up
Cldr.Unit.new!(:yard, "1031.7")
"""
@spec round(
unit :: Unit.t(),
places :: non_neg_integer,
mode :: :down | :up | :ceiling | :floor | :half_even | :half_up | :half_down
) :: Unit.t()
def round(unit, places \\ 0, mode \\ :half_up)
# def round(%Unit{value: %Ratio{} = value} = unit, places, mode) do
# value = Ratio.to_float(value)
# round(%{unit | value: value}, places, mode)
# end
def round(%Unit{value: value} = unit_1, places, mode) do
rounded_value =
value
|> Cldr.Math.round(places, mode)
|> Conversion.maybe_integer()
%{unit_1 | value: rounded_value}
end
@doc """
Truncates a unit's value.
"""
# def trunc(%Unit{value: %Ratio{} = value} = unit) do
# value = Ratio.to_float(value)
# trunc(%{unit | value: value})
# end
def trunc(%Unit{value: value} = unit) when is_float(value) do
%{unit | value: Kernel.trunc(value)}
end
def trunc(%Unit{value: value} = unit) when is_integer(value) do
unit
end
def trunc(%Unit{value: %Decimal{} = value} = unit) do
trunc =
value
|> Decimal.round(0, :floor)
|> Decimal.to_integer()
%{unit | value: trunc}
end
@doc """
Compare two units, converting to a common unit
type if required.
If conversion is performed, the results are both
rounded to a single decimal place before
comparison.
Returns `:gt`, `:lt`, or `:eq`.
## Example
iex> x = Cldr.Unit.new!(:kilometer, 1)
iex> y = Cldr.Unit.new!(:meter, 1000)
iex> Cldr.Unit.Math.compare x, y
:eq
"""
@spec compare(unit_1 :: Unit.t(), unit_2 :: Unit.t()) :: :eq | :lt | :gt
def compare(
%Unit{unit: unit, value: %Decimal{}} = unit_1,
%Unit{unit: unit, value: %Decimal{}} = unit_2
) do
Cldr.Decimal.compare(unit_1.value, unit_2.value)
end
def compare(%Unit{unit: unit, value: value_1}, %Unit{unit: unit, value: value_2})
when is_number(value_1) and is_number(value_2) do
cond do
value_1 == value_2 -> :eq
value_1 > value_2 -> :gt
value_1 < value_2 -> :lt
end
end
# def compare(%Unit{unit: unit, value: %Ratio{} = value_1}, %Unit{unit: unit, value: value_2}) do
# Ratio.compare(value_1, Ratio.new(value_2))
# end
#
# def compare(%Unit{unit: unit, value: value_1}, %Unit{unit: unit, value: %Ratio{} = value_2}) do
# Ratio.compare(Ratio.new(value_1), value_2)
# end
def compare(%Unit{unit: unit, value: %Decimal{} = value_1}, %Unit{unit: unit, value: value_2}) do
Decimal.compare(value_1, Decimal.new(value_2))
end
def compare(%Unit{unit: unit, value: value_1}, %Unit{unit: unit, value: %Decimal{} = value_2}) do
Decimal.compare(Decimal.new(value_1), value_2)
end
def compare(%Unit{value: %Decimal{}} = unit_1, %Unit{value: %Decimal{}} = unit_2) do
unit_2 = Unit.Conversion.convert!(unit_2, unit_1.unit)
compare(unit_1, unit_2)
end
def compare(%Unit{} = unit_1, %Unit{} = unit_2) do
unit_1 =
unit_1
|> round(1, :half_even)
unit_2 =
unit_2
|> Unit.Conversion.convert!(unit_1.unit)
|> round(1, :half_even)
compare(unit_1, unit_2)
end
@deprecated "Please use Cldr.Unit.Math.compare/2"
def cmp(unit_1, unit_2) do
compare(unit_1, unit_2)
end
### Helpers
defp product(%Unit{base_conversion: conv_1} = unit_1, %Unit{base_conversion: conv_2} = unit_2)
when is_per_unit(conv_1) and is_per_unit(conv_2) do
{numerator_1, denominator_1} = conv_1
{numerator_2, denominator_2} = conv_2
new_numerator = Enum.sort(numerator_1 ++ numerator_2, &Parser.unit_sorter/2)
new_denominator = Enum.sort(denominator_1 ++ denominator_2, &Parser.unit_sorter/2)
new_conversion = combine_power_instances({new_numerator, new_denominator})
new_value = Conversion.mult(unit_1.value, unit_2.value)
unit_name =
new_conversion
|> Parser.canonical_unit_name()
|> Unit.maybe_translatable_unit()
%{unit_1 | unit: unit_name, value: new_value, base_conversion: new_conversion}
end
defp product(%Unit{base_conversion: conv_1} = unit_1, %Unit{base_conversion: conv_2} = unit_2)
when is_per_unit(conv_1) and is_simple_unit(conv_2) do
{numerator_1, denominator_1} = conv_1
new_numerator = Enum.sort(numerator_1 ++ conv_2, &Parser.unit_sorter/2)
new_denominator = denominator_1
new_conversion = combine_power_instances({new_numerator, new_denominator})
new_value = Conversion.mult(unit_1.value, unit_2.value)
unit_name =
new_conversion
|> Parser.canonical_unit_name()
|> Unit.maybe_translatable_unit()
%{unit_1 | unit: unit_name, value: new_value, base_conversion: new_conversion}
end
defp product(%Unit{base_conversion: conv_1} = unit_1, %Unit{base_conversion: conv_2} = unit_2)
when is_simple_unit(conv_1) and is_per_unit(conv_2) do
{numerator_2, denominator_2} = conv_2
new_numerator = Enum.sort(conv_1 ++ numerator_2, &Parser.unit_sorter/2)
new_denominator = denominator_2
new_conversion = combine_power_instances({new_numerator, new_denominator})
new_value = Conversion.mult(unit_1.value, unit_2.value)
unit_name =
new_conversion
|> Parser.canonical_unit_name()
|> Unit.maybe_translatable_unit()
%{unit_1 | unit: unit_name, value: new_value, base_conversion: new_conversion}
end
defp product(%Unit{base_conversion: conv_1} = unit_1, %Unit{base_conversion: conv_2} = unit_2)
when is_simple_unit(conv_1) and is_simple_unit(conv_2) do
new_conversion =
(conv_1 ++ conv_2)
|> Enum.sort(&Parser.unit_sorter/2)
|> combine_power_instances()
new_value = Conversion.mult(unit_1.value, unit_2.value)
unit_name =
new_conversion
|> Parser.canonical_unit_name()
|> Unit.maybe_translatable_unit()
%{unit_1 | unit: unit_name, value: new_value, base_conversion: new_conversion}
end
# Invert a unit. This is used to convert a division
# into a multiplication. Its not a valid standalone
# unit.
@doc false
def invert({numerator, denominator}) do
{denominator, numerator}
end
def invert(numerator) do
Map.put(null_unit(), :base_conversion, {[], numerator.base_conversion})
end
@doc false
def null_unit do
%Cldr.Unit{unit: nil, value: 1, usage: :default, format_options: [], base_conversion: []}
end
# Combine consecutive identical units into square or cubic units.
# Assumes the units are ordered using `Parser.unit_sorter/2`.
defp combine_power_instances({numerator, denominator}) do
{combine_power_instances(numerator), combine_power_instances(denominator)}
end
defp combine_power_instances([{name, conversion} = first, first, first | rest]) do
conversion_factor = Conversion.pow(conversion.factor, 3)
conversion_base_unit = [:cubic | conversion.base_unit]
new_conversion = %{conversion | factor: conversion_factor, base_unit: conversion_base_unit}
new_name = Unit.maybe_translatable_unit("cubic_#{name}")
combine_power_instances([{new_name, new_conversion} | rest])
end
defp combine_power_instances([{name, conversion} = first, first | rest]) do
conversion_factor = Conversion.mult(conversion.factor, conversion.factor)
conversion_base_unit = [:square | conversion.base_unit]
new_conversion = %{conversion | factor: conversion_factor, base_unit: conversion_base_unit}
new_name = Unit.maybe_translatable_unit("square_#{name}")
combine_power_instances([{new_name, new_conversion} | rest])
end
defp combine_power_instances([first | rest]) do
[first | combine_power_instances(rest)]
end
defp combine_power_instances([]) do
[]
end
defp combine_power_instances(other) do
other
end
end