using System.Collections.Generic;
using System.Globalization;
using System.Text.RegularExpressions;
namespace Polymator.Games.Utils.MathHelper.Test {
public class MathExpressionEvaluatorTest {
public static void Main() {
var testCases = new Dictionary<string, (string Expression, double ExpectedResult)> {
["Simple Scientific Add"] = ("1.2e3 + 4.5e2", 1650),
["Scientific Subtraction"] = ("2.5e4 - 1.2e3", 23800),
["Scientific Multiplication"] = ("3e2 * 2e1", 6000),
["Scientific Division"] = ("1.2e6 / 4e2", 3000),
["Scientific Exponent Base"] = ("2e2^2", 40000),
["Scientific Exponent Power"] = ("2^3e1", 1073741824),
["Scientific Inside Sqrt"] = ("sqrt(9e4)", 300),
["Scientific Inside Log"] = ("log(1e3)", 3),
["Scientific with Negative Power"] = ("5e-1 * 1e1", 5),
["Scientific Notation with Parentheses"] = ("(1e2 + 2e2) * (1e0 + 3)", 1200),
["Mixed Scientific and Decimal"] = ("1.5 + 2e2", 201.5),
["Scientific with Function Nesting"] = ("sqrt(1.6e2 + 1.6e1)", 13.2665),
["Nested Scientific Log"] = ("log(1e2 + 9e1)", 2.27875),
["Deep Scientific Nesting"] = ("sqrt((1.2e3 + 3e2)^2)", 1500),
["Scientific Chained Ops"] = ("1e1 + 2e1 * 3e1", 610),
["Negative Scientific Notation"] = ("5e-1 + 2", 2.5),
["Square Root & Log"] = ("sqrt(16) + log(10) * 2", 6),
["Compound Interest"] = ("1000 * (1 + 0.05)^3", 1157.625),
["Pythagorean Theorem"] = ("(10^2 + 5^2)^0.5", 11.1803398875),
["Power & Division"] = ("(2^3 + 3^2) / (5 - 3)", 8.5),
["Nested Operations"] = ("10 * (5 + 2) / sqrt(16)", 17.5),
["Exponent & Logarithm"] = ("(2^4 - log(10) * 3) / 2", 6.5),
["Physics Formula"] = ("(sqrt(49) + 3^3) * 2", 68),
["Basic Math"] = ("100 * 1.2 + (10 * 2.5)", 145),
["Multiple Nested Braces"] = ("(5 * (3 + 2)) + ((4 + 1) * 2)", 35),
["Deep Nested Braces"] = ("(((2+3) * 2) + 4) * 3", 42),
["Function in Parentheses"] = ("sqrt((9 + 7) * 4)", 8),
["Log with Nested Braces"] = ("log((100 + 10) / 2) * 3", 5.2210880685),
["Complex Nested Functions"] = ("sqrt((2^3 + log(100)) * (3 + 1))", 6.3245553203),
["Nested Log Function"] = ("log(sqrt(100)) + 5", 6),
["Exponent with Function"] = ("(3^sqrt(9)) + (2^log(100))", 31),
["Bracket within Function"] = ("sqrt((4 + 5) * 9)", 9),
["Deep Nested Exponents"] = ("((2^3)^2)^0.5", 8),
["Log with Multiplication"] = ("log(1000) * 2 + sqrt(25)", 11),
["Triple Nested Functions"] = ("sqrt(log(100) + (3^2))", 3.31662),
["Exponent with Log and Parentheses"] = ("(2^(log(100))) / sqrt(16)", 1),
["Combination of Operations"] = ("(5 + 3) * (2^3) - sqrt(64) / 2", 60),
["Multiple Function Nesting"] = ("sqrt((10 + log(100))^2)", 12),
["Complex Parentheses & Functions"] = ("((3^2 + 4^2) / sqrt(25)) + log(1000)", 8),
["Highly Nested Expression"] = ("(sqrt((2^3) + log(100)) * (3 + 1))^2", 160),
["Multiple Exponents and Roots"] = ("(4^sqrt(16)) + (9^(1/2))", 259),
["Function in Deep Nested Brackets"] = ("sqrt(((3+4)^2) + ((5-2)^2))", 7.61577),
["Long Mixed Expression"] = ("(2^3 + sqrt(49) - log(10)) * 3", 42),
["Power and Logarithm Interaction"] = ("(5^(log(100))) / 5", 5),
["Sqrt of Multiple Operations"] = ("sqrt((3+7) * (5-2))", 5.47723),
["Nested Logarithms"] = ("log(log(1000) * 10)", 1.47712),
["Deep Exponential Roots"] = ("((2^6)^(1/3))", 4),
["Scientific Mixed with Functions"] = ("sqrt(1e4) + log(1e2)", 102),
["Natural Logarithm"] = ("ln(e)", 1),
["Exponential Function"] = ("exp(1)", Math.E),
["Pi Constant"] = ("pi * 2", Math.PI * 2),
["Euler Constant"] = ("e + 1", Math.E + 1),
["Sine in Radians"] = ("sin(pi/2)", 1),
["Cosine in Radians"] = ("cos(0)", 1),
["Tangent in Radians"] = ("tan(pi/4)", 1),
["Sine in Degrees"] = ("sind(90)", 1),
["Cosine in Degrees"] = ("cosd(180)", -1),
["Tangent in Degrees"] = ("tand(45)", 1),
["Nested Trig & Exp"] = ("sin(pi/2) + exp(0)", 2),
["Complex Constants & Trig"] = ("cos(pi) + e", Math.Cos(Math.PI) + Math.E),
foreach (var testCase in testCases) {
var expr = testCase.Value.Expression;
var expected = testCase.Value.ExpectedResult;
expected = Math.Round(expected, 5);
var result = MathExpressionEvaluator.EvaluateExpression(expr);
result = Math.Round(result, 5);
var rs = result == expected ? "Pass" : "Fail";
Console.WriteLine($"Expression: {testCase.Key} => {expr}");
Console.WriteLine($"Result: {result} <=> ER: {expected} => {rs}");
Console.WriteLine("--------------------------------------\n");
namespace Polymator.Games.Utils.MathHelper {
public static class MathExpressionEvaluator {
static Dictionary<string, Func<double, double>> singleArgFunctions = new() {
["sqrt"] = x => Math.Sqrt(x),
["log"] = x => Math.Log10(x),
["ln"] = x => Math.Log(x),
["exp"] = x => Math.Exp(x),
["sin"] = x => Math.Sin(x),
["cos"] = x => Math.Cos(x),
["tan"] = x => Math.Tan(x),
["sind"] = x => Math.Sin(DegToRad(x)),
["cosd"] = x => Math.Cos(DegToRad(x)),
["tand"] = x => Math.Tan(DegToRad(x)),
static Dictionary<string, Func<double, double, double>> dualArgFunctions = new() {
["pow"] = (x, y) => Math.Pow(x, y)
static Dictionary<string, double> constants = new() {
public static double EvaluateExpression(this string expression) {
var tokens = Tokenize(expression);
var postfix = ConvertToPostfix(tokens);
return EvaluatePostfix(postfix);
static double DegToRad(double degrees) => degrees * Math.PI / 180;
static bool IsRightAssociative(string op) => op == "^";
static Queue<string> Tokenize(string expr) {
var tokens = new Queue<string>();
var pattern = @"([A-Za-z]+|[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?|[+\-*/^(),])";
foreach (Match match in Regex.Matches(expr, pattern))
tokens.Enqueue(match.Value);
static Queue<string> ConvertToPostfix(Queue<string> tokens) {
Queue<string> output = new();
Stack<string> ops = new();
while (tokens.Count > 0) {
string token = tokens.Dequeue();
if (double.TryParse(token, NumberStyles.Float, CultureInfo.InvariantCulture, out _) || constants.ContainsKey(token)) {
} else if (singleArgFunctions.ContainsKey(token) || dualArgFunctions.ContainsKey(token)) {
} else if (token == ",") {
while (ops.Peek() != "(")
output.Enqueue(ops.Pop());
} else if (token == "(") {
} else if (token == ")") {
while (ops.Count > 0 && ops.Peek() != "(")
output.Enqueue(ops.Pop());
if (ops.Count > 0 && ops.Peek() == "(")
if (ops.Count > 0 && (singleArgFunctions.ContainsKey(ops.Peek()) || dualArgFunctions.ContainsKey(ops.Peek())))
output.Enqueue(ops.Pop());
} else if ("+-*/^".Contains(token)) {
while (ops.Count > 0 && GetPrecedence(ops.Peek()) > 0 &&
(GetPrecedence(ops.Peek()) > GetPrecedence(token) ||
(GetPrecedence(ops.Peek()) == GetPrecedence(token) && !IsRightAssociative(token)))) {
output.Enqueue(ops.Pop());
output.Enqueue(ops.Pop());
static int GetPrecedence(string op) => op switch {
static double EvaluatePostfix(Queue<string> postfix) {
Stack<double> stack = new();
while (postfix.Count > 0) {
string token = postfix.Dequeue();
if (double.TryParse(token, NumberStyles.Float, CultureInfo.InvariantCulture, out double num)) {
} else if (constants.TryGetValue(token, out double constVal)) {
} else if (singleArgFunctions.TryGetValue(token, out var func1)) {
stack.Push(func1(stack.Pop()));
} else if (dualArgFunctions.TryGetValue(token, out var func2)) {
stack.Push(ApplyOperator(token, b, a));
private static double ApplyOperator(string token, double b, double a) {
_ => throw new Exception($"Invalid operator: {token}")