Skip to content

Instantly share code, notes, and snippets.

@rogerleite
Created April 30, 2021 22:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rogerleite/20173fa3bf982c5d5c1f97b1b7a99525 to your computer and use it in GitHub Desktop.
Save rogerleite/20173fa3bf982c5d5c1f97b1b7a99525 to your computer and use it in GitHub Desktop.
Interpret and evaluate arithmetic expressions written in plain English (Livebook Elixir)

Arithmetic expressions in plain English

Interpret and evaluate arithmetic expressions written in plain English

Example: "one plus two times four"

  • numbers are [zero-ten]
  • numbers can be negative, for example: "negative five"
  • "plus" and "times" are the only supported operations natural order of operations apply, multiply before add.
  • "negative" is optional string before the number and is not an operation "one minus two" is expressed as "one plus negative two"

You are not allowed to use eval function that is built in the language macros or compiler internals external interpreter.

# Solution based on: https://panda.ime.usp.br/pythonds/static/pythonds_pt/02-EDBasicos/InfixPrefixandPostfixExpressions.html
defmodule Plain do
  @operators ["plus", "times"]
  @operands [
    "negative",
    "zero",
    "one",
    "two",
    "three",
    "four",
    "five",
    "six",
    "seven",
    "eight",
    "nine",
    "ten"
  ]
  @numbers %{
    "one" => 1,
    "two" => 2,
    "three" => 3,
    "four" => 4,
    "five" => 5,
    "six" => 6,
    "seven" => 7,
    "eight" => 8,
    "nine" => 9,
    "ten" => 10
  }
  @precedence %{"times" => 3, "plus" => 2}

  defp token_type(token) do
    cond do
      token in @operators -> :operator
      token in @operands -> :operand
      true -> :unknown
    end
  end

  defp add_operator_with_precedence(new_operator, operators) do
    [new_operator | operators]
    |> Enum.map(&%{operator: &1, precedence: @precedence[&1]})
    |> Enum.sort_by(& &1.precedence, :desc)
    |> Enum.map(& &1.operator)
  end

  defp to_numbers(reverse_operands) do
    {values, _} =
      Enum.flat_map_reduce(reverse_operands, 1, fn operand, multiplier ->
        value = @numbers[operand]

        cond do
          is_nil(value) -> {[nil], -1}
          multiplier < 1 -> {[value * multiplier], 1}
          true -> {[value], 1}
        end
      end)

    values |> Enum.reject(&is_nil/1)
  end

  defp build_postfix(tokens) do
    [operands, operators] =
      Enum.reduce(tokens, [[], []], fn token, [operands, operators] ->
        case token_type(token) do
          :operand ->
            [[token | operands], operators]

          :operator ->
            [operands, add_operator_with_precedence(token, operators)]

          :unknown ->
            raise "Eval is aborted. Unknown token '#{token}' found."
        end
      end)

    Enum.concat(to_numbers(operands), operators)
  end

  defp split_by_operator(postfix) do
    Enum.reduce(postfix, %{op: nil, left: [], right: []}, fn token, acc ->
      case acc do
        %{op: nil, left: left} ->
          if token in @operators do
            Map.put(acc, :op, token)
          else
            Map.put(acc, :left, [token | left])
          end

        %{right: right} ->
          Map.put(acc, :right, [token | right])
      end
    end)
  end

  defp eval_postfix(postfix) do
    %{op: operator, left: left, right: right} = split_by_operator(postfix)

    [token1 | remain_left1] = left
    [token2 | remain_left2] = remain_left1

    result =
      case operator do
        "times" -> token1 * token2
        "plus" -> token1 + token2
      end

    remain_postfix =
      remain_left2
      |> Enum.concat([result])
      |> Enum.concat(right)

    if length(remain_postfix) > 1 do
      eval_postfix(remain_postfix)
    else
      result
    end
  end

  def evaluate(expression, options \\ []) do
    inspect_option = Keyword.get(options, :inspect, false)

    postfix = String.split(expression) |> build_postfix
    result = postfix |> eval_postfix

    if inspect_option,
      do: IO.puts("#{expression}:\n postfix: #{inspect(postfix)}\n  result: #{result}")

    result
  end
end

Plain.evaluate("one plus two times four") |> IO.puts()
Plain.evaluate("one plus negative two") |> IO.puts()
Plain.evaluate("two times ten") |> IO.puts()
# Plain.evaluate("five times negative ten plus two", inspect: true) |> IO.puts()
Plain.evaluate("five times negative ten plus two") |> IO.puts()

try do
  Plain.evaluate("one thousand plus two")
rescue
  ex ->
    IO.puts("one thousand plus two:\n" <> ex.message)
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment