Packages
expression
2.36.0
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.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]
"""
alias Expression.DateHelpers
@numeric_kernel_operators [:+, :-, :*, :/, :>, :>=, :<, :<=]
@kernel_operators @numeric_kernel_operators ++ [:==, :!=]
@allowed_nested_function_arguments [:function, :lambda] ++ @kernel_operators
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) when is_map(context) do
Map.get(context, atom, {:not_found, [atom]})
end
def eval!({:atom, _atom}, _context, _mod), do: nil
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
function_name = opts[:name] || raise "Functions need a name"
args = opts[:args] || []
arguments = Enum.reduce_while(args, [], &args_reducer(&1, function_name, context, mod, &2))
case mod.handle(function_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, []}, _context, _mod), do: []
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)
read_key_from_subject(subject, key)
end
def eval!({:literal, literal}, context, mod) when is_binary(literal) do
Expression.evaluate_as_string!(literal, context, mod)
end
def eval!({:literal, literal}, _context, _mod), do: literal
def eval!({:text, text}, _context, _mod), do: text
def eval!({operator, [a, b]}, ctx, mod) when operator in @kernel_operators do
a = eval!(a, ctx, mod)
b = eval!(b, ctx, mod)
op(operator, a, b)
end
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
defp read_key_from_subject(_subject, {:not_found, _}), do: nil
defp read_key_from_subject(subject, index)
when is_number(index) and (is_list(subject) or is_map(subject)),
do: get_in(subject, [Access.at(index)])
defp read_key_from_subject(subject, range) when is_struct(range, Range) and is_list(subject),
do: Enum.slice(subject, range)
defp read_key_from_subject(subject, binary) when is_binary(binary) and is_map(subject),
do: Map.get(subject, binary)
defp read_key_from_subject(_subject, _other), do: nil
# when acting on integer or Elixir literal numeric types
def op(operator, a, b)
when operator in @numeric_kernel_operators and
(is_number(a) or is_float(a)) and
(is_number(b) or is_float(b)) do
[a, b] = Enum.map([a, b], &guard_type!(&1, :num))
apply(Kernel, operator, [a, b])
end
def op(:>, a, b) when is_struct(a, DateTime) and is_struct(b, DateTime),
do: DateTime.compare(a, b) == :gt
def op(:>=, a, b) when is_struct(a, DateTime) and is_struct(b, DateTime),
do: DateTime.compare(a, b) in [:gt, :eq]
def op(:<, a, b) when is_struct(a, DateTime) and is_struct(b, DateTime),
do: DateTime.compare(a, b) == :lt
def op(:<=, a, b) when is_struct(a, DateTime) and is_struct(b, DateTime),
do: DateTime.compare(a, b) in [:lt, :eq]
def op(:==, a, b) when is_struct(a, DateTime) and is_struct(b, DateTime),
do: DateTime.compare(a, b) == :eq
def op(:=, a, b) when is_struct(a, DateTime) and is_struct(b, DateTime),
do: Date.compare(a, b) == :eq
def op(:>, a, b) when is_struct(a, Date) and is_struct(b, Date),
do: Date.compare(a, b) == :gt
def op(:>=, a, b) when is_struct(a, Date) and is_struct(b, Date),
do: Date.compare(a, b) in [:gt, :eq]
def op(:<, a, b) when is_struct(a, Date) and is_struct(b, Date),
do: Date.compare(a, b) == :lt
def op(:<=, a, b) when is_struct(a, Date) and is_struct(b, Date),
do: Date.compare(a, b) in [:lt, :eq]
def op(:==, a, b) when is_struct(a, Date) and is_struct(b, Date),
do: Date.compare(a, b) == :eq
def op(:=, a, b) when is_struct(a, Date) and is_struct(b, Date),
do: Date.compare(a, b) == :eq
# Support comparing a `Date`/`DateTime` value with an ISO8601 date/datetime string
# credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity
def op(operator, a, b)
when operator in [:=, :==, :!=, :<, :<=, :>, :>=] and
(((is_struct(a, Date) or is_struct(a, DateTime)) and is_binary(b)) or
(is_binary(a) and (is_struct(b, Date) or is_struct(b, DateTime)))) do
{date_a, date_b} =
cond do
is_struct(a, Date) -> {a, DateHelpers.extract_dateish(b)}
is_struct(a, DateTime) -> {a, DateHelpers.extract_datetimeish(b)}
is_struct(b, Date) -> {DateHelpers.extract_dateish(a), b}
is_struct(b, DateTime) -> {DateHelpers.extract_datetimeish(a), b}
end
comparison_result =
if is_struct(date_a, Date) do
Date.compare(date_a, date_b)
else
DateTime.compare(date_a, date_b)
end
case {operator, comparison_result} do
{:=, :eq} -> true
{:==, :eq} -> true
{:!=, :lt} -> true
{:!=, :gt} -> true
{:<, :lt} -> true
{:<=, :lt} -> true
{:<=, :eq} -> true
{:>, :gt} -> true
{:>=, :gt} -> true
{:>=, :eq} -> true
_ -> false
end
end
# when acting on any other supported type but still expected to be numeric
def op(operator, a, b) when operator in @numeric_kernel_operators do
args =
[a, b]
|> Enum.map(&parse_number/1)
|> Enum.map(&guard_type!(&1, :num))
|> Enum.map(&default_value/1)
apply(Kernel, operator, args)
end
# just leave it to the Kernel to figure out at this stage
def op(operator, a, b) when operator in @kernel_operators do
args =
[a, b]
|> Enum.map(&parse_number/1)
|> Enum.map(&default_value/1)
apply(Kernel, operator, args)
end
@doc """
Parses a given value into a number, being an integer or a float.
If the given value is not able to be parsed, then it returns the raw value.
"""
def parse_number(%{"__value__" => value}), do: parse_number(value)
def parse_number(value) when is_binary(value) do
case Decimal.parse(value) do
{decimal, _} ->
if Decimal.integer?(decimal) do
Decimal.to_integer(decimal)
else
Decimal.to_float(decimal)
end
:error ->
value
end
end
def parse_number(value), do: value
@doc """
Return the default value for a potentially complex value.
Complex values can be Maps that have a `__value__` key, if that's
returned then we can to use the `__value__` value when eval'ing against
operators or functions.
"""
def default_value(val, opts \\ [])
def default_value(%{"__value__" => default_value}, _opts), do: default_value
def default_value({:not_found, attributes}, opts) do
if(opts[:handle_not_found], do: "@#{Enum.join(attributes, ".")}", else: nil)
end
def default_value(items, opts) when is_list(items),
do: Enum.map(items, &default_value(&1, opts))
def default_value(value, _opts), do: value
def not_founds_as_nil({:not_found, _}), do: nil
def not_founds_as_nil(other), do: other
defp guard_type!(v, :num) when is_number(v), do: v
defp guard_type!({:not_found, attributes}, :num),
do: raise("attribute is not found: `#{Enum.join(attributes, ".")}`")
defp guard_type!({:not_found, attributes}, _),
do: raise("attribute is not found: `#{Enum.join(attributes, ".")}`")
defp guard_type!(v, :num), do: raise("expression is not a number: `#{inspect(v)}`")
def handle_not_found({:not_found, _}), do: nil
def handle_not_found(value), do: value
defp args_reducer({type, _args} = function, function_name, context, mod, acc)
when type in @allowed_nested_function_arguments do
value = eval!(function, context, mod)
flag = if value == true && function_name == "or", do: :halt, else: :cont
{flag, acc ++ [[literal: value]]}
end
defp args_reducer(function, _function_name, _context, _mod, acc),
do: {:cont, acc ++ [function]}
end