65.9K
CodeProject 正在变化。 阅读更多。
Home

Visual Basic 中的复杂数学表达式求值模块

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.57/5 (7投票s)

2013年9月1日

CPOL

13分钟阅读

viewsIcon

68726

downloadIcon

927

面向对象的数学表达式求值器

引言

正如我的老师所说,编写一个表达式求值模块是程序员的基本技能,确实如此。当你想编写一个求值数学表达式的模块时,你应该很好地掌握一些数据结构、算法以及你所使用语言的高级特性。最近,我在 Visual Basic 中重构了 PLAS 程序的生物化学系统分析核心模块,该程序由 **Antonio E.N.Fereira** 编写(我将在下一篇 CodeProject 文章中分享这项工作),在这个分析模块中,我需要一个模块来求值一组方程,于是我编写了一个数学求值模块。在这里,我将分享我关于这个数学求值模块的工作。

使用代码

数学表达式可能包含函数、常量、特定运算符,以及一个非常重要的事情:括号表达式!这是一个复杂的例子

Max(Log((((sin(tan(20)^5+50)*40)!-(99*Rnd(4,20)))%((56+8*cos(PI))^2)^-2.3)!^4), log(e))^3 

我上面展示的表达式是我认为在我重构中会遇到的最复杂的表达式。这个表达式符合 Visual Basic 语言中数学表达式的语法(除了 % 和 ! 运算符)。在我看来,数学表达式可以分为两种类型:一种是只包含数字和运算符的简单表达式,另一种是包含函数、运算符和括号对的复杂表达式。而复杂表达式则由多个简单表达式和函数组成。

SimpleExpression

1. 基本算术计算

这是简单表达式中的算术运算符

+(Plus), -(Subtraction), *(Multiplication), /(Division), \(Exact Division), ^(Power), %(Mod), !(Factorial)

我使用委托(或 Lambda 表达式)数组来执行这些操作

''' <summary>
''' +-*/\%^! 
''' </summary>
''' <remarks></remarks>
Public Shared ReadOnly Arithmetic As System.Func(Of Double, Double, Double)() = {
   Function(a As Double, b As Double) a + b,
   Function(a As Double, b As Double) a - b,
   Function(a As Double, b As Double) a * b,
   Function(a As Double, b As Double) a / b,
   Function(a As Double, b As Double) a \ b,
   Function(a As Double, b As Double) a Mod b,
   Function(a As Double, b As Double) System.Math.Pow(a, b),
   AddressOf Factorial}  

由于 Visual Basic 没有阶乘计算的运算符,我必须编写一个函数来执行数字的阶乘计算。这是阶乘计算函数

''' <summary>
''' Calculate the factorial value of a number, as this function is the part of the arithmetic operation 
''' delegate type of 'System.Func(Of Double, Double, Double)', so it must keep the form of two double 
''' parameter, well, the parameter 'b As Double' is useless.
''' (计算某一个数的阶乘值,由于这个函数是四则运算操作委托'System.Func(Of Double, Double, Double)'中的一部分,
''' 故而本函数保持着两个双精度浮点型数的函数参数的输入形式,也就是说本函数的第二个参数'b'是没有任何用途的)  
''' </summary>
''' <param name="a">The number that will be calculated(将要被计算的数字)</param>
''' <param name="b">Useless parameter 'b'(无用的参数'b')</param>
''' <returns>
''' Return the factorial value of the number 'a', if 'a' is a negative number then this function 
''' return value 1.
''' (函数返回参数'a'的阶乘计算值,假若'a'是一个负数的话,则会返回1)
''' </returns>
''' <remarks></remarks>
Public Shared Function Factorial(a As Double, b As Double) As Double
    If a <= 0 Then
        Return 1
    Else
        Dim n As Integer = a
        For i As Integer = n - 1 To 1 Step -1
            n *= i
        Next
        Return n
    End If
End Function  

由于我不知道如何计算负数的阶乘,我将此函数中负数的返回值设为“1”。

正如你可以在 **Delegate** 数组定义中所看到的:Function(....) 等同于 ** AddressOf** 关键字的声明形式。事实上,Visual Basic 中的委托 Lambda 可以通过以下三种方式声明

(a) simplest only in one line: 
    Function(<paramenter list>) <only one line statement> 
(b) much complicated way in sevral lines with no function name: 
    Function(<paramenter list>) As <Result type>
       <Statements>
    End Function
(c) declare as a normal function but should use AddressOf keyword to get this 
    function(AddressOf is just like a operator to get the pointer of a function)
    AddressOf <function name>  

最重要的是,函数必须与你声明的委托具有相同的签名(相同的签名意味着相同的参数和相同的返回类型,即使名称不同)。

我已经将这些基本算术运算定义为一个辅助类,你可以在我上传的项目中 `Helpers` 命名空间下找到这个辅助类。这个辅助类有一个使用这些算术运算符的接口

''' <summary>
''' Do a basically arithmetic calculation.
''' (进行一次简单的四则运算) 
''' </summary>
''' <param name="a"></param>
''' <param name="b"></param>
''' <param name="o">Arithmetic operator(运算符)</param>
''' <returns></returns>
''' <remarks></remarks>
Public Shared Function Evaluate(a As Double, b As Double, o As Char) As Double
    Dim idx As Integer = InStr(OPERATORS, o) - 1
    Return Arithmetic(idx)(a, b)
