Click here to Skip to main content
Click here to Skip to main content

An expression evaluator written in VB.NET

By , 14 Oct 2012
 



There is a new version (still very simple), at the address below: 

http://www.codeproject.com/Articles/13779/The-expression-evaluator-revisited-Eval-function-i 



Introduction

My company needed a small expression evaluator to use in our .NET application. Using the .NET framework compilation capabilities seem to be the most obvious way to make an evaluator. However, in practice this technique has a nasty side effect, it looks like it creates a new DLL in memory each time you evaluate your function and it seems nearly impossible to unload the DLL. You can refer to remarks at the end of the article Evaluating Mathematical Expressions by Compiling C# Code at Runtime for more details.

This evaluator is neither using CodeDOM nor trying to compile VB source. On the contrary, it parses your expression and evaluates its value.

Compared to other projects that I have seen, this evaluator can do the following:

  • access and process string expressions.

    You can evaluate "Hello" + " " + "world"

  • access and process objects.

    You can evaluate ThisForm.Left.

  • it also offers easy extensibility.

You can add any number of custom functions without having to change the evaluator code.

Using the code

The evaluator can be run with just two lines of code:

Dim mEvaluator As New Evaluator 
Dim res As integer = CInt(mEvaluator.Eval("1+1"))

How to provide variables for the evaluator

The evaluator raises an event GetVariable when a keyword is not detected. There is no need for you to publish all the variables and then run the eval. On the contrary, you can provide an on demand function which provides only the needed variables:

 Private Sub Evaluator1_GetVariable(ByVal name As String, _
            ByRef value As Object) Handles Evaluator1.GetVariable
    Select Case name
      Case "anumber"
        value = 5.0
      Case "theForm"
        value = Me
      Case "adate"
        value = #1/1/2005#
    End Select
End Sub

How to extend the evaluator with custom functions

The member functions found in the class EvalFunctions are automatically used by the evaluator. In this example, you can see how we can make the evaluator implement the sin and now functions:

Public Class EvalFunctions 
   Function sin(ByVal v As Double) As Double
     Return Math.Sin(v)
   End Function

   Function now() As DateTime
     Return Microsoft.VisualBasic.Now
   End Function

As you can see you don't need much wrapping, the function can be written and used straightaway in this class. Note however that the evaluator does not make any distinction between the Integers and Doubles. Therefore, remember to use Doubles and not Integers for your function parameters.

How does this work?

The evaluator is made of a classic Tokenizer followed by a classic Parser. I wrote both of them in VB, without using any Lex or Bisons tools. The aim was readability over speed. Tokenizing, parsing and execution is done in one pass. This is elegant and at the same time quite efficient because the evaluator never looks ahead or back, more than one character.

The tokenizer

It reads the characters one by one and changes its state according to the characters it encounters. When it recognizes one of the recognized Token types, it returns it to the parser. If it does not recognize a character, it will raise a syntax error exception.

' Recognized token types :
Private Enum eTokenType
 none                   ' temporary state
 end_of_formula         ' when the tokenizer reach the end
 operator_plus          ' +
 operator_minus         ' -
 operator_mul           ' *
 operator_div           ' /
 operator_percent       ' %
 open_parenthesis       ' (
 comma                  ' ,
 dot                    ' .
 close_parenthesis      ' )
 operator_ne            ' <>
 operator_gt            ' <=
 operator_ge            ' >=
 operator_eq            ' =
 operator_le            ' <=
 operator_lt            ' <
 operator_and           ' AND
 operator_or            ' OR
 operator_not           ' NOT
 operator_concat        ' & 
 value_identifier       ' any word starting with a letter or _ 
 value_true             ' TRUE 
 value_false            ' FALSE 
 value_number           ' any number starting 0-9 or . 
 value_string           ' any string starting ' or " 
 open_bracket           ' [ 
 close_bracket          ' ] 
End Enum

The Tokenizer is fairly simple, it accepts a loose VB/Excel syntax. The evaluator is split into two classes, one does the tokenization and the second processes the tokens. This is the standard way of doing it. This is quite flexible also. This way, if you wish you could amend it to accept a C++ syntax by changing the way the parser detects the operators eq, ne, and, or, not... Changing the Tokenizer will not force you to reprogram the rest of the evaluator.

The Parser

The Parser is a bit more complicated than a Tokenizer. It is like the Tokenizer with a sort of flow machine, a bit like a pipe. It will process the token one by one without looking ahead or back.

In this article, I speak about operators, left parts and right parts. In the expression 1 + 2, I call + the operator, 1 is the left part and 2 is the right part.

One of the complicated concepts of the Parser is priorities. For example, the expression:

1 + 2 * 3

is not treated the same way as the expression:

1 * 2 + 3

The evaluator operates using a standard set of priorities. The multiplication has more priority than addition. Therefore:

1 + 2 * 3 = 1 + 6 = 7
1 * 2 + 3 = 2 + 3 = 5

In the above cases, we need to do the multiplication first.

So how can this be done in one pass?

At any time, the parser knows what is its level of priority.

Private Enum ePriority
  none = 0
  [concat] = 1
  [or] = 2
  [and] = 3
  [not] = 4
  equality = 5
  plusminus = 6
  muldiv = 7
  percent = 8
  unaryminus = 9
