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