End Function 

所以,在这个接口函数中,运算符委托是通过我们从表达式中解析出的运算符字符,使用一个常量字符串进行索引的

''' <summary>
''' A string constant that enumerate all of the arithmetic operators.
''' (一个枚举所有的基本运算符的字符串常数) 
''' </summary>
''' <remarks></remarks>
Public Const OPERATORS As String = "+-*/\%^!"  

2. 元表达式

正如你所看到的,简单表达式只包含数字和运算符,所以在我看来,简单表达式可以细分为许多元表达式。这是我定义元表达式的方式:元表达式是一个表达式标记,其运算符的左侧只有一个运算符和一个数字。所以我们可以得到元表达式的数据结构为

Public Class MetaExpression
    <Xml.Serialization.XmlAttribute> Public [Operator] As Char
    <Xml.Serialization.XmlAttribute> Public LEFT As Double
    Public Overrides Function ToString() As String
        Return String.Format("{0} {1}", LEFT, [Operator])
    End Function
End Class 

然后我们可以将简单表达式视为这些元表达式的有序列表

''' <summary>
''' A class object stand for a very simple mathematic expression that have no bracket or function.
''' It only contains limited operator such as +-*/\%!^ in it.
''' (一个用于表达非常简单的数学表达式的对象,在这个所表示的简单表达式之中不能够包含有任何括号或者函数,
''' 其仅包含有有限的计算符号在其中,例如:+-*/\%^!)
''' </summary>
''' <remarks></remarks>
Public Class SimpleExpression

    ''' <summary>
    ''' A simple expression can be view as a list collection of meta expression.
    ''' (可以将一个简单表达式看作为一个元表达式的集合)
    ''' </summary>
    ''' <remarks></remarks>
    Friend MetaList As New List(Of MetaExpression)  

下面的函数是一个简单的函数,用于将这个元表达式列表还原为字符串类型的表达式

''' <summary>
''' Debugging displaying in VS IDE
''' </summary>
''' <returns></returns>
''' <remarks></remarks>
Public Overrides Function ToString() As String
    Dim s As StringBuilder = New StringBuilder(128)
    For Each e In MetaList
        s.Append(e.ToString)
    Next
    Return s.ToString
End Function   

我们如何将简单表达式解析为元表达式列表?我在简单表达式类的 `Construct` 方法中进行解析工作

''' <summary> 
''' Convert a string mathematical expression to a simple expression class object.
''' (将一个字符串形式的数学表达式转换为一个'SimpleExpression'表达式对象)  
''' </summary>
''' <param name="expression">A string arithmetic expression to be converted.(一个待转换的数学表达式)</param>
''' <returns></returns>
''' <remarks></remarks>
Public Shared Widening Operator CType(expression As String) As SimpleExpression 

使用 `CType` 构造方法可以使你的代码更接近人类语言,例如

Dim Result As SimpleExpression = "(1+1)*2"   

我也在制作一个输出转换方法

''' <summary>
''' Evaluate the specific simple expression class object.
''' (计算一个特定的简单表达式对象的值) 
''' </summary>
''' <param name="e">A simple expression that will be evaluated.(待计算的简单表达式对象)</param>
''' <returns>
''' Return the value of the specific simple expression object.
''' (返回目标简单表达式对象的值)
''' </returns>
''' <remarks></remarks>
Shared Narrowing Operator CType(e As SimpleExpression) As Double 

这个 `CType` 构造方法也会使你的代码更自然,就像

Dim Result2 As Double = Result  

那么,我是如何进行元表达式解析工作的呢?首先,我使用正则表达式解析简单表达式中出现的所有实数

'Get all of the number that appears in this expression including factoral operator.
Dim Numbers As MatchCollection = Regex.Matches(ClearOverlapOperator(expression), DOUBLE_NUMBER_REGX)   

实数是 Visual Basic 中的 `Double` 类型数字,它看起来像下面的模式

''' <summary>
''' A string constant RegularExpressions that stands a double type number.
''' (一个用于表示一个双精度类型的实数的正则表达式)
''' </summary>
''' <remarks></remarks>
Public Const DOUBLE_NUMBER_REGX As String = "-?\d+([.]\d+)?" 

使用正则表达式会使我们的工作稍慢一些,但这可以通过升级我们计算机的 CPU 来解决,实际上,这种方法是我们实验室通常的做法,呃……不管了。

我们假设每个数字的前一个和后一个标记是运算符,这样我们就可以按照以下步骤将简单表达式解析为元表达式列表

(1) Get the string value of a number from the MatchCollection
(2) Get the String length of this number and then we can get the reading position in the 
    simple expression and then we can using the string length to skip the current read number 
    and the next character in the expression will be a operator
(3) Using current number and the operator that appears next to 
    the current number then we can generate a meta expression  

这就是它的工作原理

Dim s As String = Numbers(0).Value
Dim p As Integer = Len(s) 'The current read position on the expression string
Dim o As Char = expression.Chars(p)

NewExpression.MetaList.Add(New MetaExpression With {.LEFT = Val(s), .Operator = o})
p += 1
For i As Integer = 1 To Last - 1
'Assume that the next char in each number string is the arithmetic operator character. 

  s = Numbers(i).Value
  If NewExpression.MetaList.Last.Operator = "-"c Then  'Horrible negative number controls!
      p += Len(s) - 1
      s = Mid(s, 2) 'This is not a negative number as the previous operator
      ' is a "-", we must remove the additional negative operaotr
      ' that was  matched success by the regular expression constant 'DOUBLE_NUMBER_REGX'
  Else
      If expression.Chars(p) = "+"c Then
          p += Len(s) + 1
      Else
          p += Len(s)
      End If
  End If
  o = expression.Chars(p)
  'Assume that the next char in each number string is the arithmetic operator character. 

  p += 1
  NewExpression.MetaList.Add(New MetaExpression With {.LEFT = Val(s), .Operator = o})
Next 

最后我们得到一个元表达式列表:`NewExpression`,这就是我们的简单表达式对象。

3. 计算简单表达式

`simpleExpression` 有一个计算其值的功能

''' <summary>
''' Evaluate the specific simple expression class object.
''' (计算一个特定的简单表达式对象的值) 
''' </summary>
''' <returns>
''' Return the value of the specific simple expression object.
''' (返回目标简单表达式对象的值)
''' </returns>
''' <remarks></remarks>
Public Function Evaluate() As Double 

我们编写了一个计算器函数,可以执行特定运算符的计算,这个功能使得运算符的工作变得容易,例如

Calculator("^")
Calculator("*/\%")
Calculator("+-")
Return MetaList.First.LEFT 

在 `Calculator` 函数中,它接受要执行计算的运算符列表,然后操作模块变量 `MetaList`。当完成一个运算符的处理后,它将从列表中移除一个元素,最终 `metalist` 只包含一个元素,这个元素就是计算结果。

Friend Sub Calculator(OperatorList As String)
   Dim LQuery As Generic.IEnumerable(Of MetaExpression) =
       From e As MetaExpression In MetaList
       Where InStr(OperatorList, e.Operator) > 0
       Select e 'Defines a LINQ query use for select the meta element that contains target operator.
   Dim Counts As Integer = LQuery.Count
   Dim M, NextElement As MetaExpression
   
   For index As Integer = 0 To MetaList.Count - 1
   'Scan the expression object and do the calculation at the mean time
       If Counts = 0 OrElse MetaList.Count = 1 Then
           Return
           'No more calculation could be done since there is only 
           'one number in the expression, break at this situation.
       ElseIf OperatorList.IndexOf(MetaList(index).Operator) <> -1 Then
       'We find a meta expression element that contains target operator,
       'then we do calculation on this element and the element next to it.  
           M = MetaList(index)  'Get current element and the element that next to him
           NextElement = MetaList(index + 1)
           NextElement.LEFT = Arithmetic.Evaluate(M.LEFT, NextElement.LEFT, M.Operator)
           'Do some calculation of type target operator 

           MetaList.RemoveAt(index) 'When the current element is calculated, it is no use anymore, we remove it
           index -= 1  'Keep the reading position order
           Counts -= 1  'If the target operator is position at the front side of the expression,
           ' using this flag will make the for loop exit when all of the target operator
           ' is calculated to improve the performance as no needs to scan all of the expression at this situation. 
       End If
   Next
End Sub

4. 测试

Dim sExpression As String = "1-2-3+4+5+6+7+8+9+55%6*3^2"
Dim e As Microsoft.VisualBasic.Mathematical.Types.SimpleExpression = sExpression
    
Console.WriteLine("> {0} = {1}", sExpression, e.Evaluate)  

控制台输出

> 1-2-3+4+5+6+7+8+9+55%6*3^2 = 44  

ComplexExpression

只包含基本运算符的表达式不足以满足我们的生物化学系统分析工作,因此我们必须获得一个更复杂的类来处理我们研究中出现的函数和括号对的情况。但首先我应该介绍函数计算工作

1. 函数计算引擎

在这个辅助类中,我们列出了 Visual Basic 中所有可用的数学函数。与前一节中的基本算术运算符委托一样,函数计算引擎也应该由一个委托数组组成,但由于我们需要一个名称来标识每个函数,因此我们使用 `Dictionary` 对象来存储这个数组

''' <summary>
''' The mathematics calculation delegates collection with its specific name.
''' (具有特定名称的数学计算委托方法的集合) 
''' </summary>
''' <remarks></remarks>
Public Shared ReadOnly Functions As Dictionary(Of String, System.Func(Of Double, Double, Double)) =
   New Dictionary(Of String, System.Func(Of Double, Double, Double)) From {
       {"abs", Function(a As Double, b As Double) Math.Abs(a)},
       {"acos", Function(a As Double, b As Double) Math.Acos(a)},
       {"asin", Function(a As Double, b As Double) Math.Asin(a)},
       {"atan", Function(a As Double, b As Double) Math.Atan(a)},
       {"atan2", Function(a As Double, b As Double) Math.Atan2(a, b)},
       {"bigmul", Function(a As Double, b As Double) Math.BigMul(a, b)},
       {"ceiling", Function(a As Double, b As Double) Math.Ceiling(a)},
       {"cos", Function(a As Double, b As Double) Math.Cos(a)},
       {"cosh", Function(a As Double, b As Double) Math.Cosh(a)},
       {"exp", Function(a As Double, b As Double) Math.Exp(a)},
       {"floor", Function(a As Double, b As Double) Math.Floor(a)},
       {"ieeeremainder", Function(a As Double, b As Double) Math.IEEERemainder(a, b)},
       {"log", Function(a As Double, b As Double) Math.Log(a)},
       {"log10", Function(a As Double, b As Double) Math.Log10(a)},
       {"max", Function(a As Double, b As Double) Math.Max(a, b)},
       {"min", Function(a As Double, b As Double) Math.Min(a, b)},
       {"pow", Function(a As Double, b As Double) Math.Pow(a, b)},
       {"round", Function(a As Double, b As Double) Math.Round(a)},
       {"sign", Function(a As Double, b As Double) Math.Sign(a)},
       {"sin", Function(a As Double, b As Double) Math.Sin(a)},
       {"sinh", Function(a As Double, b As Double) Math.Sinh(a)},
       {"sqrt", Function(a As Double, b As Double) Math.Sqrt(a)},
       {"tan", Function(a As Double, b As Double) Math.Tan(a)},
       {"tanh", Function(a As Double, b As Double) Math.Tanh(a)},
       {"truncate", Function(a As Double, b As Double) Math.Truncate(a)},
       {"rnd", AddressOf Microsoft.VisualBasic.Mathematical.Helpers.Function.RND},
       {"int", Function(a As Double, b As Double) CType(a, Integer)},
       {String.Empty, Function(a As Double, b As Double) a}}
       'If no function name, then return the paramenter a directly. 

2. 解析表达式

解析包含函数和括号对的表达式并不容易,但仍然可以解决,尽管这项编码工作可能会让程序员生气。

`Evaluate` 函数是这项解析工作的入口,它接受一个字符串表达式,然后将其解析为简单表达式,再计算该简单表达式

''' <summary>
''' Evaluate the a specific mathematics expression string to a double value, the functions, constants, 
''' bracket pairs can be include in this expression but the function are those were originally exists 
''' in the visualbasic. I'm sorry for this...
''' (对一个包含有函数、常数和匹配的括号的一个复杂表达式进行求值,但是对于表达式中的函数而言:仅能够使用在
''' VisualBaisc语言中存在的有限的几个数学函数。)  
''' </summary>
''' <param name="expression"></param>
''' <returns></returns>
''' <remarks></remarks>
Public Shared Function Evaluate(expression As String) As Double 

我们如何解析括号对?我们假设表达式中只允许一种括号对,因为 Visual Basic 在其数学表达式中只允许出现括号对 `()`,所以我们只需要一个堆栈来记录左括号的位置

'The position stack of left bracket character. We push the reading position 
'to this stack when we met a character '(' left bracket, pop up then
' position when we met a character ')' right bracket in the expression string. 
Dim LBStack As New Stack(Of Integer)  

