dbMango/Rms.Risk.Mango.Language/Ast/AstExpressionOperation.cs
Alexander Shabarshov 2a7a24c9e7 Initial contribution
2025-11-03 14:43:26 +00:00

291 lines
8.6 KiB
C#

/*
* dbMango
*
* Copyright 2025 Deutsche Bank AG
* SPDX-License-Identifier: Apache-2.0
*
* 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.
*/
using System.Diagnostics.CodeAnalysis;
namespace Rms.Risk.Mango.Language.Ast;
public class AstExpressionOperation : AstExpression
{
[SuppressMessage("ReSharper", "InconsistentNaming")]
public enum OperationType
{
AND,
OR,
EQ,
NEQ,
GT,
GTE,
LT,
LTE,
PLUS,
MINUS,
DIVIDE,
MULTIPLY
}
public static OperationType GetOperationType(string op) => op switch
{
"$and" => OperationType.AND,
"AND" => OperationType.AND,
"$or" => OperationType.OR,
"OR" => OperationType.OR,
"$eq" => OperationType.EQ,
"==" => OperationType.EQ,
"$ne" => OperationType.NEQ,
"<>" => OperationType.NEQ,
"!=" => OperationType.NEQ,
"$gt" => OperationType.GT,
">" => OperationType.GT,
"$gte" => OperationType.GTE,
">=" => OperationType.GTE,
"$lt" => OperationType.LT,
"<" => OperationType.LT,
"$lte" => OperationType.LTE,
"<=" => OperationType.LTE,
"$add" => OperationType.PLUS,
"+" => OperationType.PLUS,
"$subtract" => OperationType.MINUS,
"-" => OperationType.MINUS,
"$divide" => OperationType.DIVIDE,
"/" => OperationType.DIVIDE,
"$multiply" => OperationType.MULTIPLY,
"*" => OperationType.MULTIPLY,
_ => throw new($"Invalid operator '{op}'")
};
public static string GetOperationStr(OperationType op) => op switch
{
OperationType.AND => "AND",
OperationType.OR => "OR",
OperationType.EQ => "==",
OperationType.NEQ => "!=",
OperationType.GT => ">",
OperationType.GTE => ">=",
OperationType.LT => "<",
OperationType.LTE => "<=",
OperationType.PLUS => "+",
OperationType.MINUS => "-",
OperationType.DIVIDE => "/",
OperationType.MULTIPLY => "*",
_ => throw new($"Invalid operator '{op}'")
};
public static string GetOperationCall(OperationType op) => op switch
{
OperationType.AND => "$and",
OperationType.OR => "$or",
OperationType.EQ => "$eq",
OperationType.NEQ => "$ne",
OperationType.GT => "$gt",
OperationType.GTE => "$gte",
OperationType.LT => "$lt",
OperationType.LTE => "$lte",
OperationType.PLUS => "$add",
OperationType.MINUS => "$subtract",
OperationType.DIVIDE => "$divide",
OperationType.MULTIPLY => "$multiply",
_ => throw new($"Invalid operator '{op}'")
};
private static HashSet<OperationType> _condition =
[
OperationType.EQ,
OperationType.NEQ,
OperationType.LT,
OperationType.LTE,
OperationType.GT,
OperationType.GTE
];
private static HashSet<OperationType> _groupable =
[
OperationType.AND,
OperationType.OR,
OperationType.PLUS,
OperationType.MULTIPLY
];
public AstExpressionOperation(string op, params IEnumerable<AstExpression> args)
: this(GetOperationType(op), args)
{
}
public AstExpressionOperation(OperationType op, params IEnumerable<AstExpression> args)
{
Operator = op;
foreach (var arg in args)
Add(arg);
}
public OperationType Operator { get; }
public string OperatorStr => GetOperationStr(Operator);
public IReadOnlyList<AstExpression> Args => Children.OfType<AstExpression>().ToList();
public override void Append(StringBuilder sb, int indent)
{
var first = true;
foreach (var arg in Args)
{
if (first)
{
first = false;
}
else
{
if (Operator == OperationType.AND || Operator == OperationType.OR)
{
sb.AppendLine();
sb.Append($"{Spaces(indent)}{OperatorStr} ");
}
else
{
sb.Append($" {OperatorStr} ");
}
}
arg.Append(sb, indent);
}
}
public override JsonNode? AsJson()
{
var functionArgs = Args;
var withinFunction = GetProperty(AstExpressionFunctionCall.WithinFunctionProperty, false);
// special case - equality operator
if (_condition.Contains(Operator) && !withinFunction)
{
// for root level = use this form: { field : value }
if (Operator == OperationType.EQ
&& functionArgs is [
AstExpressionVariable varArg,
{ } condArg and (AstExpressionNumber or AstExpressionString or AstExpressionVariable)
]
)
{
var op = new JsonObject(
[
new(
varArg.Name,
condArg.AsJson()
)
]);
return op;
}
if (Operator == OperationType.EQ
&& functionArgs is [
{ } condArg1 and (AstExpressionNumber or AstExpressionString or AstExpressionVariable),
AstExpressionVariable varArg1
]
)
{
var op = new JsonObject(
[
new(
varArg1.Name,
condArg1.AsJson()
)
]);
return op;
}
if (functionArgs is
[
AstExpressionVariable varArg2,
{ } condArg2
])
{
// for other conditions use { field : { $op : value } }
var op1 = new JsonObject(
[
new(
varArg2.Name,
new JsonObject([
new(
GetOperationCall(Operator),
condArg2.AsJson()
)
])
)
]
);
return op1;
}
}
var funcArgs = Args;
// make one for expressions like a+b+c+d
if (_groupable.Contains(Operator))
{
var op = CreateFunctionCall(funcArgs);
return op;
}
// make separate calls for expressions like a-b-c-d
var revArgs = funcArgs.Reverse().ToList();
var currentFunc = CreateFunctionCall(revArgs[1], revArgs[0]);
for (var i = 2; i < revArgs.Count; i++)
currentFunc = ChainFunctionCall(revArgs[i], currentFunc);
return currentFunc;
JsonObject CreateFunctionCall(params IReadOnlyList<AstExpression> a)
{
var args = new JsonArray();
foreach (var v in a.Select(x => x.AsJson()))
{
args.Add(v);
}
var op = new JsonObject(
[
new(
GetOperationCall(Operator),
args
)
]
);
return op;
}
JsonObject ChainFunctionCall(AstExpression arg1, JsonObject arg2)
{
JsonArray args =
[
arg1.AsJson(),
arg2
];
var op = new JsonObject(
[
new(
GetOperationCall(Operator),
args
)
]
);
return op;
}
}
}