lib/json_logic.ex

defmodule JsonLogic do
  @moduledoc """
  An Elixir implementation of [JsonLogic](http://jsonlogic.com/).
  """

  @falsey [0, "", [], nil, false]

  @doc """
  Resolves the JsonLogic. It accepts logic and data arguments as a map, and
  returns the results as a map.

  ## Examples

  ```elixir
  JsonLogic.resolve(nil)
  #=> nil

  JsonLogic.resolve(%{})
  #=> %{}

  JsonLogic.resolve(%{"var" => "key"}, %{"key" => "value"})
  #=> "value"

  JsonLogic.resolve(%{"var" => "nested.key"}, %{"nested" => %{"key" => "value"}})
  #=> "value"

  JsonLogic.resolve(%{"var" => ["none", "default"]}, %{"key" => "value"})
  #=> "default"

  JsonLogic.resolve(%{"var" => 0}, ~w{a b})
  #=> "a"

  JsonLogic.resolve(%{"==" => [1, 1]})
  #=> true

  JsonLogic.resolve(%{"==" => [0, 1]})
  #=> false

  JsonLogic.resolve(%{"!=" => [1, 1]})
  #=> false

  JsonLogic.resolve(%{"!=" => [0, 1]})
  #=> true

  JsonLogic.resolve(%{"===" => [1, 1]})
  #=> true

  JsonLogic.resolve(%{"===" => [1, 1.0]})
  #=> false

  JsonLogic.resolve(%{"===" => [1, %{"var" => "key"}]}, %{"key" => 1})
  #=> true

  JsonLogic.resolve(%{"!==" => [1, 1.0]})
  #=> true

  JsonLogic.resolve(%{"!==" => [1, 1]})
  #=> false

  JsonLogic.resolve(%{"!" => true})
  #=> false

  JsonLogic.resolve(%{"!" => false})
  #=> true

  JsonLogic.resolve(%{"if" => [true, "yes", "no" ]})
  #=> "yes"

  JsonLogic.resolve(%{"if" => [false, "yes", "no" ]})
  #=> "no"

  JsonLogic.resolve(%{"if" => [false, "unexpected", false, "unexpected", "default" ]})
  #=> "default"

  JsonLogic.resolve(%{"or" => [false, nil, "truthy"]})
  #=> "truthy"

  JsonLogic.resolve(%{"or" => ["first", "truthy"]})
  #=> "first"

  JsonLogic.resolve(%{"and" => [false, "falsy"]})
  #=> false

  JsonLogic.resolve(%{"and" => [true, 1, "truthy"]})
  #=> "truthy"

  JsonLogic.resolve(%{"max" => [1,2,3]})
  #=> 3

  JsonLogic.resolve(%{"min" => [1,2,3]})
  #=> 1

  JsonLogic.resolve(%{"<" => [0, 1]})
  #=> true

  JsonLogic.resolve(%{"<" => [1, 0]})
  #=> false

  JsonLogic.resolve(%{"<" => [0, 1, 2]})
  #=> true

  JsonLogic.resolve(%{"<" => [0, 2, 1]})
  #=> false

  JsonLogic.resolve(%{">" => [1, 0]})
  #=> true

  JsonLogic.resolve(%{">" => [0, 1]})
  #=> false

  JsonLogic.resolve(%{">" => [2, 1, 0]})
  #=> true

  JsonLogic.resolve(%{">" => [2, 0, 1]})
  #=> false

  JsonLogic.resolve(%{"<=" => [1, 1]})
  #=> true

  JsonLogic.resolve(%{"<=" => [1, 0]})
  #=> false

  JsonLogic.resolve(%{"<=" => [1, 1, 2]})
  #=> true

  JsonLogic.resolve(%{"<=" => [1, 0, 2]})
  #=> false

  JsonLogic.resolve(%{">=" => [1, 1]})
  #=> true

  JsonLogic.resolve(%{">=" => [0, 1]})
  #=> false

  JsonLogic.resolve(%{">=" => [1, 1, 0]})
  #=> true

  JsonLogic.resolve(%{">=" => [0, 1, 2]})
  #=> false

  JsonLogic.resolve(%{"+" => [1,2,3]})
  #=> 6

  JsonLogic.resolve(%{"+" => [2]})
  #=> 2

  JsonLogic.resolve(%{"-" => [7,4]})
  #=> 3

  JsonLogic.resolve(%{"-" => [2]})
  #=> -2

  JsonLogic.resolve(%{"*" => [2,3,4]})
  #=> 24

  JsonLogic.resolve(%{"/" => [5,2]})
  #=> 2.5

  JsonLogic.resolve(%{"%" => [7, 3]})
  #=> 1

  JsonLogic.resolve(%{"map" => [[1,2,3,4,5], %{"*" => [%{"var" => ""}, 2]}]})
  #=> [2,4,6,8,10]

  JsonLogic.resolve(%{"filter" => [[1,2,3,4,5], %{">" => [%{"var" => ""}, 2]}]})
  #=> [3,4,5]

  JsonLogic.resolve(%{"reduce" => [[1,2,3,4,5], %{"+" => [%{"var" => "current"}, %{"var" => "accumulator"}]}, 0]})
  #=> 15

  JsonLogic.resolve(%{"all" => [[1,2,3], %{">" => [%{"var" => ""}, 0]}]})
  #=> true

  JsonLogic.resolve(%{"all" => [[-1,2,3], %{">" => [%{"var" => ""}, 0]}]})
  #=> false

  JsonLogic.resolve(%{"none" => [[1,2,3], %{"<" => [%{"var" => ""}, 0 ]}]})
  #=> true

  JsonLogic.resolve(%{"none" => [[-1,2,3], %{"<" => [%{"var" => ""}, 0 ]}]})
  #=> false

  JsonLogic.resolve(%{"some" => [[-1,2,3], %{"<" => [%{"var" => ""}, 0 ]}]})
  #=> true

  JsonLogic.resolve(%{"some" => [[1,2,3], %{"<" => [%{"var" => ""}, 0 ]}]})
  #=> false

  JsonLogic.resolve(%{"in" => ["sub", "substring"]})
  #=> true

  JsonLogic.resolve(%{"in" => ["na", "substring"]})
  #=> false

  JsonLogic.resolve(%{"in" => ["a", ["a", "b", "c"]]})
  #=> true

  JsonLogic.resolve(%{"in" => ["z", ["a", "b", "c"]]})
  #=> false

  JsonLogic.resolve(%{"cat" => ["a", "b", "c"]})
  #=> "abc"

  JsonLogic.resolve(%{"log" => "string"})
  #=> "string"
  ```
  """
  @spec resolve(map()) :: term()
  @spec resolve(map(), map() | nil) :: term()
  def resolve(logic, data \\ nil)

  def resolve(logic, data) when map_size(logic) == 1 do
    operation_name =
      logic
      |> Map.keys()
      |> List.first()

    values =
      logic
      |> Map.values()
      |> List.first()

    operation(operation_name, values, data)
  end

  def resolve(logic, data) when is_list(logic) do
    operation("merge", logic, data)
  end

  def resolve(logic, _), do: logic

  defp operation("and", [first], data), do: resolve(first, data)

  defp operation("and", [first | rest], data) do
    case resolve(first, data) do
      resolved when resolved in @falsey ->
        resolved

      _resolved ->
        operation("and", rest, data)
    end
  end

  defp operation("or", [first], data), do: resolve(first, data)

  defp operation("or", [first | others], data) do
    case resolve(first, data) do
      resolved when resolved in @falsey ->
        operation("or", others, data)

      resolved ->
        resolved
    end
  end

  defp operation("==", [left, right], data) do
    {op1, op2} = cast_comparison_operator(resolve(left, data), resolve(right, data))
    equal_to(op1, op2)
  end

  defp operation("!=", [left, right], data) do
    {op1, op2} = cast_comparison_operator(resolve(left, data), resolve(right, data))

    !equal_to(op1, op2)
  end

  defp operation("===", [left, right], data) do
    resolve(left, data) === resolve(right, data)
  end

  defp operation("!==", [left, right], data) do
    resolve(left, data) !== resolve(right, data)
  end

  defp operation("!!", [condition], data) do
    case resolve(condition, data) do
      resolved when resolved in @falsey -> false
      _truthy -> true
    end
  end

  defp operation("!!", condition, data) do
    operation("!!", [condition], data)
  end

  defp operation("!", condition, data) do
    !operation("!!", condition, data)
  end

  defp operation("max", [], _data), do: nil

  defp operation("max", list, data) when is_list(list) do
    # When reducing the list of resolved json logic, we need to try to coerce it
    # to a numeric value in order to find the largest number, but we need to
    # ensure we return the resolved value, and not the coerced numeric value.
    list
    |> Enum.reduce_while([], fn element, acc ->
      case resolve(element, data) do
        %Decimal{} = resolved ->
          {:cont, [{resolved, resolved} | acc]}

        resolved when is_number(resolved) ->
          {:cont, [{resolved, resolved} | acc]}

        resolved when is_binary(resolved) ->
          if numeric_string?(resolved) do
            {:ok, parsed} = parse_number(resolved)
            {:cont, [{resolved, parsed} | acc]}
          else
            {:halt, nil}
          end
      end
    end)
    |> case do
      nil ->
        nil

      otherwise ->
        otherwise
        |> Enum.max_by(fn {_, num} -> num end, &greater_than_equal_to/2)
        |> then(fn {resolved, _} -> resolved end)
    end
  end

  defp operation("min", [], _data), do: nil

  defp operation("min", list, data) when is_list(list) do
    # When reducing the list of resolved json logic, we need to try to coerce it
    # to a numeric value in order to find the smallest number, but we need to
    # ensure we return the resolved value, and not the coerced numeric value.
    list
    |> Enum.reduce_while([], fn element, acc ->
      case resolve(element, data) do
        %Decimal{} = resolved ->
          {:cont, [{resolved, resolved} | acc]}

        resolved when is_number(resolved) ->
          {:cont, [{resolved, resolved} | acc]}

        resolved when is_binary(resolved) ->
          if numeric_string?(resolved) do
            {:ok, parsed} = parse_number(resolved)
            {:cont, [{resolved, parsed} | acc]}
          else
            {:halt, nil}
          end
      end
    end)
    |> case do
      nil ->
        nil

      otherwise ->
        otherwise
        |> Enum.min_by(fn {_, num} -> num end, &less_than_equal_to/2)
        |> then(fn {resolved, _} -> resolved end)
    end
  end

  defp operation("?:", logic, data), do: operation("if", logic, data)

  defp operation("if", [], _data), do: nil
  defp operation("if", [last], data), do: resolve(last, data)

  defp operation("if", [condition, yes], data) do
    case resolve(condition, data) do
      resolved when resolved in @falsey ->
        nil

      _resolved ->
        resolve(yes, data)
    end
  end

  defp operation("if", [condition, yes, no], data) do
    case resolve(condition, data) do
      resolved when resolved in @falsey ->
        resolve(no, data)

      _resolved ->
        resolve(yes, data)
    end
  end

  defp operation("if", [condition, yes | others], data) do
    case resolve(condition, data) do
      resolved when resolved in @falsey ->
        operation("if", others, data)

      _resolved ->
        resolve(yes, data)
    end
  end

  defp operation("missing", keys, data) when is_list(keys) and is_map(data) do
    Enum.filter(keys, &(operation("var", [&1, :missing], data) == :missing))
  end

  defp operation("missing", keys, _data) when is_list(keys), do: keys

  defp operation("missing", keys, data) do
    case resolve(keys, data) do
      resolved when is_list(resolved) ->
        operation("missing", resolved, data)

      resolved ->
        operation("missing", [resolved], data)
    end
  end

  defp operation("missing_some", [min, keys], data) do
    case operation("missing", keys, data) do
      list when length(keys) - length(list) < min ->
        list

      _otherwise ->
        []
    end
  end

  defp operation("<", [left, right], data) do
    case cast_comparison_operator(resolve(left, data), resolve(right, data)) do
      {nil, nil} -> true
      {nil, _op2} -> false
      {_op1, nil} -> false
      {op1, op2} -> less_than(op1, op2)
    end
  end

  defp operation("<", [left, middle, right | _], data) do
    operation("<", [left, middle], data) &&
      operation("<", [middle, right], data)
  end

  defp operation("<=", [left, right], data) do
    case cast_comparison_operator(resolve(left, data), resolve(right, data)) do
      {nil, nil} -> true
      {nil, _op2} -> false
      {_op1, nil} -> false
      {op1, op2} -> less_than_equal_to(op1, op2)
    end
  end

  defp operation("<=", [left, middle, right | _], data) do
    operation("<=", [left, middle], data) &&
      operation("<=", [middle, right], data)
  end

  defp operation(">", [left, right], data) do
    case cast_comparison_operator(resolve(left, data), resolve(right, data)) do
      {nil, nil} -> true
      {nil, _op2} -> false
      {_op1, nil} -> false
      {op1, op2} -> greater_than(op1, op2)
    end
  end

  defp operation(">", [left, middle, right | _], data) do
    operation(">", [left, middle], data) &&
      operation(">", [middle, right], data)
  end

  defp operation(">=", [left, right], data) do
    case cast_comparison_operator(resolve(left, data), resolve(right, data)) do
      {nil, nil} -> true
      {nil, _op2} -> false
      {_op1, nil} -> false
      {op1, op2} -> greater_than_equal_to(op1, op2)
    end
  end

  defp operation(">=", [left, middle, right | _], data) do
    operation(">=", [left, middle], data) &&
      operation(">=", [middle, right], data)
  end

  defp operation("+", [], _data), do: 0

  defp operation("+", numbers, data) when is_list(numbers) do
    numbers
    |> Enum.map(&resolve(&1, data))
    |> Enum.reduce(0, &add/2)
  end

  defp operation("+", numbers, data), do: operation("+", [numbers], data)

  defp operation("-", [], _data), do: nil

  defp operation("-", [first, last], data) do
    {op1, op2} = cast_comparison_operator(resolve(first, data), resolve(last, data))
    subtract(op1, op2)
  end

  defp operation("-", [first], data) do
    case resolve(first, data) do
      resolved when is_number(resolved) ->
        -resolved

      resolved ->
        if numeric_string?(resolved) do
          {:ok, parsed} = parse_number(resolved)
          -parsed
        else
          raise ArgumentError, "Unsupported operation `-` for `#{first}`"
        end
    end
  end

  defp operation("*", numbers, data) do
    numbers
    |> Enum.map(&resolve(&1, data))
    |> Enum.reduce_while(1, fn
      str, total when is_binary(str) ->
        if numeric_string?(str) do
          {:ok, num} = parse_number(str)
          {:cont, multiply(num, total)}
        else
          {:halt, nil}
        end

      num, total ->
        {:cont, multiply(num, total)}
    end)
  end

  defp operation("/", [first, last], data) do
    {op1, op2} = cast_comparison_operator(resolve(first, data), resolve(last, data))
    divide(op1, op2)
  end

  defp operation("%", [first, last], data) do
    remainder(resolve(first, data), resolve(last, data))
  end

  defp operation("map", [list, map_action], data) do
    case resolve(list, data) do
      resolved when is_list(resolved) ->
        Enum.map(resolved, &resolve(map_action, &1))

      _resolved ->
        []
    end
  end

  defp operation("filter", [list, filter_action], data) do
    list
    |> resolve(data)
    |> Enum.filter(&operation("!!", filter_action, &1))
  end

  defp operation("reduce", [list, reduce_action], data) do
    operation("reduce", [list, reduce_action, nil], data)
  end

  defp operation("reduce", [list, reduce_action, first], data) do
    first_resolved = resolve(first, data)

    case resolve(list, data) do
      resolved when is_list(resolved) ->
        Enum.reduce(resolved, first_resolved, fn item, accumulator ->
          resolve(reduce_action, %{"current" => item, "accumulator" => accumulator})
        end)

      _resolved ->
        first
    end
  end

  defp operation("all", [list, test], data) do
    case resolve(list, data) do
      [] ->
        false

      resolved when is_list(resolved) ->
        Enum.all?(resolved, &resolve(test, &1))

      _resolved ->
        false
    end
  end

  defp operation("none", [list, test], data) do
    list
    |> resolve(data)
    |> Enum.all?(fn item -> Kernel.if(resolve(test, item), do: false, else: true) end)
  end

  defp operation("some", [list, test], data) do
    list
    |> resolve(data)
    |> Enum.any?(&resolve(test, &1))
  end

  defp operation("merge", [], _data), do: []

  defp operation("merge", [element | rest], data) do
    case resolve(element, data) do
      list when is_list(list) ->
        list ++ operation("merge", rest, data)

      element ->
        [element | operation("merge", rest, data)]
    end
  end

  defp operation("merge", element, data), do: [resolve(element, data)]

  defp operation("in", [member, list], data) when is_list(list) do
    list
    |> Enum.map(&resolve(&1, data))
    |> Enum.member?(resolve(member, data))
  end

  defp operation("in", [substring, string], data) when is_binary(string) do
    String.contains?(string, resolve(substring, data))
  end

  defp operation("in", [_, nil], _), do: false

  defp operation("in", [member, list], _) when not is_map(list) do
    raise ArgumentError, "Cannot apply `in` to non-enumerable: `#{inspect([member, list])}`"
  end

  defp operation("in", [find, from], data) do
    operation("in", [resolve(find, data), resolve(from, data)], data)
  end

  defp operation("cat", list, data) when is_list(list) do
    Enum.map_join(list, "", &resolve(&1, data))
  end

  defp operation("cat", string, data) do
    string
    |> resolve(data)
    |> to_string()
  end

  defp operation("substr", [string, offset], data) do
    string
    |> resolve(data)
    |> String.slice(offset..-1)
  end

  defp operation("substr", [string, offset, length], data) when length >= 0 do
    string
    |> resolve(data)
    |> String.slice(offset, length)
  end

  defp operation("substr", [string, offset, length], data) do
    string
    |> resolve(data)
    |> String.slice(offset..(length - 1))
  end

  defp operation("var", "", data), do: data

  defp operation("var", [path, default_key], data) do
    operation("var", path, data) || resolve(default_key, data)
  end

  defp operation("var", [path], data) do
    operation("var", path, data)
  end

  defp operation("var", path, data) when not is_number(path) do
    case resolve(path, data) do
      string when is_binary(string) ->
        string
        |> String.split(".")
        |> Enum.reduce(data, fn
          _key, nil ->
            nil

          key, acc when is_list(acc) ->
            {index, _} = Integer.parse(key)
            Enum.at(acc, index)

          key, acc when is_map(acc) ->
            Map.get(acc, key)

          _key, _acc ->
            nil
        end)

      _otherwise ->
        data
    end
  end

  defp operation("var", index, data) when is_number(index) do
    Enum.at(data, index)
  end

  defp operation("log", logic, data), do: resolve(logic, data)

  defp operation(name, _logic, _data),
    do: raise(ArgumentError, "Unrecognized operation `#{name}`")

  defp cast_comparison_operator(left, right) when is_number(left) and is_binary(right) do
    if numeric_string?(right) do
      case parse_number(right) do
        {:ok, parsed} ->
          {left, parsed}

        _ ->
          raise ArgumentError, "Unable to parse number `#{right}`"
      end
    else
      {left, right}
    end
  end

  defp cast_comparison_operator(left, right) when is_binary(left) and is_number(right) do
    if numeric_string?(left) do
      case parse_number(left) do
        {:ok, parsed} ->
          {parsed, right}

        _ ->
          raise ArgumentError, "Unable to parse number `#{left}`"
      end
    else
      {left, right}
    end
  end

  defp cast_comparison_operator(left, right) when is_binary(left) and is_binary(right) do
    if numeric_string?(left) and numeric_string?(right) do
      with {:ok, left} <- parse_number(left),
           {:ok, right} <- parse_number(right) do
        {left, right}
      else
        :error ->
          raise ArgumentError, "Unsupported numeric values `#{left}` and `#{right}`"
      end
    else
      {left, right}
    end
  end

  defp cast_comparison_operator(left, right), do: {left, right}

  @numeric_regex ~r/^[\+-]?(\d+)((\.((\d+)([eE][\-\+]?(\d+))?)?))?$/
  defp numeric_string?(value), do: String.match?(value, @numeric_regex)

  defp parse_number(value) do
    case Integer.parse(value) do
      {integer, ""} ->
        {:ok, integer}

      _ ->
        case Float.parse(value) do
          {float, ""} ->
            {:ok, float}

          {float, "."} ->
            {:ok, float}

          _ ->
            :error
        end
    end
  end

  defp less_than(left, right) when is_float(left) and is_float(right),
    do: left < right

  defp less_than(left, right) when is_float(left),
    do: less_than(Decimal.from_float(left), right)

  defp less_than(left, right) when is_float(right),
    do: less_than(left, Decimal.from_float(right))

  defp less_than(left, right) when is_integer(left),
    do: less_than(Decimal.new(left), right)

  defp less_than(left, right) when is_integer(right),
    do: less_than(left, Decimal.new(right))

  defp less_than(left, right) when is_binary(left) do
    if numeric_string?(left) do
      less_than(Decimal.new(left), right)
    else
      false
    end
  end

  defp less_than(left, right) when is_binary(right) do
    if numeric_string?(right) do
      less_than(left, Decimal.new(right))
    else
      false
    end
  end

  defp less_than(%Decimal{} = left, %Decimal{} = right) do
    case Decimal.compare(left, right) do
      :lt -> true
      :eq -> false
      _ -> false
    end
  end

  defp less_than(left, right), do: left < right

  defp less_than_equal_to(left, right) when is_float(left) and is_float(right),
    do: left <= right

  defp less_than_equal_to(left, right) when is_float(left),
    do: less_than_equal_to(Decimal.from_float(left), right)

  defp less_than_equal_to(left, right) when is_float(right),
    do: less_than_equal_to(left, Decimal.from_float(right))

  defp less_than_equal_to(left, right) when is_integer(left),
    do: less_than_equal_to(Decimal.new(left), right)

  defp less_than_equal_to(left, right) when is_integer(right),
    do: less_than_equal_to(left, Decimal.new(right))

  defp less_than_equal_to(left, right) when is_binary(left) do
    if numeric_string?(left) do
      less_than_equal_to(Decimal.new(left), right)
    else
      false
    end
  end

  defp less_than_equal_to(left, right) when is_binary(right) do
    if numeric_string?(right) do
      less_than_equal_to(left, Decimal.new(right))
    else
      false
    end
  end

  defp less_than_equal_to(%Decimal{} = left, %Decimal{} = right) do
    case Decimal.compare(left, right) do
      :lt -> true
      :eq -> true
      _ -> false
    end
  end

  defp less_than_equal_to(left, right), do: left <= right

  defp greater_than(left, right) when is_float(left) and is_float(right),
    do: left > right

  defp greater_than(left, right) when is_float(left),
    do: greater_than(Decimal.from_float(left), right)

  defp greater_than(left, right) when is_float(right),
    do: greater_than(left, Decimal.from_float(right))

  defp greater_than(left, right) when is_integer(left),
    do: greater_than(Decimal.new(left), right)

  defp greater_than(left, right) when is_integer(right),
    do: greater_than(left, Decimal.new(right))

  defp greater_than(left, right) when is_binary(left) do
    if numeric_string?(left) do
      greater_than(Decimal.new(left), right)
    else
      false
    end
  end

  defp greater_than(left, right) when is_binary(right) do
    if numeric_string?(right) do
      greater_than(left, Decimal.new(right))
    else
      false
    end
  end

  defp greater_than(%Decimal{} = left, %Decimal{} = right) do
    case Decimal.compare(left, right) do
      :gt -> true
      :eq -> false
      _ -> false
    end
  end

  defp greater_than(left, right), do: left > right

  defp greater_than_equal_to(left, right) when is_float(left) and is_float(right),
    do: left >= right

  defp greater_than_equal_to(left, right) when is_float(left),
    do: greater_than_equal_to(Decimal.from_float(left), right)

  defp greater_than_equal_to(left, right) when is_float(right),
    do: greater_than_equal_to(left, Decimal.from_float(right))

  defp greater_than_equal_to(left, right) when is_integer(left),
    do: greater_than_equal_to(Decimal.new(left), right)

  defp greater_than_equal_to(left, right) when is_integer(right),
    do: greater_than_equal_to(left, Decimal.new(right))

  defp greater_than_equal_to(left, right) when is_binary(left) do
    if numeric_string?(left) do
      greater_than_equal_to(Decimal.new(left), right)
    else
      false
    end
  end

  defp greater_than_equal_to(left, right) when is_binary(right) do
    if numeric_string?(right) do
      greater_than_equal_to(left, Decimal.new(right))
    else
      false
    end
  end

  defp greater_than_equal_to(%Decimal{} = left, %Decimal{} = right) do
    case Decimal.compare(left, right) do
      :gt -> true
      :eq -> true
      _ -> false
    end
  end

  defp greater_than_equal_to(left, right), do: left >= right

  defp equal_to(left, right) when is_float(left),
    do: equal_to(Decimal.from_float(left), right)

  defp equal_to(left, right) when is_float(right),
    do: equal_to(left, Decimal.from_float(right))

  defp equal_to(left, right) when is_integer(left),
    do: equal_to(Decimal.new(left), right)

  defp equal_to(left, right) when is_integer(right),
    do: equal_to(left, Decimal.new(right))

  defp equal_to(left, right) when is_binary(left) do
    if numeric_string?(left) do
      equal_to(Decimal.new(left), right)
    else
      left == right
    end
  end

  defp equal_to(left, right) when is_binary(right) do
    if numeric_string?(right) do
      equal_to(left, Decimal.new(right))
    else
      left == right
    end
  end

  defp equal_to(left, right) when is_integer(left) and is_integer(right),
    do: left == right

  defp equal_to(%Decimal{} = left, %Decimal{} = right),
    do: Decimal.compare(left, right) == :eq

  defp equal_to(left, right), do: left == right

  defp multiply(%Decimal{} = left, right) when is_float(right),
    do: multiply(left, Decimal.from_float(right))

  defp multiply(%Decimal{} = left, right) when is_integer(right),
    do: multiply(left, Decimal.new(right))

  defp multiply(left, %Decimal{} = right) when is_float(left),
    do: multiply(Decimal.from_float(left), right)

  defp multiply(left, %Decimal{} = right) when is_integer(left),
    do: multiply(Decimal.new(left), right)

  defp multiply(%Decimal{} = left, %Decimal{} = right),
    do: Decimal.mult(left, right)

  defp multiply(left, right) when is_binary(left) do
    if numeric_string?(left) do
      {:ok, parsed} = parse_number(left)
      multiply(parsed, right)
    end
  end

  defp multiply(left, right) when is_binary(right) do
    if numeric_string?(right) do
      {:ok, parsed} = parse_number(right)
      multiply(left, parsed)
    end
  end

  defp multiply(left, right), do: left * right

  defp divide(%Decimal{} = left, right) when is_float(right),
    do: divide(left, Decimal.from_float(right))

  defp divide(%Decimal{} = left, right) when is_integer(right),
    do: divide(left, Decimal.new(right))

  defp divide(left, %Decimal{} = right) when is_float(left),
    do: divide(Decimal.from_float(left), right)

  defp divide(left, %Decimal{} = right) when is_integer(left),
    do: divide(Decimal.new(left), right)

  defp divide(%Decimal{} = left, %Decimal{} = right),
    do: Decimal.div(left, right)

  defp divide(left, right) when is_binary(left) do
    if numeric_string?(left) do
      {:ok, parsed} = parse_number(left)
      divide(parsed, right)
    end
  end

  defp divide(left, right) when is_binary(right) do
    if numeric_string?(right) do
      {:ok, parsed} = parse_number(right)
      divide(left, parsed)
    end
  end

  defp divide(left, right), do: left / right

  defp subtract(left, right) when is_float(left),
    do: subtract(Decimal.from_float(left), right)

  defp subtract(left, right) when is_float(right),
    do: subtract(left, Decimal.from_float(right))

  defp subtract(%Decimal{} = left, right) when is_integer(right),
    do: subtract(left, Decimal.new(right))

  defp subtract(left, %Decimal{} = right) when is_integer(left),
    do: subtract(Decimal.new(left), right)

  defp subtract(%Decimal{} = left, %Decimal{} = right),
    do: Decimal.sub(left, right)

  defp subtract(left, right) when is_binary(left) do
    if numeric_string?(left) do
      {:ok, parsed} = parse_number(left)
      subtract(parsed, right)
    end
  end

  defp subtract(left, right) when is_binary(right) do
    if numeric_string?(right) do
      {:ok, parsed} = parse_number(right)
      subtract(left, parsed)
    end
  end

  defp subtract(left, right), do: left - right

  defp add(left, right) when is_float(left),
    do: add(Decimal.from_float(left), right)

  defp add(left, right) when is_float(right),
    do: add(left, Decimal.from_float(right))

  defp add(left, %Decimal{} = right) when is_integer(left),
    do: add(Decimal.new(left), right)

  defp add(%Decimal{} = left, right) when is_integer(right),
    do: add(left, Decimal.new(right))

  defp add(%Decimal{} = left, %Decimal{} = right),
    do: Decimal.add(left, right)

  defp add(left, right) when is_binary(left) do
    if numeric_string?(left) do
      {:ok, parsed} = parse_number(left)
      add(parsed, right)
    end
  end

  defp add(left, right) when is_binary(right) do
    if numeric_string?(right) do
      {:ok, parsed} = parse_number(right)
      add(left, parsed)
    end
  end

  defp add(left, right), do: left + right

  defp remainder(dividend, devisor) when is_float(dividend),
    do: remainder(Decimal.from_float(dividend), devisor)

  defp remainder(dividend, devisor) when is_float(devisor),
    do: remainder(dividend, Decimal.from_float(devisor))

  defp remainder(%Decimal{} = dividend, devisor) when is_integer(devisor),
    do: remainder(dividend, Decimal.new(devisor))

  defp remainder(dividend, %Decimal{} = devisor) when is_integer(dividend),
    do: remainder(Decimal.new(dividend), devisor)

  defp remainder(%Decimal{} = dividend, %Decimal{} = devisor),
    do: Decimal.rem(dividend, devisor)

  defp remainder(dividend, devisor) when is_binary(dividend) do
    if numeric_string?(dividend) do
      {:ok, parsed} = parse_number(dividend)
      remainder(parsed, devisor)
    end
  end

  defp remainder(dividend, devisor) when is_binary(devisor) do
    if numeric_string?(devisor) do
      {:ok, parsed} = parse_number(devisor)
      remainder(dividend, parsed)
    end
  end

  defp remainder(dividend, devisor) do
    rem(dividend, devisor)
  end
end