我们使用一个变量 `p` 来指向我们在表达式字符串中读取的位置,当 `p` 的值等于表达式最后一个字符的位置时,表示我们已经完成了计算工作。所以我们可以在 `While` 循环中使用这个条件

'Scaning the whole expression, the loop var 'p' is the reading position on the expression string
Do While p <= Expression2.Length - 1  
    '....
Loop

程序从左到右逐个字符读取字符串。当它读取到左括号字符时,它将位置 `p` 推入堆栈,然后继续读取右括号。当它读取到右括号时,它会查找堆栈中的最后一个左括号,如果堆栈为空,则表示表达式存在语法错误,当堆栈不为空时,我们就得到一个简单表达式。最后使用 `Mid` 函数获取这个简单表达式,然后将其求值为一个数字。

这项工作看起来很简单,但事实是这项工作比我们想象的要复杂得多:首先,函数会得到一个括号对,所以有些括号对不是独立的。其次,有些函数有两个参数,所以表达式中可能存在逗号字符。

因此,我们将括号字符和逗号视为我们解析工作中的标志字符

If Expression2.Chars(p) = "("c Then
   '......
ElseIf Expression2.Chars(p) = ")"c Then
'The expression string between two paired bracket is a mostly simple expression.
   '......
ElseIf Expression2.Chars(p) = ","c Then
'Meet a parameter seperator of a function, that means we should calculate
'this parameter as a simple expression as the bracket calculation has been done before. 

End If  

如前所述:当我们读取到左括号字符时,我们将当前读取位置推入堆栈,然后读取下一个字符

LBStack.Push(item:=p + 1) 

然后可怕的事情发生了:我们必须解析表达式中的函数。由于复杂表达式可以作为函数的一个参数,这种情况使得我们的编码工作不太愉快。但在观察了表达式的模式后,我们发现表达式中存在一种模式,例如:`Function()` 或 `Function(, )`,函数参数中的表达式可能是另一个函数表达式,我们可以递归地进行评估:再次使用 `Evaluate(expression As String) As Double` 函数来获取这个参数表达式的值。

所以代码最终看起来是这样的

Do While p <= Expression2.Length - 1
'Scaning the whole expression, the loop var 'p' is the reading position on the expression string
    If Expression2.Chars(p) = "("c Then
       LBStack.Push(item:=p + 1)
    ElseIf Expression2.Chars(p) = ")"c Then
    'The expression string between two paired bracket is a mostly simple expression.
       LBLocation = LBStack.Pop
       se = Mid(Expression2.ToString, LBLocation + 1, p - LBLocation)
       r = se
       LBLocation += 1
       If LBLocation < Expression2.Length AndAlso OPERATORS.IndexOf(Expression2.Chars(LBLocation)) = -1 Then
       'The previous character not is a operator, then it maybe a function name. 
           Dim f = GetFunctionName(Expression2, LBLocation)   'Get the function name
          Call CalcFunction(f, r.Evaluate, 0, 1)  'Calculate the function with only one paramenter. 
       Else
           Expression2.Replace("(" & se & ")", r.Evaluate)
           p -= Len(se)
       End If
    ElseIf Expression2.Chars(p) = ","c Then
    'Meet a parameter seperator of a function, that means we should calculate
    'this parameter as a simple expression as the bracket calculation has been done before. 
       LBLocation = LBStack.Peek
       'We get a function paramenter 'a', it maybe a simple expression,
       'do some calculation for this parameter. 
    
       se = Mid(Expression2.ToString, LBLocation + 1, p - LBLocation)
       a = CType(se, Types.SimpleExpression)
       LBStack.Push(item:=p + 1)  'Push the position of seperator character ',' to the stack
       p += 1
       'Calculate the function parameter 'b'
       Dim LBStack2 As New Stack(Of Integer)
       Do While p <= Expression2.Length - 1   'Using a loop to get the paramenter 'b'
           If Expression2.Chars(p) = "("c Then
               LBStack2.Push(item:=p + 1)
           ElseIf Expression2.Chars(p) = ")"c Then
           'The expression string between two paired bracket is a mostly simple expression.
               If LBStack2.Count = 0 Then Exit Do Else LBStack2.Pop()
           End If
           p += 1
       Loop
       LBLocation = LBStack.Pop 'Parse the pramenter 'b'
       se = Mid(Expression2.ToString, LBLocation + 1, p - LBLocation)
       'Paramenter 'b' maybe a complex expression. 
    
       b = Evaluate(se) 'Get the value of the parameter 'b'
       'Calculate the value of the function
       LBLocation = LBStack.Pop
       Dim f = GetFunctionName(Expression2, LBLocation)   'Get the function name
       Call CalcFunction(f, a, b, 0)  'Calculate the function with two paramenters. 
    End If
    p += 1
Loop

最后,我们计算括号对和函数中的所有表达式,最后得到一个简单表达式,然后使用 `SimpleExpression` 类对象计算这个表达式,并得到该表达式的最终结果。这是这个函数的全部代码

''' <summary>
''' Evaluate the a specific mathematics expression string to a double value, the functions, constants, 
''' bracket pairs can be include in this expression but the function are those were originally exists 
''' in the visualbasic. I'm sorry for this...
''' (对一个包含有函数、常数和匹配的括号的一个复杂表达式进行求值,但是对于表达式中的函数而言:仅能够使用在
''' VisualBaisc语言中存在的有限的几个数学函数。)  
''' </summary>
''' <param name="expression"></param>
''' <returns></returns>
''' <remarks></remarks>
Public Shared Function Evaluate(expression As String) As Double
    Dim LBStack As New Stack(Of Integer)
    'The position stack of left bracket character. We push the reading position
    'to this stack when we met a character '(' left bracket, pop up then position 
    'when we met a character ')' right bracket in the expression string. 

    Dim r As Microsoft.VisualBasic.Mathematical.Types.SimpleExpression, se As String
    'se' is a simple expression string

    Dim LBLocation As Integer
    Dim Expression2 As StringBuilder = New StringBuilder(value:=expression)
    Dim a, b As Double 'Parameter a, b of a function 
    Dim p As Integer, CalcFunction As System.Action(Of String, Double, _
           Double, Integer) = Sub(Func As String, pa As Double, pb As Double, d As Integer)
        pa = Microsoft.VisualBasic.Mathematical.Helpers.Function.Functions(Func)(pa, pb)
        LBLocation -= Len(Func) + d
        se = Mid(Expression2.ToString, LBLocation, p - LBLocation + 2)
        Expression2.Replace(se, pa)
        p -= Len(se)
    End Sub

    Do While p <= Expression2.Length - 1
    'Scaning the whole expression, the loop var 'p' is
    'the reading position on the expression string

        If Expression2.Chars(p) = "("c Then
            LBStack.Push(item:=p + 1)
        ElseIf Expression2.Chars(p) = ")"c Then
        'The expression string between two paired bracket is a mostly simple expression.

            LBLocation = LBStack.Pop
            se = Mid(Expression2.ToString, LBLocation + 1, p - LBLocation)
            r = se
            LBLocation += 1
            If LBLocation < Expression2.Length AndAlso _
                    OPERATORS.IndexOf(Expression2.Chars(LBLocation)) = -1 Then
            'The previous character not is a operator, then it maybe a function name. 
                Dim f = GetFunctionName(Expression2, LBLocation)   'Get the function name
                Call CalcFunction(f, r.Evaluate, 0, 1)  'Calculate the function with only one paramenter. 
            Else
                Expression2.Replace("(" & se & ")", r.Evaluate)
                p -= Len(se)
            End If
        ElseIf Expression2.Chars(p) = ","c Then
        'Meet a parameter seperator of a function, that means we should
        'calculate this parameter as a simple expression as the bracket calculation has been done before. 
            LBLocation = LBStack.Peek
            'We get a function paramenter 'a', it maybe a simple expression, do some calculation for this parameter. 

            se = Mid(Expression2.ToString, LBLocation + 1, p - LBLocation)
            a = CType(se, Types.SimpleExpression)
            LBStack.Push(item:=p + 1)  'Push the position of seperator character ',' to the stack
            p += 1
            'Calculate the function parameter 'b'
            Dim LBStack2 As New Stack(Of Integer)
            Do While p <= Expression2.Length - 1   'Using a loop to get the paramenter 'b'
                If Expression2.Chars(p) = "("c Then
                    LBStack2.Push(item:=p + 1)
                ElseIf Expression2.Chars(p) = ")"c Then
                'The expression string between two paired bracket is a mostly simple expression.
                    If LBStack2.Count = 0 Then Exit Do Else LBStack2.Pop()
                End If
                p += 1
            Loop
            LBLocation = LBStack.Pop 'Parse the pramenter 'b'
            se = Mid(Expression2.ToString, LBLocation + 1, p - LBLocation)
            'Paramenter 'b' maybe a complex expression. 

            b = Evaluate(se) 'Get the value of the parameter 'b'
            'Calculate the value of the function
            LBLocation = LBStack.Pop
            Dim f = GetFunctionName(Expression2, LBLocation)   'Get the function name
            Call CalcFunction(f, a, b, 0)  'Calculate the function with two paramenters. 
        End If
        p += 1
    Loop
    'No more bracket pairs or any function in the expression, it only left
    'a simple expression, evaluate this simple expression and return the result.  
    Return CType(Expression2.ToString, Microsoft.VisualBasic.Mathematical.Types.SimpleExpression)
