Configurator/Core/Ini/FormulaEngine.cs (497 lines of code) (raw):

/* Copyright (c) 2023, 2024, Oracle and/or its affiliates. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License, version 2.0, as published by the Free Software Foundation. This program is designed to work with certain software (including but not limited to OpenSSL) that is licensed under separate terms, as designated in a particular file or component or in included license documentation. The authors of MySQL hereby grant you an additional permission to link the program and your derivative works with the separately licensed software that they have either included with the program or referenced in the documentation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License, version 2.0, for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ using System; using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Text.RegularExpressions; using MySql.Configurator.Base.Enums; namespace MySql.Configurator.Core.Ini { internal class FormulaEngine { private readonly Dictionary<string, string> _formulaVariables; private Stack _ops; private Queue _output; public FormulaEngine() { OriginalExpression = string.Empty; TransitionExpression = string.Empty; PostfixExpression = string.Empty; _formulaVariables = new Dictionary<string, string>(); } public string OriginalExpression { get; private set; } public string PostfixExpression { get; private set; } public string TransitionExpression { get; private set; } public void AssignFormulaVariable(string keyName, string value) { string variableValue; _formulaVariables.TryGetValue(keyName, out variableValue); if (variableValue != value) { _formulaVariables[keyName] = value; } } public string Evaluate() { var result = new Stack(); var resultToken = new FormulaToken(); // While there are input tokens left foreach (object obj in _output) { // Read the next token from input. var token = (FormulaToken)obj; FormulaToken operand1; FormulaToken operand2; switch (token.TokenValueType) { case FormulaTokenType.Number: case FormulaTokenType.Constant: case FormulaTokenType.Variable: result.Push(token); break; case FormulaTokenType.Assignment: if (result.Count >= 2) { operand2 = (FormulaToken)result.Pop(); operand1 = (FormulaToken)result.Pop(); if (operand1.TokenValueType == FormulaTokenType.Variable && (operand2.TokenValueType == FormulaTokenType.Number || operand2.TokenValueType == FormulaTokenType.Constant || operand2.TokenValueType == FormulaTokenType.Variable)) { _formulaVariables[operand1.TokenValue] = GetTokenValue(operand2).ToString(CultureInfo.CurrentCulture); } else { throw new Exception("Assignment error!"); } result.Push(operand1); } else { throw new Exception("Evaluation error!"); } break; case FormulaTokenType.Plus: if (result.Count >= 2) { operand2 = (FormulaToken)result.Pop(); operand1 = (FormulaToken)result.Pop(); resultToken.TokenValueType = FormulaTokenType.Number; resultToken.TokenValue = (GetTokenValue(operand2) + GetTokenValue(operand1)).ToString(CultureInfo.CurrentCulture); result.Push(resultToken); } else { throw new Exception("Evaluation error!"); } break; case FormulaTokenType.Minus: if (result.Count >= 2) { operand2 = (FormulaToken)result.Pop(); operand1 = (FormulaToken)result.Pop(); resultToken.TokenValueType = FormulaTokenType.Number; resultToken.TokenValue = (GetTokenValue(operand1) - GetTokenValue(operand2)).ToString(CultureInfo.CurrentCulture); result.Push(resultToken); } else { throw new Exception("Evaluation error!"); } break; case FormulaTokenType.Multiply: if (result.Count >= 2) { operand2 = (FormulaToken)result.Pop(); operand1 = (FormulaToken)result.Pop(); resultToken.TokenValueType = FormulaTokenType.Number; resultToken.TokenValue = (GetTokenValue(operand2) * GetTokenValue(operand1)).ToString(CultureInfo.CurrentCulture); result.Push(resultToken); } else { throw new Exception("Evaluation error!"); } break; case FormulaTokenType.Divide: if (result.Count >= 2) { operand2 = (FormulaToken)result.Pop(); operand1 = (FormulaToken)result.Pop(); resultToken.TokenValueType = FormulaTokenType.Number; resultToken.TokenValue = (GetTokenValue(operand1) / GetTokenValue(operand2)).ToString(CultureInfo.CurrentCulture); result.Push(resultToken); } else { throw new Exception("Evaluation error!"); } break; case FormulaTokenType.Exponent: if (result.Count >= 2) { operand2 = (FormulaToken)result.Pop(); operand1 = (FormulaToken)result.Pop(); resultToken.TokenValueType = FormulaTokenType.Number; resultToken.TokenValue = Math.Pow(GetTokenValue(operand1), GetTokenValue(operand2)).ToString(CultureInfo.CurrentCulture); result.Push(resultToken); } else { throw new Exception("Evaluation error!"); } break; case FormulaTokenType.UnaryMinus: if (result.Count >= 1) { operand1 = (FormulaToken)result.Pop(); resultToken.TokenValueType = FormulaTokenType.Number; resultToken.TokenValue = (-GetTokenValue(operand1)).ToString(CultureInfo.CurrentCulture); result.Push(resultToken); } else { throw new Exception("Evaluation error!"); } break; case FormulaTokenType.Sine: if (result.Count >= 1) { operand1 = (FormulaToken)result.Pop(); resultToken.TokenValueType = FormulaTokenType.Number; resultToken.TokenValue = Math.Sin(GetTokenValue(operand1)).ToString(CultureInfo.CurrentCulture); result.Push(resultToken); } else { throw new Exception("Evaluation error!"); } break; case FormulaTokenType.Cosine: if (result.Count >= 1) { operand1 = (FormulaToken)result.Pop(); resultToken.TokenValueType = FormulaTokenType.Number; resultToken.TokenValue = Math.Cos(GetTokenValue(operand1)).ToString(CultureInfo.CurrentCulture); result.Push(resultToken); } else { throw new Exception("Evaluation error!"); } break; case FormulaTokenType.Tangent: if (result.Count >= 1) { operand1 = (FormulaToken)result.Pop(); resultToken.TokenValueType = FormulaTokenType.Number; resultToken.TokenValue = Math.Tan(GetTokenValue(operand1)).ToString(CultureInfo.CurrentCulture); result.Push(resultToken); } else { throw new Exception("Evaluation error!"); } break; case FormulaTokenType.Round: if (result.Count >= 2) { operand2 = (FormulaToken)result.Pop(); operand1 = (FormulaToken)result.Pop(); resultToken.TokenValueType = FormulaTokenType.Number; resultToken.TokenValue = Math.Round((GetTokenValue(operand1) / GetTokenValue(operand2)) * GetTokenValue(operand2)).ToString(CultureInfo.CurrentCulture); result.Push(resultToken); } else { throw new Exception("Evaluation error!"); } break; case FormulaTokenType.Max: if (result.Count >= 2) { operand2 = (FormulaToken)result.Pop(); operand1 = (FormulaToken)result.Pop(); result.Push(GetTokenValue(operand1) >= GetTokenValue(operand2) ? operand1 : operand2); } else { throw new Exception("Evaluation error!"); } break; case FormulaTokenType.Min: if (result.Count >= 2) { operand2 = (FormulaToken)result.Pop(); operand1 = (FormulaToken)result.Pop(); result.Push(GetTokenValue(operand1) <= GetTokenValue(operand2) ? operand1 : operand2); } else { throw new Exception("Evaluation error!"); } break; } } switch (result.Count) { case 0: // No value on the stack means there is no value associated with a parameter. return ""; case 1: // If there is only one value in the stack, that value is the result of the calculation. return GetTokenText((FormulaToken)result.Pop()); default: // If there are more values in the stack // (Error) The user input too many values. throw new Exception("Evaluation error. Invalid number of arguments."); } } public void Parse(string expression) { _output = new Queue(); _ops = new Stack(); OriginalExpression = expression; string rpnBuffer = expression; // filter out the K, M, G unit specifiers. rpnBuffer = Regex.Replace(rpnBuffer, @"(?<number>\d+(\.\d+)?)K", " ${number} * 1024 "); rpnBuffer = Regex.Replace(rpnBuffer, @"(?<number>\d+(\.\d+)?)M", " ${number} * 1024 * 1024"); rpnBuffer = Regex.Replace(rpnBuffer, @"(?<number>\d+(\.\d+)?)G", " ${number} * 1024 * 1024 * 1024"); rpnBuffer = rpnBuffer.ToLower(); // captures numbers. Anything like 11 or 22.34 is captured rpnBuffer = Regex.Replace(rpnBuffer, @"(?<number>\d+(\.\d+)?)", " ${number} "); // captures these symbols: + - * / ^ ( ) rpnBuffer = Regex.Replace(rpnBuffer, @"(?<ops>[+\-*/^:()])", " ${ops} "); // captures constants, variables, and math functions. rpnBuffer = Regex.Replace(rpnBuffer, @"(?<var>([a-z_]+))", " ${var} "); // trims up consecutive spaces and replace it with just one space rpnBuffer = Regex.Replace(rpnBuffer, @"\s+", " ").Trim(); // The following chunk captures unary minus operations. // 1) We replace every minus sign with the string "MINUS". // 2) Then if we find a "MINUS" with a number or constant in front, // then it's a normal minus operation. // 3) Otherwise, it's a unary minus operation. // Step 1. rpnBuffer = Regex.Replace(rpnBuffer, "-", "MINUS"); // Step 2. Looking for pi or e or generic number \d+(\.\d+)? //rpn_buffer = Regex.Replace(rpn_buffer, @"(?<number>(pi|e|(\d+(\.\d+)?)))\s+MINUS", "${number} -"); rpnBuffer = Regex.Replace(rpnBuffer, @"(?<number>([a-z_]+|[)]|(\d+(\.\d+)?)))\s+MINUS", "${number} -"); // Step 3. Use the tilde ~ as the unary minus operator rpnBuffer = Regex.Replace(rpnBuffer, "MINUS", "~"); TransitionExpression = rpnBuffer; // tokenize it! string[] parsedTokens = rpnBuffer.Split(" ".ToCharArray()); int i; FormulaToken opstoken; for (i = 0; i < parsedTokens.Length; ++i) { var token = new FormulaToken { TokenValue = parsedTokens[i], TokenValueType = FormulaTokenType.None }; double tokenValue; if (double.TryParse(parsedTokens[i], out tokenValue)) { token.TokenValueType = FormulaTokenType.Number; // If the token is a number, then add it to the output queue. _output.Enqueue(token); continue; } switch (parsedTokens[i]) { case "+": token.TokenValueType = FormulaTokenType.Plus; if (_ops.Count > 0) { opstoken = (FormulaToken)_ops.Peek(); // while there is an operator, o2, at the top of the stack while (IsOperatorToken(opstoken.TokenValueType)) { // pop o2 off the stack, onto the output queue; _output.Enqueue(_ops.Pop()); if (_ops.Count > 0) opstoken = (FormulaToken)_ops.Peek(); else break; } } // push o1 onto the operator stack. _ops.Push(token); break; case "-": token.TokenValueType = FormulaTokenType.Minus; if (_ops.Count > 0) { opstoken = (FormulaToken)_ops.Peek(); // while there is an operator, o2, at the top of the stack while (IsOperatorToken(opstoken.TokenValueType)) { // pop o2 off the stack, onto the output queue; _output.Enqueue(_ops.Pop()); if (_ops.Count > 0) opstoken = (FormulaToken)_ops.Peek(); else break; } } // push o1 onto the operator stack. _ops.Push(token); break; case "*": token.TokenValueType = FormulaTokenType.Multiply; if (_ops.Count > 0) { opstoken = (FormulaToken)_ops.Peek(); // while there is an operator, o2, at the top of the stack while (IsOperatorToken(opstoken.TokenValueType)) { if (opstoken.TokenValueType == FormulaTokenType.Plus || opstoken.TokenValueType == FormulaTokenType.Minus || opstoken.TokenValueType == FormulaTokenType.Assignment) break; else { // Once we're in here, the following algorithm condition is satisfied. // o1 is associative or left-associative and its precedence is less than (lower precedence) or equal to that of o2, or // o1 is right-associative and its precedence is less than (lower precedence) that of o2, // pop o2 off the stack, onto the output queue; _output.Enqueue(_ops.Pop()); if (_ops.Count > 0) opstoken = (FormulaToken)_ops.Peek(); else break; } } } // push o1 onto the operator stack. _ops.Push(token); break; case "/": token.TokenValueType = FormulaTokenType.Divide; if (_ops.Count > 0) { opstoken = (FormulaToken)_ops.Peek(); // while there is an operator, o2, at the top of the stack while (IsOperatorToken(opstoken.TokenValueType)) { if (opstoken.TokenValueType == FormulaTokenType.Plus || opstoken.TokenValueType == FormulaTokenType.Minus || opstoken.TokenValueType == FormulaTokenType.Assignment) break; else { // Once we're in here, the following algorithm condition is satisfied. // o1 is associative or left-associative and its precedence is less than (lower precedence) or equal to that of o2, or // o1 is right-associative and its precedence is less than (lower precedence) that of o2, // pop o2 off the stack, onto the output queue; _output.Enqueue(_ops.Pop()); if (_ops.Count > 0) opstoken = (FormulaToken)_ops.Peek(); else break; } } } // push o1 onto the operator stack. _ops.Push(token); break; case "^": token.TokenValueType = FormulaTokenType.Exponent; // push o1 onto the operator stack. _ops.Push(token); break; case "~": token.TokenValueType = FormulaTokenType.UnaryMinus; // push o1 onto the operator stack. _ops.Push(token); break; case "(": token.TokenValueType = FormulaTokenType.LeftParenthesis; // If the token is a left parenthesis, then push it onto the stack. _ops.Push(token); break; case ")": token.TokenValueType = FormulaTokenType.RightParenthesis; if (_ops.Count > 0) { opstoken = (FormulaToken)_ops.Peek(); // Until the token at the top of the stack is a left parenthesis while (opstoken.TokenValueType != FormulaTokenType.LeftParenthesis) { // pop operators off the stack onto the output queue _output.Enqueue(_ops.Pop()); if (_ops.Count > 0) opstoken = (FormulaToken)_ops.Peek(); else { // If the stack runs out without finding a left parenthesis, // then there are mismatched parentheses. throw new Exception("Unbalanced parenthesis!"); } } // Pop the left parenthesis from the stack, but not onto the output queue. _ops.Pop(); } if (_ops.Count > 0) { opstoken = (FormulaToken)_ops.Peek(); // If the token at the top of the stack is a function token if (IsFunctionToken(opstoken.TokenValueType)) { // pop it and onto the output queue. _output.Enqueue(_ops.Pop()); } } break; case "pi": token.TokenValueType = FormulaTokenType.Constant; // If the token is a number, then add it to the output queue. _output.Enqueue(token); break; case "e": token.TokenValueType = FormulaTokenType.Constant; // If the token is a number, then add it to the output queue. _output.Enqueue(token); break; case "sin": token.TokenValueType = FormulaTokenType.Sine; // If the token is a function token, then push it onto the stack. _ops.Push(token); break; case "cos": token.TokenValueType = FormulaTokenType.Cosine; // If the token is a function token, then push it onto the stack. _ops.Push(token); break; case "tan": token.TokenValueType = FormulaTokenType.Tangent; // If the token is a function token, then push it onto the stack. _ops.Push(token); break; case "rnd": token.TokenValueType = FormulaTokenType.Round; // If the token is a function token, then push it onto the stack. _ops.Push(token); break; case "max": token.TokenValueType = FormulaTokenType.Max; // If the token is a function token, then push it onto the stack. _ops.Push(token); break; case "min": token.TokenValueType = FormulaTokenType.Min; // If the token is a function token, then push it onto the stack. _ops.Push(token); break; case ":": // If the token is an assignment token, then push it onto the stack. token.TokenValueType = FormulaTokenType.Assignment; // push o0 onto the operator stack. _ops.Push(token); break; case ",": if (_ops.Count > 0) { opstoken = (FormulaToken)_ops.Peek(); // Until the token at the top of the stack is a left parenthesis while (opstoken.TokenValueType != FormulaTokenType.LeftParenthesis) { // pop operators off the stack onto the output queue _output.Enqueue(_ops.Pop()); if (_ops.Count > 0) opstoken = (FormulaToken)_ops.Peek(); else { // If the stack runs out without finding a left parenthesis, // then there are mismatched parentheses. throw new Exception("Couldn't find function start!"); } } } break; default: if (Regex.IsMatch(parsedTokens[i], @"[a-z_]+")) { token.TokenValueType = FormulaTokenType.Variable; _output.Enqueue(token); // This is a variable. } break; } } // While there are still operator tokens in the stack: while (_ops.Count != 0) { opstoken = (FormulaToken)_ops.Pop(); // If the operator token on the top of the stack is a parenthesis if (opstoken.TokenValueType == FormulaTokenType.LeftParenthesis) { // then there are mismatched parenthesis. throw new Exception("Unbalanced parenthesis!"); } // Pop the operator onto the output queue. _output.Enqueue(opstoken); } PostfixExpression = string.Empty; foreach (object obj in _output) { opstoken = (FormulaToken)obj; PostfixExpression += $"{opstoken.TokenValue} "; } } private string GetTokenText(FormulaToken token) { string result = string.Empty; switch (token.TokenValueType) { case FormulaTokenType.Number: result = token.TokenValue; break; case FormulaTokenType.Variable: if (!_formulaVariables.TryGetValue(token.TokenValue, out result)) result = "0.0"; break; case FormulaTokenType.Constant: switch (token.TokenValue) { case "pi": result = Math.PI.ToString(CultureInfo.CurrentCulture); break; case "e": result = Math.E.ToString(CultureInfo.CurrentCulture); break; } break; } return result; } private double GetTokenValue(FormulaToken token) { return double.Parse(GetTokenText(token)); } private bool IsFunctionToken(FormulaTokenType t) { bool result; switch (t) { case FormulaTokenType.Sine: case FormulaTokenType.Cosine: case FormulaTokenType.Tangent: case FormulaTokenType.Round: case FormulaTokenType.Max: case FormulaTokenType.Min: result = true; break; default: result = false; break; } return result; } private bool IsOperatorToken(FormulaTokenType t) { bool result; switch (t) { case FormulaTokenType.Plus: case FormulaTokenType.Minus: case FormulaTokenType.Multiply: case FormulaTokenType.Divide: case FormulaTokenType.Exponent: case FormulaTokenType.UnaryMinus: result = true; break; default: result = false; break; } return result; } } }