src/template_compiler_operators.erl

%% @author Marc Worrell <marc@worrell.nl>
%% @copyright 2010-2023 Marc Worrell
%% @doc Operators for expression evaluation in templates
%% @end

%% Copyright 2010-2023 Marc Worrell
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%%     http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.

-module(template_compiler_operators).
-author("Marc Worrell <marc@worrell.nl>").

-export([
    'not'/3,
    'xor'/4,
    'and'/4,
    'or'/4,

    concat/4,
    subtract/4,

    add/4,
    sub/4,
    divide/4,
    multiply/4,
    modulo/4,

    negate/3,

    ge/4,
    le/4,
    gt/4,
    lt/4,
    eq/4,
    ne/4,
    seq/4,
    sne/4
]).

'not'(false, _Runtime, _Context) -> true;
'not'(true, _Runtime, _Context) -> false;
'not'(undefined, _Runtime, _Context) -> true;
'not'(A, Runtime, Context) ->
    not Runtime:to_bool(A, Context).

'xor'(A, A, _Runtime, _Context) -> false;
'xor'(A, B, Runtime, Context) ->
    A1 = Runtime:to_simple_value(A, Context),
    B1 = Runtime:to_simple_value(B, Context),
    Runtime:to_bool(A1, Context) xor Runtime:to_bool(B1, Context).

% 'and' and 'or' are used by the expression compiler.
'and'(A, B, Runtime, Context) ->
    A1 = Runtime:to_simple_value(A, Context),
    B1 = Runtime:to_simple_value(B, Context),
    Runtime:to_bool(A1, Context) andalso Runtime:to_bool(B1, Context).

'or'(A, B, Runtime, Context) ->
    A1 = Runtime:to_simple_value(A, Context),
    B1 = Runtime:to_simple_value(B, Context),
    Runtime:to_bool(A1, Context) orelse Runtime:to_bool(B1, Context).


concat(A, undefined, _Runtime, _Context) when is_binary(A) -> A;
concat(undefined, B, _Runtime, _Context) when is_binary(B) -> B;
concat({trans, _} = Tr, B, Runtime, Context) ->
    concat(Runtime:to_simple_value(Tr, Context), B, Runtime, Context);
concat(A, {trans, _} = Tr, Runtime, Context) ->
    concat(A, Runtime:to_simple_value(Tr, Context), Runtime, Context);
concat(A, undefined, Runtime, Context) ->
    to_maybe_list(A, Runtime, Context);
concat(undefined, B, Runtime, Context) ->
    to_maybe_list(B, Runtime, Context);
concat(A, B, _Runtime, _Context) when is_list(A), is_list(B) ->
    A++B;
concat(A, B, _Runtime, _Context) when is_binary(A), is_binary(B) ->
    <<A/binary, B/binary>>;
concat(A, B, Runtime, Context) when is_binary(A); is_binary(B) ->
    concat(
        to_maybe_binary(A, Runtime, Context),
        to_maybe_binary(B, Runtime, Context),
        Runtime, Context);
concat(A, B, Runtime, Context) ->
    concat(
        to_maybe_list(A, Runtime, Context),
        to_maybe_list(B, Runtime, Context),
        Runtime, Context).

subtract(A, undefined, _Runtime, _Context) ->
    A;
subtract(undefined, _, _Runtime, _Context) ->
    undefined;
subtract(A, B, _Runtime, _Context) when is_list(A), is_list(B) ->
    A--B;
subtract(A, B, Runtime, Context) ->
    subtract(
        to_maybe_list(A, Runtime, Context),
        to_maybe_list(B, Runtime, Context),
        Runtime, Context).


add(A, B, Runtime, Context) ->
    case to_numbers(A, B, Runtime, Context) of
        {undefined, _} -> undefined;
        {_, undefined} -> undefined;
        {A1, B1} -> A1 + B1
    end.

sub(A, B, Runtime, Context) ->
    case to_numbers(A, B, Runtime, Context) of
        {undefined, _} -> undefined;
        {_, undefined} -> undefined;
        {A1, B1} -> A1 - B1
    end.

divide(A, B, Runtime, Context) ->
    case to_numbers(A, B, Runtime, Context) of
        {undefined, _} -> undefined;
        {_, undefined} -> undefined;
        {_, 0} -> undefined;
        {_, +0.0} -> undefined;
        {_, -0.0} -> undefined;
        {A1, B1} -> A1 / B1
    end.

multiply(A, B, Runtime, Context) ->
    case to_numbers(A, B, Runtime, Context) of
        {undefined, _} -> undefined;
        {_, undefined} -> undefined;
        {A1, B1} -> A1 * B1
    end.

modulo(A, B, Runtime, Context) ->
    case to_numbers(A, B, Runtime, Context) of
        {undefined, _} -> undefined;
        {_, undefined} -> undefined;
        {_, 0} -> undefined;
        {_, +0.0} -> undefined;
        {_, -0.0} -> undefined;
        {A1, B1} -> A1 rem B1
    end.


negate(undefined, _Runtime, _Context) ->
    undefined;
negate(A, _Runtime, _Context) when is_number(A) ->
    0 - A;
negate(A, Runtime, Context) ->
    negate(to_maybe_integer(A, Runtime, Context), Runtime, Context).

ge(Input, Value, Runtime, Context) ->
    case to_values(Input, Value, Runtime, Context) of
        {undefined, _} -> undefined;
        {_, undefined} -> undefined;
        {A, B} -> A >= B
    end.