End Function   

3. 测试

Module Program
    Function Main() As Integer
        Dim Cmd As String = String.Empty
#If DEBUG Then
        Dim sExpression As String = "1-2-3+4+5+6+7+8+9+55%6*3^2"
        Dim e As Microsoft.VisualBasic.Mathematical.Types.SimpleExpression = sExpression
        Console.WriteLine("> {0} = {1}", sExpression, e.Evaluate)
#End If
        '(log(max(sinh(((1-2-3+4+5+6+7+8+9)-20)^0.5)+5,rnd(-10, 100)))!%5)^3!
        Console.Write("> ")
        Do While Cmd <> ".quit"
            Cmd = Console.ReadLine
            Console.WriteLine("  = {0}", Expression.Evaluate(Cmd))
            Console.Write("> ")
        Loop
        Return 0
    End Function 
End Module 

一个微型数学脚本引擎

如何解析常量

常量是一个假变量,它的值在任何时候都不会改变。当我们想从表达式中解析一个常量时,我们应该先获取常量名。然后我们就可以从表达式中解析出常量,但问题是,常量名可能太短,以至于它可能出现在函数名或变量名中,例如

pie+pi+e    

其中 `pie` 是一个变量名,而 `pi` 和 `e` 是常量名。当我们直接将常量名替换为值时,它会破坏变量 `pie`,这是非常糟糕的。所以常量解析工作变得很困难。

通过观察常量的模式,我们发现常量的模式是

[Operator][Constant Name][Operator]  

这个模式和变量或数字的模式一样。所以从这一点我们可以进行常量和变量名的解析工作。我们是这样做的

  1. 获取一个名称列表,该列表按字符串长度降序排列。
  2. 使用 `InStr` 函数获取此常量名称在表达式中的位置。
  3. 这是关键步骤:获取我们获得的位置的前一个字符和下一个标记字符,然后查看它们是否都是运算符。
  4. 如果为真,那么我们得到一个常量,并将其标记替换为常量值。
  5. 如果不是,则使用 `InStr` 的重载函数获取下一个常量位置。
  6. 下一个循环。

那么,让我们看看我的代码在 `Function Replace(expression As String) As String` 函数中是如何工作的

首先,为了应对常量出现在表达式开头和结尾的情况,我们给表达式添加零,以简化这种情况

expression = "0+" & expression & "+0" 

然后通过一个 `For` 循环替换模块中的所有常量

For Each [Const] In ConstantList
    Dim p As Integer = InStr(expression, [Const]), _
             l As Integer = Len([Const]) 'The length of the constant name
    c = Constants([Const])  

变量 `p` 是我们表达式中特定常量位置,所以使用函数我们得到一个位置,这个位置可能是常量的位置,或者只是变量或函数名的一部分。接下来我们获取此常量之间的字符,看看它是否是常量名。

Do While p 
    Right = expression(p + l - 1) 
    Left = expression(p - 2) 
    'if this tokens is surrounded by two operators then it is a constant name, not part 
    'of the function name or other user define constant name.
    If InStr(LEFT_OPERATOR_TOKENS, Left) AndAlso InStr(RIGHT_OPERATOR_TOKENS, Right) Then
        s = Mid(expression, p - 1, l + 2) 
        Call sBuilder.Replace(s, Left & c & Right) 
        expression = sBuilder.ToString 
        p = InStr(p + Len(c), expression, [Const]) 
    Else
        p = InStr(p + l, expression, [Const]) 
    End If
Loop 

如果我们得到一个常量,那么我们就用它的值替换它;否则,我们就转到下一个位置

p = InStr(p + l, expression, [Const])  

用户变量的解析与常量解析相同,常量和变量的区别在于,常量存储在 `dictionary` 中,因此我们创建它之后就不能再次修改其值;变量存储在 `hashtable` 中,因此我们将来可以修改其值。

请注意,我们在 `Expression.Evaluate` 函数中先进行常量解析工作,然后再进行变量解析工作

expression = Helpers.Constants.Replace(expression) 'Replace the constant value
expression = Helpers.Variable.Replace(expression) 

因此,常量比变量具有更高的优先级,这意味着如果我们有一个名为 `o` 的常量和一个同名的变量,则常量将覆盖变量 `o` 的值。

编写一个简单的数学计算脚本引擎

我在这里为我的数学求值器编写了一个简单的脚本执行引擎,到目前为止它只有三个命令,看看它是如何工作的

该引擎的工作原理

命令以 `{Name, Action}` 的形式存储在一个字典中