End Enum

When the parser encounters an operator, it will recursively call the parser to get the right part. When the parser returns the right part, the operator can apply its operation (for example +) and the parsing continues.

The interesting part is that while calculating the right part, the Tokenizer already knows its current level of priority. Therefore, while parsing the right part, if it detects an operator with more priority, it will continue its parsing and return only the resulting value.

You said it supports object?

Yes, the evaluator supports the . operator. If you enter the expression theForm.text then the evaluator will return the title of the form. If you enter the expression theForm.left, it will return its runtime left position. This feature is only experimental and has not been tested yet. That is why I have put this code here, hoping others will find its features valuable and submit their improvements.

How does this work?

In fact the object came free. I used System.Reflection to evaluate the custom functions. And the same code is used to access the object's methods and properties. When the parser encounters an identifier that is a keyword without any meaning for it, it will try to reflect the CurrentObject to see if it can find a method or a property with the same name.

 mi = CurrentObject.GetType().GetMethod(func, _
  _Reflection.BindingFlags.IgnoreCase _ 
  Or Reflection.BindingFlags.Public _ Or Reflection.BindingFlags.Instance)

If a method or a property is found, it will feed its parameters.

valueleft = mi.Invoke(CurrentObject, _
  _ System.Reflection.BindingFlags.Default, Nothing, 
  _ DirectCast(parameters.ToArray(GetType(Object)), Object()), Nothing)

Points of Interest

This is the only formula evaluator available on CodeProject with a separate Tokenizer and Parser (I believe). The extensibility can be pushed to the maximum due to the use of System.Reflection.

History

  • 7th Feb 2005
    • First release.
  • 10th Feb 2005
    • Greatly increased the length and detail of this article.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Pascal Ganaye
Software Developer (Senior)
United Kingdom United Kingdom
Member
I am a French programmer.
These days I spend most of my time with the .NET framework, JavaScript and html.

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralMy vote of 5memberSSementsov23 Oct '12 - 1:23 
Excellent!!!
GeneralMy vote of 5memberopisana27 Sep '12 - 2:56 
Excelente! Genial!
GeneralMy vote of 5memberhareshPrj26 Jul '12 - 21:52 
thanx.....
GeneralShort crrcuiting iifmemberoscargs12 Jun '11 - 9:46 
Hello, this project is great!
I need to add a short-circuiting IIF() (similar to IF() in vb 2010) that is, if condition is false, the second argument is not even evaluated, if condition is true, the third argument is not evaluated.
Obviously I cannot use the ifn(), ifd() functions because being functions, all arguments are evaluated before calling the function. I am intervening InternalGetVariable just checking for func="IIF", but I cannot figure how to "skip" the expression that must not be evaluated.
Thanks for any help.
GeneralRe: Short crrcuiting iifmemberPascal Ganaye12 Jun '11 - 10:54 
This version of the evaluator evaluates results as it goes along parsing the string.
It might be do-able to have a variable that let the parsing but blocks the evaluation.
It would be quite hard though.
Fortunately, there is a version of
The expression evaluator revisited (Eval function in 100% managed .NET)[^]
In this version the evaluator build the full expression tree upfront and then only evaluate the result.
In that version it is a lot easier to implement a functional if that evaluates only one side.
GeneralRe: Short crrcuiting iifmemberfastal20 Jul '11 - 10:24 
I thought I'd made this mod!
 
Alas, When I looked back to post it, I had implmemented the short-circuiting AndAlso and OrElse, not for IIF. Sorry.
 
Basically involved duping all refs to And/Or with AndAlso/OrElse, including e.g., just using them to calc the result, VB's functionality does the short circuiting; sample from opcode:
 
    Private Function BOOL_ANDALSO_BOOL() As Object
        Return DirectCast(mParam1.value, Boolean) AndAlso DirectCast(mParam2.value, Boolean)
    End Function
 
This is to the 2007 version.
 
Thanks Pascal, great code!
GeneralMy vote of 5memberJymmy09710 Dec '10 - 7:15 
Grande codice!
Programmo da un anno e lo cerco dallo stesso momento in cui ho cominciato.
Ne avevo azzardato uno ma il codice era molto lungo e complesso.
Grazie!!!
GeneralVery nice, but 1 questionmemberPieterSO24 Nov '10 - 1:19 
I want to use it with dates.
When I do "now + 0.006944" I get now 10 minutes later
but when i do "now - 0.006944", I get an error
=> Error :Conversion from type 'Double' to type 'Date' is not valid.
 
Does someone has an idee?
GeneralGreat Codememberwldrumstcs17 Aug '10 - 11:03 
To reiterate what everyone else has said, thank you very much for the code! You saved me countless hours of implementing this functionality. It's always nice when someone else's code is so easily assimilated into your own.
QuestionFound an issue. .1+.1 fails.memberWantToLearn.NET15 Jan '10 - 2:29 
I was trying this code and I found that it fails if I try to evaluate .1+.1, but if I enter 0.1+0.1 it works properly. I am a beginner and I don't know how to fix this myself. Can someone help?

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web04 | 2.6.130523.1 | Last Updated 14 Oct 2012
Article Copyright 2005 by Pascal Ganaye
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid