Packages

A native Elixir parser and evaluator for the Friendly Enough Expression Language defined by DMN.

Current section

Files

Jump to
boxic_feel lib boxic feel evaluator.ex
Raw

lib/boxic/feel/evaluator.ex

defmodule Boxic.FEEL.Evaluator do
@moduledoc false
alias Boxic.FEEL.{Builtins, Duration, Error, Function, Range, Semantics}
alias Boxic.FEEL.DateTime, as: FeelDateTime
alias Boxic.FEEL.Time, as: FeelTime
def evaluate_unary_test(test_expression, value, context) do
trimmed = String.trim(test_expression)
cond do
trimmed == "-" ->
{:ok, true}
String.starts_with?(trimmed, "not(") and String.ends_with?(trimmed, ")") ->
inner = trimmed |> String.slice(4, String.length(trimmed) - 5)
with {:ok, matches?} <- evaluate_unary_test(inner, value, context),
do: {:ok, not matches?}
comparator_unary_test?(trimmed) ->
eval_comparator_unary_test(trimmed, value, context)
range_unary_test?(trimmed) ->
with {:ok, range_value} <- Boxic.FEEL.evaluate(trimmed, context),
true <- match?(%Range{}, range_value) do
{:ok, range_contains?(range_value, value)}
else
false -> {:error, error(:type_error, "range unary test expected a range")}
{:error, %Error{} = e} -> {:error, e}
end
true ->
with {:ok, expected} <- Boxic.FEEL.evaluate(trimmed, context) do
if is_list(expected),
do: {:ok, Enum.any?(expected, &Semantics.equal?(value, &1))},
else: {:ok, Semantics.equal?(value, expected)}
end
end
end
def eval({:literal, value}, _context), do: {:ok, value}
def eval({:identifier, name}, context) do
case Map.fetch(context, name) do
{:ok, value} ->
{:ok, value}
:error ->
Builtins.resolve(name)
end
end
def eval({:list, items}, context) do
eval_list(items, context, [])
end
def eval({:context, entries}, context) do
eval_context_entries(entries, context, %{})
end
def eval({:path, expr, key}, context) do
with {:ok, value} <- eval(expr, context) do
case value do
list when is_list(list) ->
{:ok, Enum.map(list, fn item -> if is_map(item), do: Map.get(item, key), else: nil end)}
map when is_map(map) ->
{:ok, property_value(map, key)}
{:unary_test_value, operator, _operand} = unary_test
when operator in [:lt, :lte, :gt, :gte, :eq] ->
{:ok, property_value(unary_test, key)}
_ ->
{:ok, nil}
end
end
end
def eval({:filter, source_expr, predicate}, context) do
with {:ok, source} <- eval(source_expr, context) do
case source do
list when is_list(list) ->
eval_list_filter(list, predicate, context)
scalar ->
eval_scalar_filter(scalar, predicate, context)
end
end
end
def eval({:range, start_inclusive, end_inclusive, start_ast, end_ast}, context) do
with {:ok, start_value} <- eval(start_ast, context),
{:ok, end_value} <- eval(end_ast, context) do
{:ok,
%Range{
start: start_value,
end: end_value,
start_inclusive: start_inclusive,
end_inclusive: end_inclusive
}}
end
end
def eval({:if, condition_ast, then_ast, else_ast}, context) do
with {:ok, condition} <- eval(condition_ast, context) do
case condition do
true -> eval(then_ast, context)
false -> eval(else_ast, context)
nil -> eval(else_ast, context)
_ -> {:error, error(:type_error, "if condition must be boolean")}
end
end
end
def eval({:in, value_ast, tests_ast}, context) do
with {:ok, value} <- eval(value_ast, context) do
evaluate_in(value, tests_ast, context)
end
end
def eval({:between, value_ast, lower_ast, upper_ast}, context) do
with {:ok, value} <- eval(value_ast, context),
{:ok, lower} <- eval(lower_ast, context),
{:ok, upper} <- eval(upper_ast, context),
{:ok, above} <- eval_binary(:gte, value, lower),
{:ok, below} <- eval_binary(:lte, value, upper) do
{:ok, feel_and(above, below)}
end
end
def eval({:instance_of, value_ast, type}, context) do
with {:ok, value} <- eval(value_ast, context) do
{:ok, instance_of?(value, type, context)}
end
end
def eval({:unary_test, operator, operand_ast}, context) do
with {:ok, operand} <- eval(operand_ast, context) do
{:ok, {:unary_test_value, operator, operand}}
end
end
def eval({:sequence, first_ast, last_ast}, context) do
with {:ok, first} <- eval(first_ast, context),
{:ok, last} <- eval(last_ast, context) do
{:ok, {:sequence, first, last}}
end
end
def eval({:for, var, source_ast, body_ast}, context) do
with {:ok, source} <- eval(source_ast, context) do
case source do
list when is_list(list) ->
list
|> Enum.reduce_while({:ok, []}, fn item, {:ok, acc} ->
scoped_context = Map.put(context, var, item)
case eval(body_ast, scoped_context) do
{:ok, value} -> {:cont, {:ok, acc ++ [value]}}
{:error, %Error{} = e} -> {:halt, {:error, e}}
end
end)
_ ->
{:error, error(:type_error, "for-expression source must be a list")}
end
end
end
def eval({:for, bindings, body_ast}, context) when is_list(bindings) do
with {:ok, contexts} <- expand_for_contexts(bindings, [context]) do
Enum.reduce_while(contexts, {:ok, []}, fn scoped_context, {:ok, acc} ->
case eval(body_ast, Map.put(scoped_context, "partial", acc)) do
{:ok, value} -> {:cont, {:ok, acc ++ [value]}}
{:error, %Error{} = error} -> {:halt, {:error, error}}
end
end)
end
end
def eval({:quantifier, kind, var, source_ast, predicate_ast}, context) do
with {:ok, source} <- eval(source_ast, context) do
case source do
list when is_list(list) ->
case kind do
:some -> {:ok, Enum.any?(list, &quantifier_match?(&1, var, predicate_ast, context))}
:every -> {:ok, Enum.all?(list, &quantifier_match?(&1, var, predicate_ast, context))}
end
_ ->
{:error, error(:type_error, "quantifier source must be a list")}
end
end
end
def eval({:function, params, body}, context) do
{:ok, %Function{params: params, body: body, closure: context}}
end
def eval({:call, callee_ast, args_ast}, context) do
with {:ok, callee} <- eval(callee_ast, context),
{:ok, arg_values} <- eval_call_args(args_ast, context, []) do
case callee do
{:builtin, "range"} -> apply_range_function(arg_values, context)
_ -> apply_function(callee, arg_values)
end
end
end
def eval({:unary, :not, expr}, context) do
with {:ok, value} <- eval(expr, context) do
{:ok, feel_not(value)}
end
end
def eval({:unary, :negate, expr}, context) do
with {:ok, value} <- eval(expr, context) do
case value do
nil -> {:ok, nil}
%Decimal{} = decimal -> {:ok, decimal_negate(decimal)}
%Duration{} = duration -> {:ok, Duration.negate(duration)}
_ -> {:error, error(:type_error, "negation requires a number")}
end
end
end
def eval({:binary, op, left, right}, context) do
with {:ok, left_value} <- eval(left, context),
{:ok, right_value} <- eval(right, context) do
eval_binary(op, left_value, right_value)
end
end
defp property_value(%Date{} = value, "year"), do: decimal_new(value.year)
defp property_value(%Date{} = value, "month"), do: decimal_new(value.month)
defp property_value(%Date{} = value, "day"), do: decimal_new(value.day)
defp property_value(%Date{} = value, "weekday"), do: decimal_new(Date.day_of_week(value))
defp property_value(%FeelDateTime{date: date}, key) when key in ~w(year month day weekday),
do: property_value(date, key)
defp property_value(%FeelDateTime{time: time}, key), do: property_value(time, key)
defp property_value(%FeelTime{} = value, "hour"), do: decimal_new(value.hour)
defp property_value(%FeelTime{} = value, "minute"), do: decimal_new(value.minute)
defp property_value(%FeelTime{} = value, "second"), do: value.second
defp property_value(%FeelTime{zone: {:offset, seconds}}, "time offset"),
do: Duration.from_seconds(seconds)
defp property_value(%FeelTime{}, "time offset"), do: nil
defp property_value(%FeelTime{zone: {:iana, name}}, "timezone"), do: name
defp property_value(%FeelTime{}, "timezone"), do: nil
defp property_value(%Duration{kind: :year_month, months: months}, "years"),
do: decimal_new(div(months, 12))
defp property_value(%Duration{kind: :year_month, months: months}, "months"),
do: decimal_new(rem(months, 12))
defp property_value(%Duration{}, key) when key in ~w(years months), do: nil
defp property_value(%Duration{kind: :day_time, seconds: seconds}, "days"),
do: seconds_component(seconds, 86_400, :quotient)
defp property_value(%Duration{kind: :day_time, seconds: seconds}, "hours"),
do: seconds_component(seconds, 3_600, :remainder)
defp property_value(%Duration{kind: :day_time, seconds: seconds}, "minutes"),
do: seconds_component(seconds, 60, :remainder)
defp property_value(%Duration{kind: :day_time, seconds: seconds}, "seconds"),
do: Decimal.rem(decimal_value(seconds), Decimal.new(60))
defp property_value(%Duration{}, key) when key in ~w(days hours minutes seconds), do: nil
defp property_value(%Range{} = value, "start"), do: value.start
defp property_value(%Range{} = value, "end"), do: value.end
defp property_value(%Range{} = value, "start included"), do: value.start_inclusive
defp property_value(%Range{} = value, "end included"), do: value.end_inclusive
defp property_value({:unary_test_value, operator, operand}, property)
when operator in [:lt, :lte, :gt, :gte, :eq] do
range =
case operator do
:lt -> %Range{start: nil, end: operand, start_inclusive: false, end_inclusive: false}
:lte -> %Range{start: nil, end: operand, start_inclusive: false, end_inclusive: true}
:gt -> %Range{start: operand, end: nil, start_inclusive: false, end_inclusive: false}
:gte -> %Range{start: operand, end: nil, start_inclusive: true, end_inclusive: false}
:eq -> %Range{start: operand, end: operand, start_inclusive: true, end_inclusive: true}
end
property_value(range, property)
end
defp property_value(map, key), do: Map.get(map, key)
defp seconds_component(seconds, divisor, :quotient) do
seconds |> decimal_value() |> Decimal.div_int(divisor) |> Decimal.round(0, :down)
end
defp seconds_component(seconds, divisor, :remainder) do
seconds
|> decimal_value()
|> Decimal.div_int(divisor)
|> Decimal.round(0, :down)
|> Decimal.rem(Decimal.new(if(divisor == 3_600, do: 24, else: 60)))
end
defp decimal_value(%Decimal{} = value), do: value
defp decimal_value(value), do: decimal_new(value)
defp eval_call_args([], _context, acc), do: {:ok, Enum.reverse(acc)}
defp eval_call_args([{:named_arg, name, value_ast} | rest], context, acc) do
with {:ok, value} <- eval(value_ast, context) do
eval_call_args(rest, context, [{:named_arg, name, value} | acc])
end
end
defp eval_call_args([arg_ast | rest], context, acc) do
with {:ok, value} <- eval(arg_ast, context) do
eval_call_args(rest, context, [value | acc])
end
end
defp eval_list([], _context, acc), do: {:ok, acc}
defp eval_list([item_ast | rest], context, acc) do
with {:ok, value} <- eval(item_ast, context) do
eval_list(rest, context, acc ++ [value])
end
end
defp eval_context_entries([], _context, acc), do: {:ok, acc}
defp eval_context_entries([{key, value_ast} | rest], context, acc) do
scoped_context = Map.merge(context, acc)
with {:ok, value} <- eval(value_ast, scoped_context) do
eval_context_entries(rest, context, Map.put(acc, key, value))
end
end
defp expand_for_contexts([], contexts), do: {:ok, contexts}
defp expand_for_contexts([{variable, source_ast} | rest], contexts) do
contexts
|> Enum.reduce_while({:ok, []}, fn scoped_context, {:ok, acc} ->
with {:ok, source} <- eval(source_ast, scoped_context),
{:ok, values} <- iteration_values(source) do
expanded = Enum.map(values, &Map.put(scoped_context, variable, &1))
{:cont, {:ok, acc ++ expanded}}
else
{:error, %Error{} = error} -> {:halt, {:error, error}}
end
end)
|> case do
{:ok, expanded} -> expand_for_contexts(rest, expanded)
{:error, %Error{} = error} -> {:error, error}
end
end
defp iteration_values(values) when is_list(values), do: {:ok, values}
defp iteration_values({:sequence, %Decimal{} = first, %Decimal{} = last}) do
numeric_sequence(first, last, true)
end
defp iteration_values({:sequence, %Date{} = first, %Date{} = last}) do
step = if Date.compare(first, last) in [:lt, :eq], do: 1, else: -1
count = abs(Date.diff(last, first))
{:ok, Enum.map(0..count, &Date.add(first, &1 * step))}
end
defp iteration_values(%Range{start: %Decimal{} = first, end: %Decimal{} = last}) do
numeric_sequence(first, last, false)
end
defp iteration_values(_source),
do: {:error, error(:type_error, "for-expression source must be a list or numeric range")}
defp numeric_sequence(first, last, descending_allowed?) do
with {:ok, first_integer} <- decimal_integer(first),
{:ok, last_integer} <- decimal_integer(last),
true <- descending_allowed? or first_integer <= last_integer do
step = if first_integer <= last_integer, do: 1, else: -1
values =
first_integer
|> Elixir.Range.new(last_integer, step)
|> Enum.map(&decimal_new/1)
{:ok, values}
else
false -> {:error, error(:evaluation_error, "range start must not exceed its end")}
:error -> {:error, error(:type_error, "numeric iteration range requires integers")}
end
end
defp eval_list_filter(list, predicate, context) do
case eval(predicate, context) do
{:ok, %Decimal{} = index} ->
{:ok, list_index(list, index)}
_ ->
filtered =
Enum.filter(list, fn item ->
predicate_context = bind_item_context(context, item)
case eval(predicate, predicate_context) do
{:ok, true} -> true
_ -> false
end
end)
{:ok, filtered}
end
end
defp eval_scalar_filter(scalar, predicate, context) do
predicate_context = bind_item_context(context, scalar)
case eval(predicate, predicate_context) do
{:ok, true} -> {:ok, [scalar]}
{:ok, false} -> {:ok, []}
{:ok, nil} -> {:ok, []}
{:ok, %Decimal{} = index} -> {:ok, scalar_index(scalar, index)}
{:ok, _other} -> {:ok, nil}
{:error, %Error{} = error} -> {:error, error}
end
end
defp list_index(list, index) do
with {:ok, position} <- decimal_integer(index),
true <- position != 0 do
if position > 0, do: Enum.at(list, position - 1), else: Enum.at(list, position)
else
_ -> nil
end
end
defp scalar_index(scalar, index) do
case decimal_integer(index) do
{:ok, position} when position in [1, -1] -> scalar
_ -> nil
end
end
defp bind_item_context(context, item) do
base = Map.put(context, "item", item)
case item do
map when is_map(map) and not is_struct(map) ->
Enum.reduce(map, base, fn
{k, v}, acc when is_binary(k) -> Map.put(acc, k, v)
_entry, acc -> acc
end)
_ ->
base
end
end
defp quantifier_match?(item, var, predicate_ast, context) do
scoped_context = Map.put(context, var, item)
case eval(predicate_ast, scoped_context) do
{:ok, true} -> true
_ -> false
end
end
defp apply_function(%Function{params: params, body: body, closure: closure}, args) do
with {:ok, args} <- order_function_args(params, args),
true <- length(params) == length(args) do
if Enum.zip(params, args)
|> Enum.all?(fn {{_name, type}, value} -> parameter_type?(value, type) end) do
call_context =
params
|> Enum.zip(args)
|> Enum.reduce(closure, fn {{name, _type}, value}, acc -> Map.put(acc, name, value) end)
eval(body, call_context)
else
{:error, error(:type_error, "function argument does not conform to parameter type")}
end
else
_ -> {:error, error(:arity_error, "function called with invalid arguments")}
end
end
defp apply_function({:builtin, "sort"}, [values, comparator]) when is_list(values) do
sort_with_comparator(values, comparator)
end
defp apply_function({:builtin, "list_replace"}, [list, matcher, new_item])
when is_list(list) and is_struct(matcher, Function) do
Enum.reduce_while(list, {:ok, []}, fn item, {:ok, result} ->
case apply_function(matcher, [item, new_item]) do
{:ok, true} ->
{:cont, {:ok, result ++ [new_item]}}
{:ok, false} ->
{:cont, {:ok, result ++ [item]}}
{:ok, _value} ->
{:halt, {:error, error(:type_error, "list replace match must return boolean")}}
{:error, %Error{} = error} ->
{:halt, {:error, error}}
end
end)
end
defp apply_function({:builtin, "list_replace"} = builtin, args) do
if Enum.all?(args, &match?({:named_arg, _, _}, &1)) do
values = Map.new(args, fn {:named_arg, name, value} -> {name, value} end)
cond do
Map.keys(values) |> Enum.sort() == ["list", "match", "newItem"] |> Enum.sort() ->
apply_function(builtin, [values["list"], values["match"], values["newItem"]])
Map.keys(values) |> Enum.sort() == ["list", "newItem", "position"] |> Enum.sort() ->
apply_function(builtin, [values["list"], values["position"], values["newItem"]])
true ->
{:error, error(:arity_error, "invalid named arguments for list replace")}
end
else
Builtins.invoke("list_replace", args)
end
end
defp apply_function({:builtin, name}, args) do
Builtins.invoke(name, args)
end
defp apply_function({:external_function, function}, args) when is_function(function, 1) do
case function.(args) do
{:ok, value} -> {:ok, value}
{:error, %Error{} = error} -> {:error, error}
{:error, reason} -> {:error, error(:evaluation_error, inspect(reason))}
end
end
defp apply_function(fun, args) when is_function(fun, length(args)) do
{:ok, apply(fun, args)}
end
defp apply_function(_callee, _args),
do: {:error, error(:type_error, "attempted to call a non-function value")}
defp order_function_args(params, args) do
names = Enum.map(params, &elem(&1, 0))
Enum.reduce_while(args, {:ok, %{}, names}, fn
{:named_arg, name, value}, {:ok, bound, remaining} ->
if name in remaining do
{:cont, {:ok, Map.put(bound, name, value), List.delete(remaining, name)}}
else
{:halt, :error}
end
value, {:ok, bound, [name | remaining]} ->
{:cont, {:ok, Map.put(bound, name, value), remaining}}
_value, {:ok, _bound, []} ->
{:halt, :error}
end)
|> case do
{:ok, bound, []} -> {:ok, Enum.map(names, &Map.fetch!(bound, &1))}
_ -> :error
end
end
defp apply_range_function([{:named_arg, "from", value}], context),
do: apply_range_function([value], context)
defp apply_range_function([source], context) when is_binary(source) do
with {:ok, ast} <- Boxic.FEEL.parse(String.trim(source)),
true <- valid_range_ast?(ast),
{:ok, %Range{} = range} <- Boxic.FEEL.evaluate_ast(ast, context),
{:ok, true} <- eval_binary(:lte, range.start, range.end) do
{:ok, range}
else
_ -> {:error, error(:evaluation_error, "invalid range string")}
end
end
defp apply_range_function(_args, _context),
do: {:error, error(:type_error, "range expects one string argument")}
defp valid_range_ast?({:range, _start_inclusive, _end_inclusive, start_ast, end_ast}),
do: valid_range_endpoint_ast?(start_ast) and valid_range_endpoint_ast?(end_ast)
defp valid_range_ast?(_ast), do: false
defp valid_range_endpoint_ast?({:literal, value}), do: not is_nil(value)
defp valid_range_endpoint_ast?({:call, {:identifier, name}, [{:literal, value}]})
when name in ["date", "date_time", "time", "duration"],
do: is_binary(value)
defp valid_range_endpoint_ast?(_ast), do: false
defp parameter_type?(_value, nil), do: true
defp parameter_type?(%Decimal{}, "number"), do: true
defp parameter_type?(value, "string"), do: is_binary(value)
defp parameter_type?(value, "boolean"), do: is_boolean(value)
defp parameter_type?(_value, _type), do: false
defp sort_with_comparator(values, comparator) do
values
|> Enum.reduce_while({:ok, []}, fn value, {:ok, sorted} ->
case insert_with_comparator(value, sorted, comparator, []) do
{:ok, next} -> {:cont, {:ok, next}}
{:error, %Error{} = error} -> {:halt, {:error, error}}
end
end)
end
defp insert_with_comparator(value, [], _comparator, prefix),
do: {:ok, Enum.reverse(prefix, [value])}
defp insert_with_comparator(value, [head | tail] = remaining, comparator, prefix) do
case apply_function(comparator, [value, head]) do
{:ok, true} -> {:ok, Enum.reverse(prefix, [value | remaining])}
{:ok, false} -> insert_with_comparator(value, tail, comparator, [head | prefix])
{:ok, _} -> {:error, error(:type_error, "sort comparator must return a boolean")}
{:error, %Error{} = error} -> {:error, error}
end
end
defp eval_binary(:plus, left, right), do: plus(left, right)
defp eval_binary(:minus, left, right), do: minus(left, right)
defp eval_binary(:mul, %Duration{} = duration, %Decimal{} = factor),
do: {:ok, Duration.scale(duration, factor)}
defp eval_binary(:mul, %Decimal{} = factor, %Duration{} = duration),
do: {:ok, Duration.scale(duration, factor)}
defp eval_binary(:mul, left, right), do: decimal_binary(left, right, &decimal_mult/2)
defp eval_binary(:pow, left, right), do: decimal_power(left, right)
defp eval_binary(:div, left, right) do
case {left, right} do
{nil, _} ->
{:ok, nil}
{_, nil} ->
{:ok, nil}
{%Decimal{} = l, %Decimal{} = r} ->
if decimal_equal?(r, decimal_new("0")) do
{:ok, nil}
else
{:ok, decimal_div(l, r)}
end
{%Duration{} = duration, %Decimal{} = divisor} ->
if decimal_equal?(divisor, decimal_new("0")) do
{:ok, nil}
else
{:ok, Duration.scale(duration, decimal_div(decimal_new("1"), divisor))}
end
{%Duration{} = left_duration, %Duration{} = right_duration} ->
case Duration.ratio(left_duration, right_duration) do
{:ok, ratio} -> {:ok, ratio}
:error -> {:error, error(:type_error, "division requires compatible durations")}
end
_ ->
{:error, error(:type_error, "division requires numbers")}
end
end
defp eval_binary(op, left, right) when op in [:gt, :gte, :lt, :lte] do
cond do
is_nil(left) or is_nil(right) ->
{:ok, nil}
match?(%Decimal{}, left) and match?(%Decimal{}, right) ->
{:ok, compare_from_op(op, decimal_compare(left, right))}
is_struct(left, Date) and is_struct(right, Date) ->
{:ok, compare_from_op(op, Date.compare(left, right))}
is_struct(left, Time) and is_struct(right, Time) ->
{:ok, compare_from_op(op, Time.compare(left, right))}
is_struct(left, DateTime) and is_struct(right, DateTime) ->
{:ok, compare_from_op(op, DateTime.compare(left, right))}
match?(%FeelTime{}, left) and match?(%FeelTime{}, right) ->
compare_temporal(op, FeelTime.compare(left, right))
match?(%FeelDateTime{}, left) and match?(%FeelDateTime{}, right) ->
compare_temporal(op, FeelDateTime.compare(left, right))
match?(%Duration{}, left) and match?(%Duration{}, right) ->
compare_temporal(op, Duration.compare(left, right))
is_binary(left) and is_binary(right) ->
{:ok, compare_from_op(op, compare_strings(left, right))}
is_boolean(left) and is_boolean(right) ->
{:ok, compare_from_op(op, compare_booleans(left, right))}
true ->
{:error, error(:type_error, "comparison requires compatible values")}
end
end
defp eval_binary(:eq, left, right), do: {:ok, feel_equal(left, right)}
defp eval_binary(:neq, left, right) do
case feel_equal(left, right) do
nil -> {:ok, nil}
equal? -> {:ok, not equal?}
end
end
defp eval_binary(:and, left, right), do: {:ok, feel_and(left, right)}
defp eval_binary(:or, left, right), do: {:ok, feel_or(left, right)}
defp instance_of?(value, "Any", _context), do: not is_nil(value)
defp instance_of?(value, "number", _context), do: match?(%Decimal{}, value)
defp instance_of?(value, "string", _context), do: is_binary(value)
defp instance_of?(value, "boolean", _context), do: is_boolean(value)
defp instance_of?(value, "date", _context), do: match?(%Date{}, value)
defp instance_of?(value, "time", _context),
do: match?(%FeelTime{}, value) or match?(%Time{}, value)
defp instance_of?(value, "date_time", _context),
do:
match?(%FeelDateTime{}, value) or match?(%DateTime{}, value) or
match?(%NaiveDateTime{}, value)
defp instance_of?(%Duration{kind: :year_month}, "years and months duration", _context),
do: true
defp instance_of?(%Duration{kind: :year_month}, "years_and_months_duration", _context),
do: true
defp instance_of?(%Duration{kind: :day_time}, "days and time duration", _context), do: true
defp instance_of?(value, "context<>", _context),
do: is_map(value) and not is_struct(value)
defp instance_of?(value, "list<" <> rest, context) when is_list(value) do
subtype = String.trim_trailing(rest, ">")
Enum.all?(value, &instance_of?(&1, subtype, context))
end
defp instance_of?(value, "context<" <> rest, context)
when is_map(value) and not is_struct(value) do
rest
|> String.trim_trailing(">")
|> split_type_fields()
|> Enum.all?(fn {key, subtype} ->
Map.has_key?(value, key) and
(is_nil(Map.get(value, key)) or instance_of?(Map.get(value, key), subtype, context))
end)
end
defp instance_of?(%Range{} = range, "range<" <> rest, context) do
subtype = String.trim_trailing(rest, ">")
instance_of?(range.start, subtype, context) and instance_of?(range.end, subtype, context)
end
defp instance_of?(value, "function<" <> _signature, _context),
do: match?(%Function{}, value) or match?({:external_function, _}, value)
defp instance_of?(value, type, context) do
types = Map.get(context, "__feel_types__", %{})
case Enum.find_value(types, fn {_id, definition} ->
Map.get(definition, :name) == type && definition
end) do
nil -> false
definition -> instance_of_definition?(value, definition, context)
end
end
defp instance_of_definition?(value, definition, context) do
collection? = Map.get(definition, :is_collection) in [true, "true"]
cond do
collection? and is_list(value) ->
Enum.all?(value, &instance_of?(&1, Map.get(definition, :type_ref) || "Any", context))
collection? ->
false
Map.get(definition, :components, []) != [] and is_map(value) and not is_struct(value) ->
Enum.all?(Map.get(definition, :components), fn component ->
key = Map.get(component, :name)
Map.has_key?(value, key) and
(is_nil(Map.get(value, key)) or
instance_of?(Map.get(value, key), Map.get(component, :type_ref), context))
end)
String.contains?(Map.get(definition, :name, ""), "Function") ->
match?(%Function{}, value) or match?({:external_function, _}, value)
is_binary(Map.get(definition, :type_ref)) ->
instance_of?(value, Map.get(definition, :type_ref), context)
true ->
false
end
end
defp split_type_fields(value) do
value
|> String.split(~r/,(?![^<]*>)/, trim: true)
|> Enum.map(fn field ->
case String.split(field, ":", parts: 2) do
[key, type] -> {String.trim(key), String.trim(type)}
_ -> {"", "Any"}
end
end)
end
defp plus(left, right) do
case {left, right} do
{nil, _} ->
{:ok, nil}
{_, nil} ->
{:ok, nil}
{%Decimal{} = l, %Decimal{} = r} ->
{:ok, decimal_add(l, r)}
{left, right} when is_binary(left) and is_binary(right) ->
{:ok, left <> right}
{%Date{} = date, %Duration{} = duration} ->
{:ok, Duration.add_to_date(date, duration)}
{%Duration{} = duration, %Date{} = date} ->
{:ok, Duration.add_to_date(date, duration)}
{%DateTime{} = datetime, %Duration{} = duration} ->
{:ok, Duration.add_to_datetime(datetime, duration)}
{%Duration{} = duration, %DateTime{} = datetime} ->
{:ok, Duration.add_to_datetime(datetime, duration)}
{%FeelDateTime{} = datetime, %Duration{} = duration} ->
temporal_arithmetic(FeelDateTime.add_duration(datetime, duration))
{%Duration{} = duration, %FeelDateTime{} = datetime} ->
temporal_arithmetic(FeelDateTime.add_duration(datetime, duration))
{%Time{} = time, %Duration{} = duration} ->
{:ok, Duration.add_to_time(time, duration)}
{%Duration{} = duration, %Time{} = time} ->
{:ok, Duration.add_to_time(time, duration)}
{%FeelTime{} = time, %Duration{months: 0} = duration} ->
{:ok, FeelTime.add_seconds(time, duration.seconds)}
{%Duration{months: 0} = duration, %FeelTime{} = time} ->
{:ok, FeelTime.add_seconds(time, duration.seconds)}
{%Duration{kind: kind} = l, %Duration{kind: kind} = r} ->
{:ok, Duration.add(l, r)}
{%Duration{}, %Duration{}} ->
{:error, error(:type_error, "duration kinds must match")}
_ ->
{:error, error(:type_error, "addition requires compatible values")}
end
end
defp minus(left, right) do
case {left, right} do
{nil, _} ->
{:ok, nil}
{_, nil} ->
{:ok, nil}
{%Decimal{} = l, %Decimal{} = r} ->
{:ok, decimal_sub(l, r)}
{%Date{} = left_date, %Date{} = right_date} ->
{:ok, Duration.from_days(Date.diff(left_date, right_date))}
{%DateTime{} = left_datetime, %DateTime{} = right_datetime} ->
{:ok, Duration.from_seconds(DateTime.diff(left_datetime, right_datetime, :second))}
{%FeelDateTime{} = left_datetime, %FeelDateTime{} = right_datetime} ->
temporal_arithmetic(FeelDateTime.difference(left_datetime, right_datetime))
{%FeelDateTime{} = left_datetime, %Date{} = right_date} ->
temporal_arithmetic(
FeelDateTime.difference(
left_datetime,
date_at_midnight(right_date, left_datetime.time.zone)
)
)
{%Date{} = left_date, %FeelDateTime{} = right_datetime} ->
temporal_arithmetic(
FeelDateTime.difference(
date_at_midnight(left_date, right_datetime.time.zone),
right_datetime
)
)
{%FeelTime{} = left_time, %FeelTime{} = right_time} ->
temporal_arithmetic(FeelTime.difference(left_time, right_time))
{%Date{} = date, %Duration{} = duration} ->
{:ok, Duration.add_to_date(date, Duration.negate(duration))}
{%DateTime{} = datetime, %Duration{} = duration} ->
{:ok, Duration.add_to_datetime(datetime, Duration.negate(duration))}
{%FeelDateTime{} = datetime, %Duration{} = duration} ->
temporal_arithmetic(FeelDateTime.add_duration(datetime, Duration.negate(duration)))
{%Time{} = time, %Duration{} = duration} ->
{:ok, Duration.add_to_time(time, Duration.negate(duration))}
{%FeelTime{} = time, %Duration{months: 0} = duration} ->
{:ok, FeelTime.add_seconds(time, Duration.negate(duration).seconds)}
{%Duration{kind: kind} = l, %Duration{kind: kind} = r} ->
{:ok, Duration.subtract(l, r)}
{%Duration{}, %Duration{}} ->
{:error, error(:type_error, "duration kinds must match")}
_ ->
{:error, error(:type_error, "subtraction requires compatible values")}
end
end
defp temporal_arithmetic({:ok, value}), do: {:ok, value}
defp temporal_arithmetic(:error),
do: {:error, error(:evaluation_error, "temporal operation failed")}
defp date_at_midnight(date, _zone) do
{:ok, time} = FeelTime.new(0, 0, 0, {:offset, 0})
FeelDateTime.new(date, time)
end
defp compare_from_op(op, cmp) do
case op do
:gt -> cmp == :gt
:gte -> cmp in [:gt, :eq]
:lt -> cmp == :lt
:lte -> cmp in [:lt, :eq]
end
end
defp compare_temporal(_op, :unordered), do: {:ok, nil}
defp compare_temporal(op, comparison), do: {:ok, compare_from_op(op, comparison)}
defp compare_strings(left, right) when left == right, do: :eq
defp compare_strings(left, right) when left < right, do: :lt
defp compare_strings(_left, _right), do: :gt
defp compare_booleans(left, right) when left == right, do: :eq
defp compare_booleans(false, true), do: :lt
defp compare_booleans(true, false), do: :gt
defp decimal_binary(left, right, operation) do
case {left, right} do
{nil, _} -> {:ok, nil}
{_, nil} -> {:ok, nil}
{%Decimal{} = l, %Decimal{} = r} -> {:ok, operation.(l, r)}
_ -> {:error, error(:type_error, "arithmetic requires numbers")}
end
end
defp decimal_power(nil, _right), do: {:ok, nil}
defp decimal_power(_left, nil), do: {:ok, nil}
defp decimal_power(%Decimal{} = base, %Decimal{} = exponent) do
case decimal_integer(exponent) do
{:ok, integer} when integer < 0 ->
powered = decimal_integer_power(base, -integer)
if decimal_equal?(powered, decimal_new("0")) do
{:ok, nil}
else
{:ok, decimal_div(decimal_new("1"), powered)}
end
{:ok, integer} ->
{:ok, decimal_integer_power(base, integer)}
:error ->
{:ok,
base
|> Decimal.to_float()
|> :math.pow(Decimal.to_float(exponent))
|> Decimal.from_float()}
end
end
defp decimal_power(_left, _right),
do: {:error, error(:type_error, "exponentiation requires numbers")}
defp decimal_integer_power(_base, 0), do: decimal_new("1")
defp decimal_integer_power(base, exponent) do
decimal_integer_power(base, exponent, decimal_new("1"))
end
defp decimal_integer_power(_base, 0, acc), do: acc
defp decimal_integer_power(base, exponent, acc) when rem(exponent, 2) == 1 do
decimal_integer_power(decimal_mult(base, base), div(exponent, 2), decimal_mult(acc, base))
end
defp decimal_integer_power(base, exponent, acc) do
decimal_integer_power(decimal_mult(base, base), div(exponent, 2), acc)
end
defp decimal_integer(value) do
integer = Decimal.to_integer(value)
if decimal_equal?(value, decimal_new(integer)), do: {:ok, integer}, else: :error
rescue
_ -> :error
end
defp feel_equal(nil, nil), do: true
defp feel_equal(nil, _right), do: false
defp feel_equal(_left, nil), do: false
defp feel_equal(%Decimal{} = left, %Decimal{} = right), do: decimal_equal?(left, right)
defp feel_equal(left, right) when is_integer(left) and is_integer(right), do: left == right
defp feel_equal(%Decimal{} = left, right) when is_integer(right),
do: decimal_equal?(left, decimal_new(right))
defp feel_equal(left, %Decimal{} = right) when is_integer(left),
do: decimal_equal?(decimal_new(left), right)
defp feel_equal(left, right) when is_boolean(left) and is_boolean(right), do: left == right
defp feel_equal(left, right) when is_binary(left) and is_binary(right), do: left == right
defp feel_equal(%Date{} = left, %Date{} = right), do: Date.compare(left, right) == :eq
defp feel_equal(%Time{} = left, %Time{} = right), do: Time.compare(left, right) == :eq
defp feel_equal(%DateTime{} = left, %DateTime{} = right),
do: DateTime.compare(left, right) == :eq
defp feel_equal(%FeelTime{} = left, %FeelTime{} = right),
do:
left.zone == right.zone and left.hour == right.hour and left.minute == right.minute and
Decimal.equal?(
Decimal.round(left.second, 0, :down),
Decimal.round(right.second, 0, :down)
)
defp feel_equal(%FeelDateTime{} = left, %FeelDateTime{} = right),
do:
if(left.time.zone == right.time.zone,
do:
Date.compare(left.date, right.date) == :eq and
feel_equal(left.time, right.time) == true,
else: FeelDateTime.compare(left, right) == :eq
)
defp feel_equal(%Duration{} = left, %Duration{} = right),
do:
if(left.kind == right.kind,
do: left.months == right.months and feel_equal(left.seconds, right.seconds) == true,
else: nil
)
defp feel_equal(%Range{} = left, %Range{} = right) do
left.start_inclusive == right.start_inclusive and
left.end_inclusive == right.end_inclusive and
feel_equal(left.start, right.start) == true and
feel_equal(left.end, right.end) == true
end
defp feel_equal(
{:unary_test_value, left_operator, left_operand},
{:unary_test_value, right_operator, right_operand}
) do
left_operator == right_operator and feel_equal(left_operand, right_operand) == true
end
defp feel_equal({:unary_test_value, _operator, _operand}, %Range{}), do: false
defp feel_equal(%Range{}, {:unary_test_value, _operator, _operand}), do: false
defp feel_equal(left, right) when is_list(left) and is_list(right) do
if length(left) == length(right) do
left |> Enum.zip(right) |> Enum.map(fn {l, r} -> feel_equal(l, r) end) |> equality_all()
else
false
end
end
defp feel_equal(left, right)
when is_map(left) and is_map(right) and not is_struct(left) and not is_struct(right) do
if Map.keys(left) |> Enum.sort() == Map.keys(right) |> Enum.sort() do
left
|> Enum.map(fn {key, value} -> feel_equal(value, Map.fetch!(right, key)) end)
|> equality_all()
else
false
end
end
defp feel_equal(_left, _right), do: nil
defp equality_all(results) do
cond do
Enum.any?(results, &(&1 == false)) -> false
Enum.any?(results, &is_nil/1) -> nil
true -> true
end
end
defp comparator_unary_test?(expression) do
Regex.match?(~r/^(<=|>=|<|>|!=|=)\s*.+$/, expression)
end
defp range_unary_test?(expression) do
String.starts_with?(expression, "[") or String.starts_with?(expression, "(")
end
defp eval_comparator_unary_test(expression, value, context) do
%{"op" => op, "rhs" => rhs} =
Regex.named_captures(~r/^(?<op><=|>=|<|>|!=|=)\s*(?<rhs>.+)$/, expression)
with {:ok, rhs_value} <- Boxic.FEEL.evaluate(rhs, context) do
case compare_for_unary_test(op, value, rhs_value) do
{:ok, result} -> {:ok, result}
{:error, %Error{} = e} -> {:error, e}
end
end
end
defp compare_for_unary_test(_op, nil, _rhs), do: {:ok, false}
defp compare_for_unary_test(_op, _lhs, nil), do: {:ok, false}
defp compare_for_unary_test(op, %Decimal{} = lhs, %Decimal{} = rhs) do
cmp = decimal_compare(lhs, rhs)
result =
case op do
">" -> cmp == :gt
">=" -> cmp in [:gt, :eq]
"<" -> cmp == :lt
"<=" -> cmp in [:lt, :eq]
"=" -> cmp == :eq
"!=" -> cmp != :eq
end
{:ok, result}
end
defp compare_for_unary_test(op, %Date{} = lhs, %Date{} = rhs)
when op in [">", ">=", "<", "<=", "=", "!="] do
unary_compare_result(op, Date.compare(lhs, rhs))
end
defp compare_for_unary_test(op, %Time{} = lhs, %Time{} = rhs)
when op in [">", ">=", "<", "<=", "=", "!="] do
unary_compare_result(op, Time.compare(lhs, rhs))
end
defp compare_for_unary_test(op, %DateTime{} = lhs, %DateTime{} = rhs)
when op in [">", ">=", "<", "<=", "=", "!="] do
unary_compare_result(op, DateTime.compare(lhs, rhs))
end
defp compare_for_unary_test(op, %FeelTime{} = lhs, %FeelTime{} = rhs)
when op in [">", ">=", "<", "<=", "=", "!="] do
unary_compare_result(op, FeelTime.compare(lhs, rhs))
end
defp compare_for_unary_test(op, %FeelDateTime{} = lhs, %FeelDateTime{} = rhs)
when op in [">", ">=", "<", "<=", "=", "!="] do
unary_compare_result(op, FeelDateTime.compare(lhs, rhs))
end
defp compare_for_unary_test(op, %Duration{} = lhs, %Duration{} = rhs)
when op in [">", ">=", "<", "<=", "=", "!="] do
case Duration.compare(lhs, rhs) do
:unordered -> {:ok, nil}
comparison -> unary_compare_result(op, comparison)
end
end
defp compare_for_unary_test(op, lhs, rhs)
when is_binary(lhs) and is_binary(rhs) and op in [">", ">=", "<", "<=", "=", "!="] do
unary_compare_result(op, compare_strings(lhs, rhs))
end
defp compare_for_unary_test(op, lhs, rhs) when op in ["=", "!="] do
eq = Semantics.equal?(lhs, rhs)
{:ok, if(op == "=", do: eq, else: not eq)}
end
defp compare_for_unary_test(_op, _lhs, _rhs),
do: {:error, error(:type_error, "unary test comparison requires compatible values")}
defp unary_compare_result(op, cmp) do
result =
case op do
">" -> cmp == :gt
">=" -> cmp in [:gt, :eq]
"<" -> cmp == :lt
"<=" -> cmp in [:lt, :eq]
"=" -> cmp == :eq
"!=" -> cmp != :eq
end
{:ok, result}
end
defp range_contains?(%Range{}, nil), do: nil
defp range_contains?(%Range{start: nil}, _value), do: nil
defp range_contains?(%Range{end: nil}, _value), do: nil
defp range_contains?(%Range{} = range, value) do
lower_ok = range_lower_ok?(range, value)
upper_ok = range_upper_ok?(range, value)
lower_ok and upper_ok
end
defp evaluate_in(value, {:unary_test, operator, operand_ast}, context) do
with {:ok, operand} <- eval(operand_ast, context) do
compare_for_unary_test(unary_operator(operator), value, operand)
end
end
defp evaluate_in(value, {:unary_tests, tests}, context) do
tests
|> Enum.reduce_while({:ok, []}, fn test, {:ok, results} ->
case evaluate_in(value, test, context) do
{:ok, true} -> {:halt, {:ok, true}}
{:ok, result} -> {:cont, {:ok, [result | results]}}
{:error, %Error{} = error} -> {:halt, {:error, error}}
end
end)
|> case do
{:ok, true} -> {:ok, true}
{:ok, results} -> {:ok, equality_any(results)}
{:error, %Error{} = error} -> {:error, error}
end
end
defp evaluate_in(value, tests_ast, context) do
with {:ok, tests} <- eval(tests_ast, context) do
{:ok, membership_result(value, tests)}
end
end
defp membership_result(value, tests) when is_list(tests) do
tests
|> Enum.map(&membership_candidate(value, &1))
|> equality_any()
end
defp membership_result(value, test), do: membership_candidate(value, test)
defp membership_candidate(value, %Range{} = range), do: range_contains?(range, value)
defp membership_candidate(value, test), do: feel_equal(value, test)
defp equality_any(results) do
cond do
Enum.any?(results, &(&1 == true)) -> true
Enum.any?(results, &(&1 == false)) -> false
true -> nil
end
end
defp unary_operator(:eq), do: "="
defp unary_operator(:neq), do: "!="
defp unary_operator(:lt), do: "<"
defp unary_operator(:lte), do: "<="
defp unary_operator(:gt), do: ">"
defp unary_operator(:gte), do: ">="
defp range_lower_ok?(%Range{start: start, start_inclusive: inclusive}, value) do
case compare_for_bounds(value, start) do
:gt -> true
:eq -> inclusive
_ -> false
end
end
defp range_upper_ok?(%Range{end: finish, end_inclusive: inclusive}, value) do
case compare_for_bounds(value, finish) do
:lt -> true
:eq -> inclusive
_ -> false
end
end
defp compare_for_bounds(%Decimal{} = left, %Decimal{} = right), do: decimal_compare(left, right)
defp compare_for_bounds(%Date{} = left, %Date{} = right), do: Date.compare(left, right)
defp compare_for_bounds(%Time{} = left, %Time{} = right), do: Time.compare(left, right)
defp compare_for_bounds(%DateTime{} = left, %DateTime{} = right),
do: DateTime.compare(left, right)
defp compare_for_bounds(%FeelTime{} = left, %FeelTime{} = right),
do: FeelTime.compare(left, right)
defp compare_for_bounds(%FeelDateTime{} = left, %FeelDateTime{} = right),
do: FeelDateTime.compare(left, right)
defp compare_for_bounds(%Duration{} = left, %Duration{} = right),
do: Duration.compare(left, right)
defp compare_for_bounds(left, right) do
cond do
left == right -> :eq
left < right -> :lt
true -> :gt
end
rescue
_ -> :not_comparable
end
defp feel_not(nil), do: nil
defp feel_not(true), do: false
defp feel_not(false), do: true
defp feel_not(_), do: nil
defp feel_and(false, _), do: false
defp feel_and(_, false), do: false
defp feel_and(nil, _), do: nil
defp feel_and(_, nil), do: nil
defp feel_and(true, true), do: true
defp feel_and(_, _), do: nil
defp feel_or(true, _), do: true
defp feel_or(_, true), do: true
defp feel_or(nil, _), do: nil
defp feel_or(_, nil), do: nil
defp feel_or(false, false), do: false
defp feel_or(_, _), do: nil
defp error(code, message), do: %Error{code: code, message: message}
defp decimal_new(value), do: apply(Decimal, :new, [value])
defp decimal_add(left, right), do: apply(Decimal, :add, [left, right])
defp decimal_sub(left, right), do: apply(Decimal, :sub, [left, right])
defp decimal_mult(left, right), do: apply(Decimal, :mult, [left, right])
defp decimal_div(left, right), do: apply(Decimal, :div, [left, right])
defp decimal_negate(value), do: apply(Decimal, :negate, [value])
defp decimal_equal?(left, right), do: apply(Decimal, :equal?, [left, right])
defp decimal_compare(left, right), do: apply(Decimal, :compare, [left, right])
end