Click here to Skip to main content
15,886,724 members
Articles / Programming Languages / C#

A Calculation Engine for .NET

Rate me:
Please Sign up or sign in to vote.
4.92/5 (183 votes)
1 Sep 2013Public Domain15 min read 651.1K   11.4K   421  
A calculation engine that is small, fast, and extensible.
''' <summary>
''' Base class that represents parsed expressions.
''' </summary>
''' <remarks>
''' For example:
''' <code>
''' Expression expr = scriptEngine.Parse(strExpression);
''' object val = expr.Evaluate();
''' </code>
''' </remarks>
Public Class Expression
    Implements IComparable(Of Expression)

    ' Fields
    Private Shared _ci As System.Globalization.CultureInfo = System.Globalization.CultureInfo.InvariantCulture
    Friend _token As Token

    ' Methods
    Friend Sub New()
        _token = New Token(Nothing, TKID.ATOM, TKTYPE.IDENTIFIER)
    End Sub

    Friend Sub New(ByVal tk As Token)
        _token = tk
    End Sub

    Friend Sub New(ByVal value As Object)
        _token = New Token(value, TKID.ATOM, TKTYPE.LITERAL)
    End Sub

    Public Function CompareTo(ByVal other As Expression) As Integer
        Dim c1 As IComparable = TryCast(Evaluate, IComparable)
        Dim c2 As IComparable = TryCast(other.Evaluate, IComparable)
        If ((c1 Is Nothing) AndAlso (c2 Is Nothing)) Then
            Return 0
        End If
        If (c2 Is Nothing) Then
            Return -1
        End If
        If (c1 Is Nothing) Then
            Return 1
        End If
        Return c1.CompareTo(c2)
    End Function

    Public Overridable Function Evaluate() As Object
        If (_token.Type <> TKTYPE.LITERAL) Then
            Throw New ArgumentException("Bad expression.")
        End If
        Return _token.Value
    End Function

    Public Shared Widening Operator CType(ByVal x As Expression) As Boolean
        Dim v As Object = x.Evaluate
        If TypeOf v Is Boolean Then
            Return CBool(v)
        End If
        If (v Is Nothing) Then
            Return False
        End If
        If TypeOf v Is Double Then
            Return (CDbl(v) <> 0)
        End If
        Return (CDbl(x) <> 0)
    End Operator

    Public Shared Widening Operator CType(ByVal x As Expression) As DateTime
        Dim v As Object = x.Evaluate
        If TypeOf v Is DateTime Then
            Return CDate(v)
        End If
        If TypeOf v Is Double Then
            Return DateTime.FromOADate(CDbl(x))
        End If
        Return CDate(Convert.ChangeType(v, GetType(DateTime), Expression._ci))
    End Operator

    Public Shared Widening Operator CType(ByVal x As Expression) As Double
        Dim v As Object = x.Evaluate
        If TypeOf v Is Double Then
            Return CDbl(v)
        End If
        If TypeOf v Is Boolean Then
            Return IIf(CBool(v), CDbl(1), CDbl(0))
        End If
        If TypeOf v Is DateTime Then
            Dim dt As DateTime = CDate(v)
            Return dt.ToOADate
        End If
        If (v Is Nothing) Then
            Return 0
        End If
        Return CDbl(Convert.ChangeType(v, GetType(Double), Expression._ci))
    End Operator

    Public Shared Widening Operator CType(ByVal x As Expression) As Integer
        Dim dbl As Double = CType(x, Double)
        Return CInt(dbl)
    End Operator

    Public Shared Widening Operator CType(ByVal x As Expression) As String
        Dim v As Object = x.Evaluate
        Return IIf((v Is Nothing), String.Empty, v.ToString)
    End Operator

    Public Overridable Function Optimize() As Expression
        Return Me
    End Function

    Public Function CompareTo1(ByVal other As Expression) As Integer Implements System.IComparable(Of Expression).CompareTo

    End Function
End Class

''' <summary>
''' Unary expression, e.g. +123
''' </summary>
Friend Class UnaryExpression
    Inherits Expression

    ' Fields
    Private _expr As Expression

    ' Methods
    Public Sub New(ByVal tk As Token, ByVal expr As Expression)
        MyBase.New(tk)
        _expr = expr
    End Sub

    Public Overrides Function Evaluate() As Object
        Select Case MyBase._token.ID
            Case TKID.ADD
                Return CDbl(_expr)
            Case TKID.SUB
                Return -CDbl(_expr)
        End Select
        Throw New ArgumentException("Bad expression.")
    End Function

    Public Overrides Function Optimize() As Expression
        _expr = _expr.Optimize
        Return IIf((_expr._token.Type = TKTYPE.LITERAL), New Expression(Evaluate), Me)
    End Function
End Class

''' <summary>
''' Binary expression, e.g. 1+2
''' </summary>
Friend Class BinaryExpression
    Inherits Expression

    ' Fields
    Private _lft As Expression
    Private _rgt As Expression

    ' Methods
    Public Sub New(ByVal tk As Token, ByVal exprLeft As Expression, ByVal exprRight As Expression)
        MyBase.New(tk)
        _lft = exprLeft
        _rgt = exprRight
    End Sub

    Public Overrides Function Evaluate() As Object
        If (MyBase._token.Type = TKTYPE.COMPARE) Then
            Dim cmp As Integer = _lft.CompareTo(_rgt)
            Select Case MyBase._token.ID
                Case TKID.GT
                    Return (cmp > 0)
                Case TKID.LT
                    Return (cmp < 0)
                Case TKID.GE
                    Return (cmp >= 0)
                Case TKID.LE
                    Return (cmp <= 0)
                Case TKID.EQ
                    Return (cmp = 0)
                Case TKID.NE
                    Return (cmp <> 0)
            End Select
        End If
        Select Case MyBase._token.ID
            Case TKID.ADD
                Return (CDbl(_lft) + CDbl(_rgt))
            Case TKID.SUB
                Return (CDbl(_lft) - CDbl(_rgt))
            Case TKID.MUL
                Return (CDbl(_lft) * CDbl(_rgt))
            Case TKID.DIV
                Return (CDbl(_lft) / CDbl(_rgt))
            Case TKID.DIVINT
                Return CDbl(CInt((CDbl(_lft) / CDbl(_rgt))))
            Case TKID.MOD
                Return CDbl(CInt((CDbl(_lft) Mod CDbl(_rgt))))
            Case TKID.POWER
                Dim a As Double = CDbl(_lft)
                Dim b As Double = CDbl(_rgt)
                If (b <> 0) Then
                    Select Case b
                        Case 0.5
                            Return Math.Sqrt(a)
                        Case 1
                            Return a
                        Case 2
                            Return (a * a)
                        Case 3
                            Return ((a * a) * a)
                        Case 4
                            Return (((a * a) * a) * a)
                    End Select
                    Return Math.Pow(CDbl(_lft), CDbl(_rgt))
                End If
                Return 1
        End Select
        Throw New ArgumentException("Bad expression.")
    End Function

    Public Overrides Function Optimize() As Expression
        _lft = _lft.Optimize
        _rgt = _rgt.Optimize
        Return IIf(((_lft._token.Type = TKTYPE.LITERAL) AndAlso (_rgt._token.Type = TKTYPE.LITERAL)), New Expression(Evaluate), Me)
    End Function
End Class

''' <summary>
''' Function call expression, e.g. sin(0.5)
''' </summary>
Friend Class FunctionExpression
    Inherits Expression

    ' Fields
    Private _fn As FunctionDefinition
    Private _parms As List(Of Expression)

    ' Methods
    Friend Sub New()
    End Sub

    Public Sub New(ByVal [function] As FunctionDefinition, ByVal parms As List(Of Expression))
        _fn = [function]
        _parms = parms
    End Sub

    Public Overrides Function Evaluate() As Object
        Return _fn.Function.Invoke(_parms)
    End Function

    Public Overrides Function Optimize() As Expression
        Dim allLits As Boolean = True
        If (Not _parms Is Nothing) Then
            Dim i As Integer
            For i = 0 To _parms.Count - 1
                Dim p As Expression = _parms.Item(i).Optimize
                _parms.Item(i) = p
                If (p._token.Type <> TKTYPE.LITERAL) Then
                    allLits = False
                End If
            Next i
        End If
        Return IIf(allLits, New Expression(Evaluate), Me)
    End Function
End Class

''' <summary>
''' Simple variable reference.
''' </summary>
Friend Class VariableExpression
    Inherits Expression

    ' Fields
    Private _dct As Dictionary(Of String, Object)
    Private _name As String

    ' Methods
    Public Sub New(ByVal dct As Dictionary(Of String, Object), ByVal name As String)
        _dct = dct
        _name = name
    End Sub

    Public Overrides Function Evaluate() As Object
        Return _dct.Item(_name)
    End Function

End Class

''' <summary>
''' Expression based on an object's properties.
''' </summary>
Friend Class BindingExpression
    Inherits Expression

    ' Fields
    Private _bindingPath As List(Of BindingInfo)
    Private _ci As System.Globalization.CultureInfo
    Private _ce As CalcEngine

    ' Methods
    Friend Sub New(ByVal engine As CalcEngine, ByVal bindingPath As List(Of BindingInfo), ByVal ci As System.Globalization.CultureInfo)
        _ce = engine
        _bindingPath = bindingPath
        _ci = ci
    End Sub

    Public Overrides Function Evaluate() As Object
        Return GetValue(_ce.DataContext)
    End Function

    Private Function GetValue(ByVal obj As Object) As Object
        Dim bf As System.Reflection.BindingFlags = System.Reflection.BindingFlags.Public Or System.Reflection.BindingFlags.Static Or System.Reflection.BindingFlags.Instance Or System.Reflection.BindingFlags.IgnoreCase
        Dim bi As BindingInfo
        For Each bi In _bindingPath
            If (bi.PropertyInfo Is Nothing) Then
                bi.PropertyInfo = obj.GetType.GetProperty(bi.Name, bf)
            End If
            obj = bi.PropertyInfo.GetValue(obj, Nothing)
            If ((Not bi.Parms Is Nothing) AndAlso (bi.Parms.Count > 0)) Then
                If (bi.PropertyInfoItem Is Nothing) Then
                    bi.PropertyInfoItem = obj.GetType.GetProperty("Item", bf)
                End If
                Dim pip As System.Reflection.ParameterInfo() = bi.PropertyInfoItem.GetIndexParameters
                Dim list As New List(Of Object)
                Dim i As Integer
                For i = 0 To pip.Length - 1
                    Dim pv As Object = Convert.ChangeType(bi.Parms.Item(i).Evaluate, pip(i).ParameterType, _ci)
                    list.Add(pv)
                Next i
                obj = bi.PropertyInfoItem.GetValue(obj, list.ToArray)
            End If
        Next
        Return obj
    End Function
End Class

''' <summary>
''' Helper used for building BindingExpression objects.
''' </summary>
Friend Class BindingInfo

    ' Fields
    Dim _name As String
    Dim _parms As List(Of Expression)
    Dim _pi, _piItem As System.Reflection.PropertyInfo

    ' Methods
    Public Sub New(ByVal member As String, ByVal parms As List(Of Expression))
        Name = member
        Parms = parms
    End Sub


    ' Properties
    Public Property Name As String
        Get
            Return _name
        End Get
        Set(ByVal value As String)
            _name = value
        End Set
    End Property
    Public Property Parms As List(Of Expression)
        Get
            Return _parms
        End Get
        Set(ByVal value As List(Of Expression))
            _parms = value
        End Set
    End Property
    Public Property PropertyInfo As System.Reflection.PropertyInfo
        Get
            Return _pi
        End Get
        Set(ByVal value As System.Reflection.PropertyInfo)
            _pi = value
        End Set
    End Property
    Public Property PropertyInfoItem As System.Reflection.PropertyInfo
        Get
            Return _piItem
        End Get
        Set(ByVal value As System.Reflection.PropertyInfo)
            _piItem = value
        End Set
    End Property

End Class

''' <summary>
''' Expression that represents an external object.
''' </summary>
Friend Class XObjectExpression
    Inherits Expression
    Implements IEnumerable

    ' Fields
    Private _value As Object

    ' Methods
    Friend Sub New(ByVal value As Object)
        _value = value
    End Sub

    Public Overrides Function Evaluate() As Object
        Dim iv As IValueObject = TryCast(_value, IValueObject)
        If (Not iv Is Nothing) Then
            Return iv.GetValue
        End If
        Return _value
    End Function

    Public Function GetEnumerator() As IEnumerator Implements IEnumerable.GetEnumerator
        Dim ie As IEnumerable = TryCast(_value, IEnumerable)
        Return IIf((Not ie Is Nothing), ie.GetEnumerator, Nothing)
    End Function
End Class


''' <summary>
''' Interface supported by external objects that have to return a value
''' other than themselves (e.g. a cell range object should return the 
''' cell content instead of the range itself).
''' </summary>
Public Interface IValueObject
    ' Methods
    Function GetValue() As Object
End Interface

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under A Public Domain dedication


Written By
Software Developer
Brazil Brazil
Software Architect/Developer with several years experience creating and delivering software.

Full-stack Web development (including React, Firebase, TypeScript, HTML, CSS), Entity Framework, C#, MS SQL Server.

Passionate about new technologies and always keen to learn new things as well as improve on existing skills.

Comments and Discussions