Skip to content

Instantly share code, notes, and snippets.

@holly-hacker
Created October 29, 2018 20:05
Show Gist options
  • Save holly-hacker/1280006d9c0c349004d6a4d0456b2235 to your computer and use it in GitHub Desktop.
Save holly-hacker/1280006d9c0c349004d6a4d0456b2235 to your computer and use it in GitHub Desktop.
Mathematical expression parser
using System;
using System.Collections.Generic;
using System.Linq;
namespace test20181029_matheval {
public static class Evaluator
{
private static Dictionary<char, Func<double, double, double>>[] functions;
static Evaluator()
{
functions = new[] {
new Dictionary<char, Func<double, double, double>> {
{'^', Math.Pow},
},
new Dictionary<char, Func<double, double, double>> {
{'*', (x, y) => x * y},
{'/', (x, y) => x / y},
},
new Dictionary<char, Func<double, double, double>> {
{'+', (x, y) => x + y},
{'-', (x, y) => x - y},
},
};
}
public static double Evaluate(string input) => Flatten(Tokenize(input.AsSpan())); // input is span to prevent string copies (heap allocations) on recursion
internal static double Flatten(List<object> root)
{
// Recursively flatten lists (replaces parens with the evaluation of their content)
for (int i = 0; i < root.Count; i++)
if (root[i] is List<object> objList)
root[i] = Flatten(objList);
// Console.Write($"Evaluating {string.Join(string.Empty, root.Select(x => x.ToString()))} to ");
// Check every level of operator precedence
foreach (Dictionary<char, Func<double, double, double>> dic in functions) {
for (int i = 0; i < root.Count; i++) {
if (!(root[i] is char c)) continue;
double prev;
double next;
if (root[i - 1] is double d1 && root[i + 1] is double d2) {
prev = d1;
next = d2;
} else throw new Exception();
if (!dic.ContainsKey(c)) continue;
root[--i] = dic[c](prev, next);
root.RemoveAt(i + 2);
root.RemoveAt(i + 1);
}
}
if (root.Count != 1 || !(root[0] is double result))
throw new Exception("Could not reduce flat token list to a single value: " + string.Join(string.Empty, root.Select(x => x.ToString())));
// Console.WriteLine(result);
return result;
}
internal static List<object> Tokenize(ReadOnlySpan<char> buffer)
{
var root = new List<object>();
for (int i = 0; i < buffer.Length;) {
char c = buffer[i++];
if (c.IsDigit()) {
double d = c.ParseInt();
int div = 0;
while (i != buffer.Length) {
c = buffer[i++];
if (c == '.') {
div = div == 0 ? 1 : throw new Exception("Multiple periods in int"); // set div to 0, or throw
} else if (c.IsDigit()) {
if (div == 0) {
d = d * 10 + c.ParseInt();
} else {
d += c.ParseInt() * (1f / (div *= 10));
}
}
// else if (c.IsWhitespace()) continue;
else {
--i;
break;
}
}
root.Add(Math.Round(d, div == 0 ? 0 : (int)Math.Log10(div)));
} else if (c.IsOperator())
root.Add(c);
else if (c.IsWhitespace())
continue;
else if (c == '(') {
int startIdx = i;
int level = 1;
while (i != buffer.Length) {
c = buffer[i++];
if (c == ')') level--;
if (c == '(') level++;
if (level == 0) break;
}
if (level != 0)
throw new Exception();
root.Add(Tokenize(buffer.Slice(startIdx, i - startIdx - 1)));
}
}
return root;
}
private static bool IsDigit(this char c) => c >= '0' && c <= '9';
private static bool IsOperator(this char c) => c == '/' || c == '*' || c == '-' || c == '+';
private static bool IsWhitespace(this char c) => c == ' ' || c == '\t' || c == '\r' || c == '\n';
private static int ParseInt(this char c) => c - '0';
}
internal static class Program
{
private static void Main(string[] args)
{
string toParse = "27+(0.5*(4*2.5))*3";
Console.WriteLine($"Parsing: '{toParse}'\n");
Console.WriteLine(TokenToString(Evaluator.Tokenize(toParse)));
Console.WriteLine("Evaluated: " + Evaluator.Evaluate(toParse));
Console.ReadLine();
// quick method to print out tokens
string TokenToString(IEnumerable<object> tlist, int tab = 0) => string.Join("\n", tlist.Select(x => x is List<object> tl ? TokenToString(tl, tab+1) : $"{new string('\t', tab)}'{x}'"));
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment