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