C#实现的表达式解析与计算类TExprParser介绍

时间:2021-03-21 15:55:18

        在一个报表审核软件项目中需要计算字符串表达式的值(例如"1+2/3")。同时,在编制审核规则时也需要获取审核表达式是否存在语法错误。于是,在国内外网上搜索了几个免费组件,感觉都不太好灵活实用。也使用了网上介绍的基于JScript.NET的表达式计算方法(本质上是模拟Javascript计算表达式),代码异常简短且能获取表达式的值,但不能满足项目需求:

  • 不能确定表达式或其子项计算后的数据类型。
  • 不支持自定义函数,如常见的数值计算函数Truncate和字符处理函数Substr等。起因是,审核软件中要判断某个字符型数据是否含空格、是否含非可见字符等,或根据某个标志位判断一个数据与另一个数据是否匹配。
  • 不能自定义常见的iif(e1,e2,e3)函数和if e1 then e2 else e3结构。
  • 不能使用{n}占位符的参数计算形式,应用缺乏灵活性。
  • 不能自定义运算符。特别,定义符合中文习惯的小于等于(≤)、大于等于(≥)和不等于(≠)运算符,以及更好理解的and/or逻辑表示。这个基于考虑,审核规则表达式不仅我们要能看懂,更需要软件使用者能看懂!例如1>=2 && 5<=6就不及1≥2 and 5≤6好理解。
  • 计算出现异常时直接抛出错误,无法内部捕获,例如表达式错误、数据被0除、数据溢出等。

        总之,能找到的现有免费组件和方法不能满足灵活性应用需求。于是,在2015年春节前编写了一组TExprParser类(其dll库文件和测试程序可以到csdn下载),基本满足了上述需求,具体介绍如下:

        1、名称空间、类版本及其构造函数

        全部类位于名称空间CSUST.Data.Expr中,可以直接使用的类有如下四个版本(基类为TExprChecker)和一个检测类(TExprChecker):

  • 核心版:TExprParserKernal。该版本的表达式使用c/c++/c#的语法,支持常规数学计算(+-*/^,其中^是幂计算)、关系计算(==、!=、>=和<=)、逻辑计算(&&、||),支持两个数学单目运算符(+-)和一个逻辑非单目运算符(!)。运算符的优先级别和结合方法与c/c++/c#的运算符相同。注意,该版本假设表达式没有空格(字符串中可以有空格)、没有非可见字符。即,该版本没有表达式规范性检测的详细解析信息。
  • 基本版:TExprParserBasic。该版本可以使用核心版的全部运算符,表达式可以不规范,将做严格的规范性检测和完整的解析信息提示。
  • 扩展版:TExprParserEx。该版本支持表达式中使用占位符{n},支持iif(e1,e2,e3)函数,支持if e1 then e2 或if e1 then e2 else e3结构,支持and/or(代替&&/||)逻辑运算符。但iff()函数不支持嵌套。
  • 中文版:TExprParserChina。该版本支持自定义函数(见下面的说明)及其嵌套使用(参数也可以使用函数),支持iif(e1,e2,e3)嵌套,支持四个中文数学运算符、六个中文关系运算符、中文左右圆括号、中文左右花括号、中文逗号。注意,表达式不支持中文双引号(该字符作为一般字符处理)。
  • 检测类:TExprChecker。该类用于测试表达式是否正确。虽然上述四个版本也可以测试一个表达式,但TExprChecker类可以测试带占位符{n}的表达式,也可以避免参数不满足要求的函数,例如被0除等。

        上述五个类都只有一个构造函数。其中,核心版、普通版和检测版没有动态参数(即不支持占位符{n}),扩展版和中文版则有动态参数(与占位符匹配,也可以省略),以扩展版和检测举例和简介如下:

  • TExprParserEx(string expr, params string[] paramValues):expr为表达式字符串,字符型数据使用西文双引号标识。paramValues为给占位符提供参数的字符动态数组,数据项之间使用逗号分隔。例如:TExprParserEx parser = new TExprParserEx("1+2/{1}", 1.1),{1}将由1.1代替。如果没有动态参数,则TExprParserEx parser = new TExprParserEx("1+2/5")即可。
  • TExprChecker(string expr):该类有两个事件Checked(检测完成通知)和Checking(跟踪每一步检测情况),以及三个方法Check()(同步调用)、CheckAsync()(异步调用)和Stop()(停止检测)。该类的IsPassed属性表示表达式是否通过测试,IsFinished表示检测是否完成。

       事实上还有一个原生版TExprParserNative类,该版本不能直接使用,用于强调速度的特定场合。设计成这么五个类,主要是考虑运行性能问题。从10万次重复计算的结果看,中文版的时间是基本版的2倍多,是核心版2.2倍左右,是原生版的3倍多(使用等价表达式)。中文版耗时的关键原因在于函数搜索及其递归调用,特别是函数及其参数的递归调用。如果在项目中使用并构建一般的计算表达式(自己可以保证表达式语法和运算符,没有if判断,也没有函数和递归调用),使用核心板或性能接近的基本版就可以满足要求。有if没有函数调用,则使用扩展版即可。

       2、表达式解析与计算结果

       四个解析与计算类均有属性:HasError、ErrorMessage、Result和IsValid(为HasError的逻辑反)。其中,布尔值HasError为true表示表达式有错误,此时ErrorMessage是错误消息。Result是一个TData类的实例,该类有三类结果值(boolean、decimal和string),由其TDataKind属性决定,结果值由DeciamalValue、StringValue和BooleanValue三个属性分别给出。注意,每个运算符均需要判断所需要数据的类型,结果也将标记数据类型。

        3、函数及其参数

        TExprParserChina版支持三大类函数,可以满足常规的数值、字符串和日期计算需求(以下n2表示函数的第二参数,n3表示第三个参数):

  • 数值型函数:Abs(取绝对值)、Ceil(上取整)、Floor(下取整)、Truncate(截取小数位)、Round(舍入到指定精度,n2)、ToStr(数值转化为字符串)、Val(转换字符串为数值)。例如,Ceil(1.1)将返回2,Round(1.555,2)将返回1.56。
  • 字符型函数:Substr(取子串,n2,n3)、Left(取左串,n2)、Right(取右串,n2)、Trim(去前后空格)、Lower(全转小写)、Upper(全转大写)、Len(取字串长)、Match(匹配正则式,n2)、IsBlank(是空白)、HasBlank(含空白)、HasHideChar(含非可见字符)、HasBlankOrHideChar(含空白或非可见字符)、HasStr(包含子串,n2)、InStr(包含在字串中,n2)。其中的,Substr的第二参数n2表示起始位置(注意,以1开始计数)。例如:Substr("12345",2,4)将返回"2345",Match("abc","abc")将返回true,InStr("abc","def")将返回false。
  • 日期型函数:IsDate(是否为正确日期格式)、GetYear(取年份数)、GetMonth(取月份数)、GetDay(取日数)、DaysBetween(取两天间的天数,n2)、Today(取当前日期,n2)、LastDay(取日期所在月的最后一天)、IsDayBefore(是否在日期之前,n2)、IsDaySame(是否为同一天,n2)、IsDayAfter(是否在日期之后,n2)、AddDay(增加日期天数,n2)、AddYear(增加日期年分数,n2)、AddMonth(增加日期月份数,n2)。例如,IsDate("2015-01-14")将返回true,Today(1)将返回"2015-01-15"(如果今天是14号),IsDayAfter("2015-02-14","2015-02-15")的结果是false。

        4、使用举例(下载rar包有exe测试程序和dll)

        1)四个版均可以使用的表达式

        (-1*2+5)/-2+(2+3*5)>5^2&&-15.3++3!=6||5>=4||3<=5||!(4==4)&&5<6

        2)if e1 then e2 else e3 语句及函数嵌套

        if len("123") > 2 then truncate(len("123")) < 1 else 5/-3 > -3 || substr("123",2,abs(-1)) != "2"

        (if 2>1 then 2 else 1) * (if 22>11 then 22 else 11)

        3)iif(e1,e2,e3) 结构

        iif(3>2,"3>2","3<=2") + iif(5>6,"5<2","5>=6") 

        4)函数与占位符使用

        Substr
        (
            IIF({1}≠{2} or 5 ≤ 4.999999999,  "12345", "abcde"),
            len({2})/2,
            5
        )

        Substr
        (
            if IsDaySame({1},today(0)) then today(1) else AddYear(today(1),1),
            2,
            iif(IsDaySame({1},today(0)), 8, 2)
        )

        上述表达式是换行形式,在测试程序中如果有占位符则需要给占位符号的值(字符型),如:"csust"、"csust"。如果是编程形式,见如下代码

        TExprParserChina parser = new TExprParserChina(str, "csust","csust"),其中str为前面给出的表达式字符串。

        5、几点使用说明

  • 占位符{n}对应的参数中不能有函数、iif和if结构,但可以是表达式。这是因为使用了TExprParserBasic类来解析占位符所代表的数据。
  • if e1 then e2 else e3 语句不能嵌套,即不支持 if ... if ... then ... then ...的形式。事实上,支持嵌套的iif(e1,e2,e3)函数完全可以代替if...then...的作用。设计if...then...这个结构完全是从用户理解表达式的角度考虑。当然,也可以支持if嵌套,只是搜索关键字麻烦点、速度慢点而已。
  • if e1 then e2 else e3 语句的结果可以使用,此时需要带左右括号。例如:if (if 3>5 then 5 else 5) > 5 then 6 else 7 表示式是有效的。
  • 检测类TExprChecker使用了穷举方法遍历占位符的两种数据类型(数值和日期字符串)。
  • 表达式中最多支持30个不同的占位符。

        该组类测试还够充分(这里谢谢几位帮测试并发现了不少问题的同学),特别是其中的函数、占位符和递归使用,难免有错误或不足,使用者如果发现问题或有更好的建议和方法,请不吝留言指正。这组类也是笔者将要使用的,自然会经常更新、维护和发布(虽然发布的是一个Dotfuscator混淆的dll库(到csdn下载),但保证该dll能及时更新和可用)。