4
\$\begingroup\$

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.");
\$\endgroup\$
3
  • 6
    \$\begingroup\$ Could you give us a sample of a JSON that you can apply this parser to it ? I'm trying to figure out what drives you to take this approach, as parsing an open string expression would be hard to maintain, but if we have the full picture, we could give you a better answer. \$\endgroup\$
    – iSR5
    Oct 18, 2020 at 11:16
  • \$\begingroup\$ Why do you want to use expression instead of Linq expression because even in your example can be done in Linq ? what do you actually need and expect from your code ? What your ExpressionParser covers that LINQ doesn't ? \$\endgroup\$
    – iSR5
    Oct 18, 2020 at 11:24
  • \$\begingroup\$ @iSR5 By providing the following JSON to the parser {"age": 19, "year": 2020}; you could evaluate the following expression: "year - age", which would give you the year I was born. The idea is to allow users to insert math expressions in text or word files that can be replaced dinamically depending on the provided JSON. \$\endgroup\$ Oct 19, 2020 at 0:49

2 Answers 2

3
\$\begingroup\$
  • ExpressionParser is too general, you might need to renamed it to be specific such as MathExpressionParser.

  • ExpressionParser should be abstract class along with Variables property.

  • Computer in ExpressionParser 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.

\$\endgroup\$
5
\$\begingroup\$

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:

enter image description here

\$\endgroup\$
1
  • 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\$ Oct 19, 2020 at 19:17

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.