le(Input, Value, Runtime, Context) ->
    case to_values(Input, Value, Runtime, Context) of
        {undefined, _} -> undefined;
        {_, undefined} -> undefined;
        {A, B} -> A =< B
    end.

gt(Input, Value, Runtime, Context) ->
    case to_values(Input, Value, Runtime, Context) of
        {undefined, _} -> undefined;
        {_, undefined} -> undefined;
        {A, B} -> A > B
    end.

lt(Input, Value, Runtime, Context) ->
    case to_values(Input, Value, Runtime, Context) of
        {undefined, _} -> undefined;
        {_, undefined} -> undefined;
        {A, B} -> A < B
    end.

eq(A, A, _Runtime, _Context) -> true;
eq(Input, Value, Runtime, Context) ->
    {A, B} = to_values(Input, Value, Runtime, Context),
    A == B.

ne(A, A, _Runtime, _Context) -> false;
ne(Input, Value, Runtime, Context) ->
    {A, B} = to_values(Input, Value, Runtime, Context),
    A /= B.

seq(A, A, _Runtime, _Context) -> true;
seq(Input, Value, Runtime, Context) ->
    A1 = Runtime:to_simple_value(Input, Context),
    B1 = Runtime:to_simple_value(Value, Context),
    A1 == B1.

sne(A, A, _Runtime, _Context) -> false;
sne(Input, Value, Runtime, Context) ->
    A1 = Runtime:to_simple_value(Input, Context),
    B1 = Runtime:to_simple_value(Value, Context),
    A1 /= B1.


%% @doc Convert the two parameters to compatible values
to_values(undefined, B, _Runtime, _Context) ->
    {undefined, B};
to_values(A, undefined, _Runtime, _Context) ->
    {A, undefined};
to_values({trans, _} = Tr, B, Runtime, Context) ->
    to_values(Runtime:to_simple_value(Tr, Context), B, Runtime, Context);
to_values(A, {trans, _} = Tr, Runtime, Context) ->
    to_values(A, Runtime:to_simple_value(Tr, Context), Runtime, Context);
to_values(A, B, _Runtime, _Context) when is_number(A), is_number(B) ->
    {A,B};
to_values(A, B, Runtime, Context) when is_boolean(A); is_boolean(B) ->
    {z_convert:to_bool(Runtime:to_simple_value(A, Context)),
     z_convert:to_bool(Runtime:to_simple_value(B, Context))};
to_values(A, B, Runtime, Context) when is_integer(A); is_integer(B) ->
    {to_maybe_integer(A, Runtime, Context),
     to_maybe_integer(B, Runtime, Context)};
to_values(A, B, Runtime, Context) when is_float(A); is_float(B) ->
    {to_maybe_float(A, Runtime, Context),
     to_maybe_float(B, Runtime, Context)};
to_values(A, B, _Runtime, _Context) when is_binary(A), is_binary(B) ->
    {A,B};
to_values(A, B, _Runtime, _Context) when is_list(A), is_list(B) ->
    {A,B};
to_values(A, B, Runtime, Context) when is_binary(A); is_binary(B) ->
    {to_maybe_binary(A, Runtime, Context),
     to_maybe_binary(B, Runtime, Context)};
to_values(A, B, Runtime, Context) when is_tuple(A), is_tuple(B) ->
    {Runtime:to_simple_value(A, Context),
     Runtime:to_simple_value(B, Context)};
to_values(A, B, Runtime, Context) when is_binary(A); is_binary(B) ->
    {to_maybe_binary(A, Runtime, Context),
     to_maybe_binary(B, Runtime, Context)};
to_values(A, B, Runtime, Context) ->
    {to_maybe_list(A, Runtime, Context),
     to_maybe_list(B, Runtime, Context)}.


%% @doc Convert the two parameters to compatible numerical values
to_numbers(undefined, B, _Runtime, _Context) ->
    {undefined, B};
to_numbers(A, undefined, _Runtime, _Context) ->
    {A, undefined};
to_numbers(A, B, _Runtime, _Context) when is_number(A), is_number(B) ->
    {A,B};
to_numbers(A, B, Runtime, Context) when is_float(A); is_float(B) ->
    {to_maybe_float(A, Runtime, Context), to_maybe_float(B, Runtime, Context)};
to_numbers(A, B, Runtime, Context) ->
    {to_maybe_integer(A, Runtime, Context), to_maybe_integer(B, Runtime, Context)}.

to_maybe_integer(A, _Runtime, _Context) when is_number(A) ->
    A;
to_maybe_integer(A, Runtime, Context) ->
    try
        A1 = Runtime:to_simple_value(A, Context),
        z_convert:to_integer(A1)
    catch
        _:_ -> undefined
    end.

to_maybe_float(A, _Runtime, _Context) when is_float(A) ->
    A;
to_maybe_float(A, Runtime, Context) ->
    try
        A1 = Runtime:to_simple_value(A, Context),
        z_convert:to_float(A1)
    catch
        _:_ -> undefined
    end.

to_maybe_binary(A, _Runtime, _Context) when is_binary(A) ->
    A;
to_maybe_binary(A, Runtime, Context) ->
    try
        A1 = Runtime:to_simple_value(A, Context),
        z_convert:to_binary(A1)
    catch
        _:_ -> undefined
    end.

to_maybe_list(A, _Runtime, _Context) when is_list(A) ->
    A;
to_maybe_list(A, Runtime, Context) ->
    try
        A1 = Runtime:to_simple_value(A, Context),
        z_convert:to_list(A1)
    catch
        _:_ -> undefined
    end.