表达式求值器 - 基础级别
一个不支持开头的括号的表达式求值器。
引言
这是一篇关于如何构建一个基本表达式求值器的文章。目前,它可以求值任何数值表达式,并结合了三角函数。像 e 和 Pi 这样的常数也支持。
背景
你应该对运算符优先级有基本的了解。我将解释这里需要的正则表达式部分。
代码
IsNumeric
检查一个字符是否是数字,而 IsFunct
是一个自定义函数,用于检查一个字符是否对应于函数符号之一。
tokens.Items.Clear()
'Remove all spaces
expr.Text = Replace(expr.Text, " ", "")
Dim num As String, op As Char
num = ""
op = ""
Dim x As New System.Text.RegularExpressions.Regex("[0-9]\-[0-9]")
For Each abc As System.Text.RegularExpressions.Match In x.Matches(expr.Text)
Dim l = CDbl(abc.Value.Split("-")(0))
Dim r = CDbl(abc.Value.Split("-")(1))
expr.Text = Replace(expr.Text, abc.Value, l & "+-" & r)
Next
'Sin : ~
'Cos : `
'Tan : !
'Sec : @
'Cosec or Csc : #
'Cot : $
expr.Text = Replace(LCase(expr.Text), "sin", "~")
expr.Text = Replace(LCase(expr.Text), "cos", "`")
expr.Text = Replace(LCase(expr.Text), "tan", "!")
expr.Text = Replace(LCase(expr.Text), "sec", "@")
expr.Text = Replace(LCase(expr.Text), "csc", "#")
expr.Text = Replace(LCase(expr.Text), "cosec", "#")
expr.Text = Replace(LCase(expr.Text), "cot", "$")
expr.Text = Replace(LCase(expr.Text), "pi", Math.Round(Math.PI, 4))
expr.Text = Replace(LCase(expr.Text), "e", Math.Round(Math.Exp(1), 4)) 'e^1 or e
For Each s As Char In expr.Text
If IsNumeric(s) Or s = "."c Or s = "-" Or IsFunct(s) Then
num = num & s
End If
If IsOp(s) Then
op = s
tkns.Add(num)
tkns.Add(op)
op = ""
num = ""
End If
Next
'The last number will not be stored because there is no operator next to it
tkns.Add(num)
'Display In ListBox
tokens.Items.Clear()
For Each Str As String In tkns
tokens.Items.Add(Str)
Next
当你尝试求值表达式时需要知道的第一个术语是“分词”。意思是将其分解成更简单的部分。例如,“12+5*3”被分词为“12”、“+”、“5”、“*”和“3”。
在分词之前,我想做一些替换来让程序员更简单。
首先,我们应该删除所有空格,以避免出现“无法将 ' ' 转换为 Double”之类的错误。
其次,将“-”视为运算符可能会导致一些复杂情况,因为当你有“5*-5”这样的表达式时,我们提供的逻辑会变得复杂。所以,为了支持带负数的简单逻辑,最简单的解决方法是将“数字-数字”形式的所有实例替换为“数字+-数字”。在分词时,我们编写一个逻辑,使得“-”符号也被连接到字符串中。
我们使用 Regex 来实现这一点。我稍后会解释。
然后,我用符号替换函数。例如,三角函数 Sine 被替换为“~”。所以,当我想求值表达式时,我就可以识别“~”符号并求值 Sine(紧随其后的数字)。我在求值部分的解释中会提到我是如何做的。
然后,我将 PI 和 e 替换为它们各自四舍五入到小数点后 4 位的数值。这里需要注意的是,用户可能会不小心输入“Pi”或“pI”或“PI”。因此,为了通用地处理替换,最好使用 LCase
函数并以小写形式搜索替换字符串(这里是“pi”)。
我一直提到的逻辑是
- 逐个字符读取表达式。
- 如果字符是数字、小数点、“-”号或表示函数的符号(如 Sine 的“~”),则将其连接到字符串。
- 如果是运算符,则将数字添加到列表中,后面跟着运算符。将数字字符串和运算符字符串的值设置为空。
- 循环结束后,将字符串中的数字(这里是
num
)添加到列表中,因为最后不会有运算符。
然后,清空 ListBox tokens
,并将列表(这里是tokens
)中的所有条目添加进去,这样用户就可以看到它是如何分词的。并非所有表达式求值器都需要包含 ListBox。这只是为了查看程序是否正确分词了表达式。
Dim x As New System.Text.RegularExpressions.Regex("[0-9]\-[0-9]")
For Each abc As System.Text.RegularExpressions.Match In x.Matches(expr.Text)
Dim l = CDbl(abc.Value.Split("-")(0))
Dim r = CDbl(abc.Value.Split("-")(1))
expr.Text = Replace(expr.Text, abc.Value, l & "+-" & r)
Next
回到 RegEx 或正则表达式,可以说它在模式匹配方面非常有用。在这里,我们要匹配的模式是“数字-数字”的形式。让我们看表达式“12-3+15-4”。
Regex 返回的匹配项将是“2-3”和“5-4”。然后,我们可以将其替换为“2+-3”和“5+-4”,以便我们将表达式分词(分解成部分)为“12”、“+”、“-3”、“+”、“15”、“+”、“-4”,然后对其进行求值。
在 x
的声明中,我将模式字符串指定为"[0-9]\-[0-9]"
。这个模式字符串是 regex 找到的匹配项。这里的模式字符串意思是“零到九之间的任意数字-零到九之间的任意数字”。方括号“[]”内的字符数量(一个或多个)将被匹配,而“\-”表示“-”应该被视为“-”字符,而不是 Regex 语法中的含义。通常,“-”在“[]”内用于表示范围,如“0-9”、“a-z”或“A-Z”。“\”称为转义序列,它强制将下一个字符视为字面量。
Private Function EvalFunction(ByVal s As String) As Double
If Not IsFunct(s.Chars(0)) Then 's.chars(0) is the function symbol
Return CDbl(s)
End If
Dim z As Double = CDbl(s.Substring(1, s.Length - 1))
' Leaves the operator and retrieves the number
'z is an angle in degrees. To use it directly in the Math.Sin
'or Math.Cos etc. functions, we give radian values
'180 degrees PI radians
'z degrees z * PI/180
z = z * Math.PI / 180
Select Case s.Chars(0)
Case "~"
Return Math.Sin(z)
Case "`"
Return Math.Cos(z)
Case "!"
Return Math.Tan(z)
Case "@"
Return 1 / (Math.Cos(z))
Case "#"
Return 1 / (Math.Sin(z))
Case "$"
Return 1 / (Math.Tan(z))
End Select
End Function
这个函数用于求值像“~30”(sin 30)等字符串。正如我已经提到的,我使用的符号总是会出现在字符串的第一个字符。所以,我检查传递过来的字符串(这里是s
)的第一个字符是否在该列表中。如果不是,则返回原样传递的字符串。在另一种情况下,我使用 substring 函数检索数字部分并将其转换为度数。三角函数将传递的值视为弧度。因此,我将数字转换为度数。转换是直接比例关系。PI 弧度等于 180 度。z
弧度等于多少度?
z = z * PI/180
然后,对 z
执行相应的函数,并通过检查 s
的第一个字符可能存在的各种符号来返回结果。
'Evaluate all functions
For i As Integer = 0 To tkns.Count - 1
Dim s = tkns(i)
If TypeOf s Is Char Then
If IsFunct(s) Then
tkns.Item(i) = EvalFunction(s)
End If
Else
If IsFunct(s.chars(0)) Then
tkns.Item(i) = EvalFunction(s)
End If
End If
Next
'Exponentiation
Dim ind = tkns.IndexOf("^"c)
While Not (ind < 0) 'If there is no such string in the list, it will return -1
Dim lhs = CDbl(tkns(ind - 1))
Dim rhs = CDbl(tkns(ind + 1))
Dim res = lhs ^ rhs
tkns.Insert(ind, res)
tkns.RemoveAt(ind - 1)
tkns.RemoveAt(ind + 1)
tkns.RemoveAt(ind)
ind = tkns.IndexOf("^"c)
End While
首先,我们求值函数。然后,我们处理运算符。
运算的逻辑是
- 获取运算符的第一个位置(或者在这里用索引更好)。
- 当运算符存在时
- 获取运算符左侧和右侧的值,分别称为左值和右值。
- 求出结果。
- 将结果插入到该位置,并移除三个部分——左值、右值和运算符。
- 找到该运算符的位置。
就这样!我们开发了一个简单的数学家 :)
关注点
用符号替换函数使整个过程变得更容易。将减法视为负数加法也是个好方法。
使用正则表达式:如果你想了解更多关于 Regex 的知识,你可以访问这个网站,它是一个简单而优秀的 Regex 学习资源:http://www.zytrax.com/tech/web/regex.htm。