defmodule Cldr.Unit.BaseUnit do @moduledoc """ Functions to support the base unit calculations for a unit. Base unit equality is used to determine whether a one unit can be converted to another """ alias Cldr.Unit.Conversion alias Cldr.Unit.Parser alias Cldr.Unit.Prefix alias Cldr.Unit @per "_per_" @currency_base Cldr.Unit.Parser.currency_base() @currencies Cldr.known_currencies() @inverted_base_units_name Cldr.Config.units() |> Map.get(:base_units) |> Kernel.++(Cldr.Unit.Additional.base_units()) |> Enum.uniq() |> Map.new() |> Map.values() @doc """ Returns the canonical base unit name for a unit. The base unit is the common unit through which conversions are passed. ## Arguments * `unit_string` is any string representing a unit such as `light_year_per_week`. ## Returns * `{:ok, canonical_base_unit}` or * `{:error, {exception, reason}}` ## Examples iex> Cldr.Unit.Parser.canonical_base_unit "meter" {:ok, :meter} iex> Cldr.Unit.Parser.canonical_base_unit "meter meter" {:ok, :square_meter} iex> Cldr.Unit.Parser.canonical_base_unit "meter per kilogram" {:ok, "meter_per_kilogram"} iex> Cldr.Unit.Parser.canonical_base_unit "yottagram per mile scandinavian" {:ok, "kilogram_per_meter"} """ def canonical_base_unit(unit) when is_binary(unit) do with {:ok, parsed} <- Parser.parse_unit(unit) do canonical_base_unit(parsed) end end # A "per" unit def canonical_base_unit({numerator, denominator}) do with numerator <- do_canonical_base_unit(numerator), denominator <- do_canonical_base_unit(denominator) do {numerator, denominator} |> merge_unit_names() |> sort_base_units() |> reduce_powers() |> reduce_factors() |> flatten_and_stringify() |> Unit.maybe_translatable_unit() |> wrap(:ok) end end # A list of conversions def canonical_base_unit(numerator) do numerator |> do_canonical_base_unit() |> flatten_and_stringify() |> Unit.maybe_translatable_unit() |> wrap(:ok) end def do_canonical_base_unit(numerator) when is_list(numerator) do numerator |> Enum.map(&canonical_base_subunit/1) |> resolve_unit_names() |> sort_base_units() |> reduce_powers() |> reduce_factors() end defp canonical_base_subunit({currency, _conversion}) when currency in @currencies do [String.downcase(@currency_base <> to_string(currency))] end defp canonical_base_subunit({_unit_name, %Conversion{base_unit: base_units}}) do base_units |> parse_base_units() |> extract_unit_names() end defp parse_base_units([prefix, unit]) do [[prefix, parse_base_units([unit])]] end defp parse_base_units([unit]) do unit |> to_string |> Cldr.Unit.normalize_unit_name() |> Parser.parse_unit!() end # Base units are either # A {numerator, denominator} tuple # A list of {unit, base_unit} tuples defp extract_unit_names({numerator, denominator}) do {extract_keys(numerator), extract_keys(denominator)} end defp extract_unit_names(numerator) do extract_keys(numerator) end # Extract the base units from the conversion # And simplify base units (ie unwrap them) defp extract_keys(list) do Enum.map(list, fn [prefix, conversion] -> [prefix, hd(extract_keys(conversion))] {_unit, conversion} -> conversion |> Map.fetch!(:base_unit) |> case do [unit] -> unit [prefix, unit] -> [prefix, unit] end end) end # Merge all list elements, starting with the first # two until the end of the list defp resolve_unit_names([first]) do first end defp resolve_unit_names([first, second | rest]) do resolve_unit_names([merge_unit_names(first, second) | rest]) end # Take two list elements and merge them noting that either # element might be a "per tuple" represented by a tuple defp merge_unit_names({numerator_a, denominator_a}, {numerator_b, denominator_b}) do {merge_unit_names(numerator_a, numerator_b), merge_unit_names(denominator_a, denominator_b)} end defp merge_unit_names({numerator_a, denominator_a}, numerator_b) do {merge_unit_names(numerator_a, numerator_b), denominator_a} end defp merge_unit_names(numerator_a, {numerator_b, denominator_b}) do {merge_unit_names(numerator_a, numerator_b), denominator_b} end defp merge_unit_names(numerator_a, numerator_b) do numerator_a ++ numerator_b end # Final pass for "per" base units defp merge_unit_names({{_numerator_a, _denominator_a}, {_numerator_b, _denominator_b}}) do raise ArgumentError, "unexpected" end defp merge_unit_names({{numerator_a, denominator_a}, numerator_b}) do {numerator_a, merge_unit_names(numerator_b, denominator_a)} end defp merge_unit_names({numerator_a, {numerator_b, denominator_b}}) do {merge_unit_names(numerator_a, denominator_b), numerator_b} end defp merge_unit_names(other) do other end # Sort the units in canonical order defp sort_base_units({numerator, denominator}) do {Enum.sort(numerator, &base_unit_sorter/2), Enum.sort(denominator, &base_unit_sorter/2)} end defp sort_base_units(numerator) do Enum.sort(numerator, &base_unit_sorter/2) end # Relies on base units only ever being a single unit # or a list with two elements being a prefix and a unit except # for a currency unit in which case it will be a binary of the # form `curr-usd` by the time we get here. And currency forms # always sort at the head of the list. defp base_unit_sorter(unit_a, unit_b) when is_atom(unit_a) and is_atom(unit_b) do Map.fetch!(base_units_in_order(), unit_a) < Map.fetch!(base_units_in_order(), unit_b) end defp base_unit_sorter(unit_a, [_prefix, unit_b]) when is_atom(unit_a) do Map.fetch!(base_units_in_order(), unit_a) < Map.fetch!(base_units_in_order(), unit_b) end defp base_unit_sorter([_prefix, unit_a], unit_b) when is_atom(unit_b) do Map.fetch!(base_units_in_order(), unit_a) < Map.fetch!(base_units_in_order(), unit_b) end defp base_unit_sorter([_prefix_a, unit_a], [_prefix_b, unit_b]) do Map.fetch!(base_units_in_order(), unit_a) < Map.fetch!(base_units_in_order(), unit_b) end defp base_unit_sorter(@currency_base <> _currency, _) do true end defp base_unit_sorter(_, @currency_base <> _currency) do false end # Compare 2 base unit names and return a comparison :eq, :lt, :gt. # Order is determined by the canonical order of units defined by CLDR # returned by base_units_in_order/0. defp compare([_power_1, unit_1], [_power_2, unit_2]) do compare(unit_1, unit_2) end defp compare([_power_1, unit_1], unit_2) do compare(unit_1, unit_2) end defp compare(unit_1, [_power_2, unit_2]) do compare(unit_1, unit_2) end defp compare(unit_1, unit_2) when is_atom(unit_1) and is_atom(unit_2) do order_1 = Map.fetch!(base_units_in_order(), unit_1) order_2 = Map.fetch!(base_units_in_order(), unit_2) cond do order_1 > order_2 -> :gt order_1 < order_2 -> :lt order_1 == order_2 -> :eq end end # Reduce factors. When its a "per" unit then # we reduce the common factors. # This is important to ensure that base unit # comparisons work correctly across different units # of the same type. @doc false def reduce_factors(list) when is_list(list) do list end def reduce_factors({numerator, denominator}) do str_numerator = flatten_and_stringify(numerator) str_denominator = flatten_and_stringify(denominator) if is_base_unit(str_numerator) || is_base_unit(str_denominator) do {numerator, denominator} else do_reduce_factors({numerator, denominator}) end end def do_reduce_factors({[], denominator}) do {[], denominator} end def do_reduce_factors({numerator, []}) do {numerator, []} end # Numerator and denominator cancel each other def do_reduce_factors({[unit | rest_1], [unit | rest_2]}) do do_reduce_factors({rest_1, rest_2}) end # When we have the same unit, but one of them is raised to a # power, we can reduce the power by one. This is true in both directions. def do_reduce_factors({[[power, unit] | rest_1], [unit | rest_2]}) do sub_unit = subtract_power([power, unit], 1) do_reduce_factors({[sub_unit | rest_1], rest_2}) end def do_reduce_factors({[unit | rest_1], [[power, unit] | rest_2]}) do sub_unit = subtract_power([power, unit], 1) do_reduce_factors({rest_1, [sub_unit | rest_2]}) end # Both units have powers so we subtract the denominator from the # numerator. def do_reduce_factors({[[power_1, unit] | rest_1], [[power_2, unit] | rest_2]}) do {sub_unit_1, sub_unit_2} = subtract_power([power_1, unit], [power_2, unit]) do_reduce_factors({[sub_unit_1 | rest_1], [sub_unit_2 | rest_2]}) end # No power units involved so we need to move along. # THe list of units is always in a canonical order. def do_reduce_factors({[unit_1 | rest_1], [unit_2 | rest_2]}) do cond do compare(unit_1, unit_2) == :lt -> {reduced_1, reduced_2} = do_reduce_factors({rest_1, [unit_2 | rest_2]}) {[unit_1 | reduced_1], reduced_2} compare(unit_1, unit_2) == :gt -> {reduced_1, reduced_2} = do_reduce_factors({[unit_1 | rest_1], rest_2}) {reduced_1, [unit_2 | reduced_2]} end end defp is_base_unit(unit) do maybe_base_unit = String.to_existing_atom(unit) maybe_base_unit in @inverted_base_units_name rescue ArgumentError -> false end # Reduce powers to square and cubic defp reduce_powers({numerator, denominator}) do {reduce_powers(numerator), reduce_powers(denominator)} end defp reduce_powers([first]) do [first] end defp reduce_powers([first, first | rest]) do reduce_powers([[:square, first] | rest]) end defp reduce_powers([[:square, first], first | rest]) do reduce_powers([[:cubic, first] | rest]) end defp reduce_powers([first, [:square, first] | rest]) do reduce_powers([[:cubic, first] | rest]) end defp reduce_powers([first | rest]) do [first | reduce_powers(rest)] end # Flaten the list and turn it into a string. defp flatten_and_stringify({[], denominator}) do flatten_and_stringify(denominator) end defp flatten_and_stringify({numerator, []}) do flatten_and_stringify(numerator) end defp flatten_and_stringify({numerator, denominator}) do flatten_and_stringify(numerator) <> @per <> flatten_and_stringify(denominator) end defp flatten_and_stringify(numerator) do numerator |> List.flatten() |> Enum.map(&to_string/1) |> Enum.join("_") end @doc """ Returns the canonical base unit name for a unit. The base unit is the common unit through which conversions are passed. ## Arguments * `unit_string` is any string representing a unit such as `light_year_per_week`. ## Returns * `canonical_base_unit` or * raises an exception ## Examples iex> Cldr.Unit.Parser.canonical_base_unit! "meter" :meter iex> Cldr.Unit.Parser.canonical_base_unit! "meter meter" :square_meter iex> Cldr.Unit.Parser.canonical_base_unit! "meter per kilogram" "meter_per_kilogram" iex> Cldr.Unit.Parser.canonical_base_unit! "yottagram per mile scandinavian" "kilogram_per_meter" """ def canonical_base_unit!(unit_string) when is_binary(unit_string) do case canonical_base_unit(unit_string) do {:ok, unit_name} -> unit_name {:error, {exception, reason}} -> raise exception, reason end end # We wrap in a tuple since a nested list can # create ambiguous processing in other places @doc false def wrap([numerator, denominator], tag) do {tag, {numerator, denominator}} end def wrap([numerator], tag) do {tag, numerator} end def wrap(other, tag) do {tag, other} end @units Cldr.Config.units() @base_units @units[:base_units] @base_units_in_order @base_units |> Cldr.Unit.Additional.merge_base_units() |> Enum.map(&elem(&1, 1)) |> Enum.with_index() |> Map.new() @doc false def base_units_in_order do @base_units_in_order end defp subtract_power([power, unit], number) when is_number(number) do exponent = Prefix.power_units()[power] - number power = Prefix.inverse_power_units()[exponent] if exponent == 1, do: unit, else: [power, unit] end defp subtract_power([pow_1, unit], [pow_2, unit]) do exp_1 = Prefix.power_units()[pow_1] exp_2 = Prefix.power_units()[pow_2] min = min(exp_1, exp_2) new_exp_1 = exp_1 - min new_exp_2 = exp_2 - min new_unit_1 = if new_exp_1 == 1, do: unit, else: [new_exp_1, unit] new_unit_2 = if new_exp_2 == 1, do: unit, else: [new_exp_2, unit] {new_unit_1, new_unit_2} end end