I needed a simple parser that could do both logical and math operations expressed in a string, as well as being able to use variables stored in json.
None of what I found online seemed to do all of the above, so I came up with my own. Below is the source code for my ExpressionParser
as well as my JExpressionParser
, in case your variables are stored in a JSON.
Hopefully this post will help someone, as well as allow others to improve my code.
Here is the base parser:
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text;
public class ExpressionParser
{
public virtual IDictionary<string, object> Variables { get; }
protected ExpressionParser() { }
protected virtual DataTable GetComputer()
{
var computer = new DataTable();
if (Variables != null)
{
computer.Columns.AddRange(Variables.Select(v => new DataColumn(v.Key) { DataType = v.Value.GetType() }).ToArray());
computer.Rows.Add(Variables.Values.ToArray());
}
return computer;
}
public ExpressionParser(IDictionary<string, object> variables = null)
{
Variables = variables ?? new Dictionary<string, object>();
}
public object Compute(string expression)
{
StringBuilder sb = new StringBuilder(expression);
foreach (var key in Variables.Keys)
sb.Replace(key, $"Sum({key})");
sb.Replace("==", "=");
using (var computer = GetComputer())
return computer.Compute(sb.ToString(), null);
}
}
And here is one that works with json objects:
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
public class JExpressionParser : ExpressionParser
{
public JObject JVariables { get; set; }
public override IDictionary<string, object> Variables =>
JVariables.Properties().ToDictionary(p => p.Name, p => p.Value.ToObject(conversions[p.Value.Type]));
static readonly Dictionary<JTokenType, Type> conversions = new Dictionary<JTokenType, Type>()
{
[JTokenType.Integer] = typeof(int),
[JTokenType.Float] = typeof(float),
[JTokenType.Boolean] = typeof(bool),
};
public JExpressionParser(JObject jVariables = null)
{
JVariables = jVariables ?? new JObject();
}
}
Usage:
var variables = new Dictionary<string, object>() { ["budget"] = 1000, ["cost"] = 965 };
var parser = new ExpressionParser(variables);
Console.WriteLine("We have $" + parser.Compute("budget - cost") + " of budget left.");
2 Answers 2
ExpressionParser
is too general, you might need to renamed it to be specific such asMathExpressionParser
.ExpressionParser
should beabstract
class along withVariables
property.Computer
inExpressionParser
can be declared globally, and used across the class.in the
Compute
method, you're not validating the string, you should add some validations such as null, empty, whitespace, and any related possible validations.it's not a good practice to default constructor arguments, instead you can do this :
public ExpressionParser() : this(null) { } public ExpressionParser(IDictionary<string, object> variables) { Variables = variables ?? new Dictionary<string, object>(); }
As I mentioned in the comments, maintaining an open-string expression it's not an easy work, so you will need to add some restrictions to it.
For now, you're depending on DataColumn
expression restrictions, if you have any plan to expand this to cover than what DataColumn
covers, then I would suggest using another approach such as Roslyn
script engine.
Basically, you can use this engine to convert a raw string to an instance of Func
lambda dynamically. with that, it'll add more fixability to your work, such as object-model, Linq ..etc. Adding more open-possibilities to your expression features, with possibility to be adopt any sort of structured data. (for instance, you can integrated with Json.NET
, XDocument
, Razor
..etc. Which would be useful in different areas.
I can see three small improvement areas:
Use ctor instead of object initializer
Instead of this:
new DataColumn(v.Key) { DataType = v.Value.GetType() }
Use this:
new DataColumn(v.Key, v.Value.GetType())
Object initializer will run after ctor. If ctor accepts that parameter then prefer to provide it in the ctor.
Use early exist instead of guard expression
Instead of this:
if (Variables != null)
{
computer.Columns.AddRange(Variables.Select(v => new DataColumn(v.Key, v.Value.GetType())).ToArray());
computer.Rows.Add(Variables.Values.ToArray());
}
return computer;
Use this:
if ((Variables?.Any() ?? true)) return computer;
computer.Columns.AddRange(Variables.Select(v => new DataColumn(v.Key, v.Value.GetType())).ToArray());
computer.Rows.Add(Variables.Values.ToArray());
return computer;
Yes, I know it's against the single return statement. But with this approach you streamline your logic.
Use immutable collection instead of just readonly
access modifier
Instead of this:
static readonly Dictionary<JTokenType, Type> conversions = new Dictionary<JTokenType, Type>()
{
[JTokenType.Integer] = typeof(int),
[JTokenType.Float] = typeof(float),
[JTokenType.Boolean] = typeof(bool),
};
Use this:
static readonly ImmutableDictionary<JTokenType, Type> conversions = new Dictionary<JTokenType, Type>()
{
[JTokenType.Integer] = typeof(int),
[JTokenType.Float] = typeof(float),
[JTokenType.Boolean] = typeof(bool),
}.ToImmutableDictionary();
With readonly
you only prevent to replace the whole object. But you can still add and remove new items to the dictionary:
//Invalid operations
conversions = null;
conversions = new Dictionary<string, JTokenType> {};
//Valid operations
conversions.Add(JTokenType.Array, typeof(Array));
conversions.Remove(JTokenType.Integer);
With ImmutableDictionary
the Add
and Remove
will return a new ImmutableDictionary
so it will not have any effect on the original collection. Visual Studio will even worn you about it:
-
3\$\begingroup\$ That's great! I didn't know ImmutableDictionary existed. It shouldn't matter too much since it's private but it's a good practice, and should be useful for other projects. \$\endgroup\$Samuel Cabrera– Samuel Cabrera2020年10月19日 19:17:39 +00:00Commented Oct 19, 2020 at 19:17
Linq expression
because even in your example can be done inLinq
? what do you actually need and expect from your code ? What yourExpressionParser
covers thatLINQ
doesn't ? \$\endgroup\$