Skip to content

Instantly share code, notes, and snippets.

@rogerleite
Created April 30, 2021 22:12
Show Gist options
  • Select an option

  • Save rogerleite/20173fa3bf982c5d5c1f97b1b7a99525 to your computer and use it in GitHub Desktop.

Select an option

Save rogerleite/20173fa3bf982c5d5c1f97b1b7a99525 to your computer and use it in GitHub Desktop.

Revisions

  1. rogerleite created this gist Apr 30, 2021.
    159 changes: 159 additions & 0 deletions PlainEnglish.livemd
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,159 @@
    # 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.

    ```elixir
    # 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
    ```