Packages
expression
2.1.1
2.49.1
2.49.0
2.48.0
2.47.5
2.47.4
2.47.3
2.47.2
2.47.1
2.47.0
2.46.0
2.45.1
2.45.0
2.44.1
2.44.0
2.43.0
2.42.1
2.41.4
2.41.3
2.41.2
2.41.0
2.40.0
2.38.0
2.37.0
2.36.2
2.36.1
2.36.0
2.35.0
2.34.0
2.33.1
2.33.0
2.32.1
2.31.6
2.31.3
2.31.0
2.30.0
2.28.0
2.27.3
2.27.2
2.27.1
2.27.0
2.26.1
2.26.0
2.25.0
2.24.2
2.24.1
2.24.0
2.23.9
2.23.8
2.23.7
2.23.6
2.23.4
2.23.3
2.23.2
2.23.1
2.23.0
2.22.2
2.22.0
2.21.0
2.20.0
2.19.0
2.18.0
2.17.0
2.16.0
2.14.0
2.13.0
2.12.0
2.11.0
2.10.1
2.10.0
2.9.0
2.8.0
2.6.0
2.5.8
2.5.7
2.5.6
2.5.5
2.5.4
2.5.3
2.5.2
2.5.1
2.5.0
2.4.1
2.4.0
2.3.2
2.3.1
2.3.0
2.2.1
2.2.0
2.1.1
2.1.0
2.0.5
2.0.4
2.0.3
2.0.2
2.0.1
2.0.0
1.1.1
1.1.0
1.0.0
0.7.2
0.7.1
0.7.0
0.6.0
0.5.0
0.4.3
0.4.2
0.4.1
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.1
0.1.0
A Excel like expression parser, compatible with FLOIP Expression language.
Current section
Files
Jump to
Current section
Files
lib/expression/eval.ex
defmodule Expression.Eval do
@moduledoc """
Expression.Eval is responsible for taking an abstract syntax
tree (AST) as generated by Expression.Parser and evaluating it.
At a high level, an AST consists of a Keyword list with two top-level
keys, either `:text` or `:expression`.
`Expression.eval!/3` will return the output for each entry in the Keyword
list. `:text` entries are returned as regular strings. `:expression` entries
are returned as typed values.
The returned value is a list containing each.
# Example
iex(1)> Expression.Eval.eval!([text: "hello"], %{})
["hello"]
iex(2)> Expression.Eval.eval!([text: "hello", expression: [literal: 1]], %{})
["hello", 1]
iex(3)> Expression.Eval.eval!([
...(3)> text: "hello",
...(3)> expression: [literal: 1],
...(3)> text: "ok",
...(3)> expression: [literal: true]
...(3)> ], %{})
["hello", 1, "ok", true]
"""
def eval!(ast, context, mod \\ Expression.Callbacks)
def eval!({:expression, [ast]}, context, mod) do
eval!(ast, context, mod)
end
def eval!({:atom, atom}, {:not_found, history}, _mod),
do: {:not_found, history ++ [atom]}
def eval!({:atom, atom}, context, _mod) do
Map.get(context, atom, {:not_found, [atom]})
end
def eval!({:attribute, [{:attribute, ast}, literal: literal]}, context, mod) do
# When we receive a key for an attribute, at times this could be a literal.
# The assumption is that all attributes are going to be string based so if we receive
# "@foo.123.bar", `123` will be parsed as a literal but the assumption is that the
# context will look like:
#
# %{"foo" => %{
# "123" => %{ <--- notice the string key here
# "bar" => "the value"
# }
# }}
eval!({:attribute, [{:attribute, ast}, atom: to_string(literal)]}, context, mod)
end
def eval!({:attribute, ast}, context, mod) do
Enum.reduce(ast, context, &eval!(&1, &2, mod))
end
def eval!({:function, opts}, context, mod) do
name = opts[:name] || raise "Functions need a name"
arguments = opts[:args] || []
case mod.handle(name, arguments, context) do
{:ok, value} -> value
{:error, reason} -> "ERROR: #{inspect(reason)}"
end
end
def eval!({:lambda, [{:args, ast}]}, context, mod) do
fn arguments ->
lambda_context = Map.put(context, "__captures", arguments)
eval!(ast, lambda_context, mod)
end
end
def eval!({:capture, index}, context, _mod) do
Enum.at(Map.get(context, "__captures"), index - 1)
end
def eval!({:range, [first, last]}, _context, _mod),
do: Range.new(first, last)
def eval!({:range, [first, last, step]}, _context, _mod),
do: Range.new(first, last, step)
def eval!({:list, [{:args, ast}]}, context, mod) do
ast
|> Enum.reduce([], &[eval!(&1, context, mod) | &2])
|> Enum.reverse()
|> Enum.map(¬_founds_as_nil/1)
end
def eval!({:key, [subject_ast, key_ast]}, context, mod) do
subject = eval!(subject_ast, context, mod)
key = eval!(key_ast, context, mod)
case key do
index when is_number(index) -> get_in(subject, [Access.at(index)])
range when is_struct(range, Range) -> Enum.slice(subject, range)
end
end
def eval!({:literal, literal}, _context, _mod), do: literal
def eval!({:text, text}, _context, _mod), do: text
def eval!({:+, [a, b]}, ctx, mod), do: eval!(a, ctx, mod, :num) + eval!(b, ctx, mod, :num)
def eval!({:-, [a, b]}, ctx, mod), do: eval!(a, ctx, mod, :num) - eval!(b, ctx, mod, :num)
def eval!({:*, [a, b]}, ctx, mod), do: eval!(a, ctx, mod, :num) * eval!(b, ctx, mod, :num)
def eval!({:/, [a, b]}, ctx, mod), do: eval!(a, ctx, mod, :num) / eval!(b, ctx, mod, :num)
def eval!({:>, [a, b]}, ctx, mod), do: eval!(a, ctx, mod, :num) > eval!(b, ctx, mod, :num)
def eval!({:>=, [a, b]}, ctx, mod), do: eval!(a, ctx, mod, :num) >= eval!(b, ctx, mod, :num)
def eval!({:<, [a, b]}, ctx, mod), do: eval!(a, ctx, mod, :num) < eval!(b, ctx, mod, :num)
def eval!({:<=, [a, b]}, ctx, mod), do: eval!(a, ctx, mod, :num) <= eval!(b, ctx, mod, :num)
def eval!({:==, [a, b]}, ctx, mod), do: eval!(a, ctx, mod) == eval!(b, ctx, mod)
def eval!({:!=, [a, b]}, ctx, mod), do: eval!(a, ctx, mod) != eval!(b, ctx, mod)
def eval!({:^, [a, b]}, ctx, mod), do: :math.pow(eval!(a, ctx, mod), eval!(b, ctx, mod))
def eval!({:&, [a, b]}, ctx, mod), do: [a, b] |> Enum.map_join("", &eval!(&1, ctx, mod))
def eval!(ast, context, mod) do
result =
ast
|> Enum.reduce([], fn ast, acc -> [eval!(ast, context, mod) | acc] end)
|> Enum.reverse()
case result do
[result] -> result
chunks -> chunks
end
end
def not_founds_as_nil({:not_found, _}), do: nil
def not_founds_as_nil(other), do: other
defp eval!(ast, ctx, mod, type), do: ast |> eval!(ctx, mod) |> guard_type!(type)
defp guard_type!(v, :num) when is_number(v) or is_struct(v, Decimal), do: v
defp guard_type!({:not_found, attributes}, :num),
do: raise("attribute is not found: `#{Enum.join(attributes, ".")}`")
defp guard_type!(v, :num), do: raise("expression is not a number: `#{inspect(v)}`")
end