/* * 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 _condition = [ OperationType.EQ, OperationType.NEQ, OperationType.LT, OperationType.LTE, OperationType.GT, OperationType.GTE ]; private static HashSet _groupable = [ OperationType.AND, OperationType.OR, OperationType.PLUS, OperationType.MULTIPLY ]; public AstExpressionOperation(string op, params IEnumerable args) : this(GetOperationType(op), args) { } public AstExpressionOperation(OperationType op, params IEnumerable args) { Operator = op; foreach (var arg in args) Add(arg); } public OperationType Operator { get; } public string OperatorStr => GetOperationStr(Operator); public IReadOnlyList Args => Children.OfType().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 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; } } }