''' <summary>
''' all of the commands are stored at here
''' </summary>
''' <remarks>
''' .quit for do nothing and end of this program.
''' </remarks>
Friend Shared ReadOnly StatementEngine As Dictionary(Of String, System.Action(Of String)) = 
    New Dictionary(Of String, System.Action(Of String)) From { 
        {"const", AddressOf Helpers.Constants.Add}, 
        {"function", AddressOf Helpers.Function.Add}, 
        {"var", AddressOf Helpers.Variable.Set}, 
        {".quit", Sub(NULL As String) NULL = Nothing}} 

每个命令都有一个 `Statement As String` 参数,以及入口函数 `Function Shell(statement As String) As String`。我们根据用户从控制台输入的字符串进行分割,以获取命令名。请注意,在我这个微型脚本引擎的语句语法中,命令名始终出现在语句的第一个位置。这是三个命令的语法和用法信息,以及一个额外的赋值命令: 

名称

语法

信息

示例

const

const <const_name> value 

声明一个新的常量  

Const p 123  

function

function <function_name> expression 

声明一个新的函数 

Function f log(x^3+e)-y 

var

var <var_name> value 

声明一个新的变量或为变量赋值 

Var p2 0.123

值分配

<var_name> <- expression 

将表达式的值赋给变量 

P2 <- f(pi!,pow(e,5))

声明一个常量或变量

常量与变量相同,但我们不能再次更改常量的值,以下是我们声明新常量或变量的方法

''' <summary>
''' Add a user constant to the dictionary.
''' (向字典之中添加用户自定义常数)
''' </summary>
''' <param name="Name"></param>
''' <param name="value"></param>
''' <remarks>
''' const [name] [value]
''' </remarks>
Public Shared Sub Add(Name As String, value As String) 
    Constants.Add(Name.ToLower, value) 
    ConstantList.Clear() 
    Dim Query As Generic.IEnumerable(Of String) = From s As String In Constants.Keys 
                                                  Select s 
                                                  Order By Len(s) Descending 'Re generate the name list of the constant
    Call ConstantList.AddRange(Query.ToArray) 
End Sub  

首先,我们将新常量添加到常量字典中,然后重新生成常量名称列表,以便将来计算时进行常量值替换。变量声明也执行相同操作。

声明一个函数

声明函数与声明常量和变量相同,并且它只有两个参数进行替换,因为 Visual Basic 的数学函数最多只有两个参数。所以我们首先将函数参数 `x` 和 `y` 替换为一个很长的字符串,该字符串很难与用户在我脚本引擎中声明的任何对象相同,然后,当我们计算用户函数时,我们可以直接将此字符串替换为其值。最后,我们使用 `Expression.Evaluate` 的共享函数通过委托函数获取此用户函数的值。

''' <summary>
''' Add a user function from the user input from the console or a text file.
''' </summary>
''' <param name="Name">The name of the user function.</param>
''' <param name="Expression">The expression of the user function.</param>
''' <remarks>
''' function [function name] expression
''' </remarks>
Public Shared Sub Add(Name As String, Expression As String) 
    Dim [Function] As System.Func(Of Double, Double, Double) 'The function delegate
    Expression = Constants.Replace(Expression) 
    Expression = Replace(Expression) 

    [Function] = Function(DblX As Double, DblY As Double) As Double
        Dim sBuilder As StringBuilder = New StringBuilder(Expression) 
        sBuilder.Replace(X, DblX) 
        sBuilder.Replace(Y, DblY) 
        Return Microsoft.VisualBasic.Mathematical.Expression.Evaluate(sBuilder.ToString) 
    End Function

    Call Add(Name.ToLower, [Function]) 
End Sub  
变量赋值
Variable <- expression  

表达式可以是任何数学表达式,具有 Visual Basic 中数学表达式的语法。实际上,我脚本引擎中的赋值语句是变量声明的另一种方式

> var x1 123 
> x1 
  = 123 
> x2 <- 33 
> x2 
  = 33 
>  
一个特殊变量

变量 `$` 是一个系统保留变量,用于保存最后一个表达式的计算值。

> $ <- e 
> $ 
  = 2.71828182845905 
> sin(e) 
  = 0.410781290502904 
> $ 
  = 0.410781290502904 
> 

我将字符 `$` 设为一个系统保留变量,因为我打算像 **Matlab** 的 m 文件一样,为我的脚本引擎添加多行脚本文件计算功能。这个系统保留变量将保存每个脚本文件的计算结果,并将其返回给脚本引擎,以便将其值赋给调用脚本文件或脚本文本的父函数。

该程序是在 Microsoft Visual Studio 2013 Preview、Microsoft Windows 7 Ultimate 上开发的,并在 Ubuntu 13.04 (Mono 2.1) 上成功调试和测试。

© . All rights reserved.