使用正则表达式实现表达式计算器

使用正则表达式实现简易表达式计算器

由于我是做工程的,在实际使用中经常要对复杂公式进行计算,觉得使用 windows 计算器非常不方便,也容易出错,而 matlab 用于小型的计算太浪费(主要是启动比较慢,而且不是所有地方都有),因此决定自己写一个表达式计算器进行使用。在写计算器的过程中,研究了 .NET 的正则表达式系统,略有心得。将其写出来与大家分享。

注:实际完成的表达式计算器可在我的 115 共享上下载,基于了 .NET2.0 版。同时也提供了一个 .NET 2.0 Compact Framework 的版本,可以用于 Windows Mobile 手机,实测在 Windows Mobile 6.1 Windows Mobile 6.5 上运行正常。

在工程应用中,用到最多的就是整数的位运算,与或非异或移位等操作非常频繁,且经常会有连续操作。特别是在通信控制中,经常使用的校验(采用累加或异或的方式),要计算也较多,因此首先考虑了整数的表达式计算,后续考虑了双精度数据的计算。


 

1.     .NET Framework 正则表达式概述( 本节内容来自 Microsoft MSDN

 

正则表达式提供了功能强大、灵活而又高效的方法来处理文本。正则表达式的全面模式匹配表示法使您可以快速分析大量文本以找到特定的字符模式;验证文本以确保它匹配预定义的模式(如电子邮件地址);提取、编辑、替换或删除文本子字符串;将提取的字符串添加到集合以生成报告。对于处理字符串或分析大文本块的许多应用程序而言,正则表达式是不可缺少的工具。

正则表达式的工作方式

使用正则表达式处理文本的中心构件是正则表达式引擎,该引擎在 .NET Framework 中由 System.Text.RegularExpressions..::. Regex 对象表示。 使用正则表达式处理文本至少要求向该正则表达式引擎提供以下两方面的信息:

  • 要在文本中标识的正则表达式模式。

.NET Framework 中,正则表达式模式用特殊的语法或语言定义,该语法或语言与 Perl 5 正则表达式兼容,并添加了一些其他功能,例如从右到左匹配。有关更多信息,请参见 正则表达式语言元素

  • 要为正则表达式模式分析的文本。

Regex 类的方法使您可以执行以下操作:

  • 通过调用 IsMatch 方法确定输入文本中是否具有正则表达式模式匹配项。 有关使用 IsMatch 方法验证文本的示例,请参见 如何:验证字符串是否为有效的电子邮件格式
  • 通过调用 Match Matches 方法检索匹配正则表达式模式的一个或所有文本匹配项。 第一个方法返回提供有关匹配文本的信息的 Match 对象。 第二个方法返回 MatchCollection 对象,该对象对于在分析的文本中找到的每个匹配项包含一个 Match 对象。
  • 通过调用 Replace 方法替换匹配正则表达式模式的文本。 有关使用 Replace 方法更改日期格式和移除字符串中的无效字符的示例,请参见 如何:从字符串中剥离无效字符 示例:更改日期格式

*** 详细的 .NET 正则表达式使用请参见 Microsoft MSDN

 

2.     整数表达式运算的实现

 

2.1 需求分析

首先要确定整数表达式运算需要实现哪一些功能。根据实际情况,当前确定整数表达式运算至少应该支持如下功能:

Ø  可进行四则运算(加法 + ;减法 - ;乘法 * ;除法 / );

Ø  可进行求模(余)操作( % );

Ø  可进行位运算(取反 ~ ;与 & ;或 | ;异或 ^ );

Ø  可进行移位操作(左移 << ;右移 >> );

Ø  可进行部分函数运算(符号 sign ;绝对值 abs ;最大值 max ;最小值 min ;幂 pow );

Ø  运算之间要按照优先级进行;

Ø  可以使用 () 提升运算优先级

Ø  支持 16 进制和 10 进制混用( 0x 开头为 16 进制,否则 10 进制)

先考虑没有括号的情况。为了简化程序设计,考虑识别计算过程按如下步骤进行:

1.    将整个长表达式按照优先级取出优先级最高的基本表达式元素;

2.    对该最基本元素求结果,并用结果替代长表达式中的元素以简化表达式;

3.    进行迭代直到剩下最后一个基本表达式元素;

4.    计算该元素的值,即为整个表达式的运算结果。

以下以一个实例说明计算流程。假设要计算 7%6-3*5 ,则:

1.    计算 3*5=15 ,并用 15 替换 3*5 ,表达式简化为 7%6-15

2.    计算 7%6=1 ,并用 1 替换 7%6 ,表达式简化为 1-15

3.    计算 1-15=-14 ,即得出最终结果。

再考虑有括号的情况。对于有括号的表达式,由于括号优先级最高,可以先将括号中的子串取出,按照上述流程计算出子串的数据,再替换(若是函数则要先计算)子串数据,逐次迭代最终得到结果。

2.2 识别数据

获取一个数( 10 进制或 16 进制)的正则表达式:

要获取一个普通的 10 进制整数,如 125 ,采用正则表达式: /d+

要获取一个带有 0x 前导的 16 进制整数( 16 进制支持 a~f ),如 0x3F ,采用正则表达式: ?(?=0x)0x[0-9a-f]

该数据可能有正负符号前导,因此采用: [/+/-]?

综上所述, (?[/+/-]?(?(?=0x)0x[0-9a-f]+|/d+)) 可以用来捕获一个带有前导符号的 10 进制或 16 进制数据

2.3 基本表达式元素的计算

首先考虑只有一个运算符的形式 ,先按照运算符优先级将相同优先级的运算符声明每个正则表达式(用行隔开了运算符和数据的捕获,下同):

    // 取反

     Regex RegxUnary = new Regex(

@”(?[~])+?

(?[/+/-]?(?(?=0x)0x[0-9a-f]+|/d+))”,

RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture);

    // 乘除余

Regex RegxMultDiv = new Regex(

@”(?[/+/-]?(?(?=0x)0x[0-9a-f]+|/d+))

(?[/*///%])

(?[/+/-]?(?(?=0x)0x[0-9a-f]+|/d+))”,

RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture);

   // 加减

    Regex RegxAddSub = new Regex(

@”(?[/+/-]?(?(?=0x)0x[0-9a-f]+|/d+))

          (?[/+/-])

        (?[/+/-]?(?(?=0x)0x[0-9a-f]+|/d+))”,

RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture);

// 然后是移位(命名为 RegxShift ),最后是其他位操作(与 | | 异或)(命名为 RegxLogic ),

// 这些正则表达式的写法都相同,此处不再一一列举。

     使用正则表达式匹配出表达式以后,可以通过 Capture 对象获取操作符和操作数,即可以求出结果。

2.4 不带括号的长表达式计算

在实际使用中,仅仅只有一个基本表达式是不可能的,因此计算器一定要匹配连续运算符,如:

1+2*3-4%5*6/7<<~8

此时,用上述的正则表达式匹配时,第一个问题就是:如何区别一个数字前面的 + - 符号是表示正负还是一个运算符?简单来讲,对于 6-4*5 -4*5 两个表达式,第一个表达式中的 - 为减法运算,而第二个表达式中的 - 表示( -4 )。如果用前面的正则表达式进行匹配,则这两个表达式首先匹配的结果都是 -4*5 ,而这个结果对于第一个表达式来讲是错误匹配。为了解决该问题,必须对前面所采用的正则表达式进行修改。

首先对 num1 增加一个顶格判定,因为如果一个表达式以 + - 开头,则这个 + - 一定表示正或负,而不是运算符。将 (?[/+/-]?(?(?=0x)0x[0-9a-f]+|/d+)) 更改为

^(?[/+/-]?(?(?=0x)0x[0-9a-f]+|/d+))

这样即可以避免将 6-4*5 的第一个表达式匹配为 -4*5 。但更改以后 6-4*5 无法匹配出表达式 4*5 ,此时 num1 应该还有另一种可能,为

(?<=([,/+/-/%&/|/^]|<<|>>))(?[/+/-]?(?(?=0x)0x[0-9a-f]+|/d+))

(上述表达式仅对应乘除余)

即:若在 + - 之前有逗号、 + - % & ^ 的时候,该 + - 为正负号而非运算符。

因此,乘除余匹配的正则表达式变更为:

Regex RegxMultDivMod = new Regex(

     @"(^(?[/+/-]?(?(?=0x)0x[0-9a-f]+|/d+))

|(?<=([,/+/-/%&/|/^~]|<<|>>))(?[/+/-]?(?(?=0x)0x[0-9a-f]+|/d+)))

     (?[/*///%])

     (?[/+/-]?(?(?=0x)0x[0-9a-f]+|/d+))",

           RegexOptions .IgnoreCase | RegexOptions .ExplicitCapture);

根据相同原理,修改前面的所有基本元素表达式。其中, +- 前出现的符号与优先级有关,即应该包含所有比本正则表达式运算符的优先级要低的其他运算符(包括逗号)。

当所有匹配的正则表达式都修改完成后,即可以进行不带括号的长表达式进行处理了。处理的顺序按照优先级的顺序。处理函数如下:

        void CalcSub(string exp, out string outexp)

        {

            long tmp;

            StringBuilder sb = new StringBuilder (exp);  

            Match m;

 

            m = RegxUnary .Match(sb .ToString());

            while (m .Success)

            {

                // 1. 获取操作数和操作符,并进行计算

                // 2. 用计算结果替换表达式

                // 3. 再次执行匹配

            }

 

            m = RegxMultDivMod .Match(sb .ToString());

            while (m .Success)

            {

                // ~ 操作,省略

            }

 

            m = RegxAddSub .Match(sb .ToString());

            while (m .Success)

            {

                // ~ 操作,省略

            }

 

             m = RegxShift .Match(sb .ToString());

            while (m .Success)

            {

                // ~ 操作,省略

            }

 

            m = RegxLogic .Match(sb .ToString());

            while (m .Success)

            {

                // ~ 操作,省略

             }

 

            outexp = sb .ToString();

        }

 

2.5 函数计算

到目前为止,已经可以很好的计算没有括号的整数长表达式了。下一步的工作是考虑函数的计算。根据先前设定,整数表达式运算需要支持的函数包括符号 sign ;绝对值 abs ;最大值 max ;最小值 min ;幂 pow 等,这些函数中,有的函数只有一个参数,如 sign abs ,而有的函数需要两个参数,如 max min pow 。为了适应这两种情况,定义两个函数正则表达式,分别匹配这两种不同类型的函数。

Regex RegxFunction = new Regex (

@"(?abs|sign)

/((?[/+/-]?(?(?=0x)0x[0-9a-f]+|/d+))/)" ,

RegexOptions .IgnoreCase | RegexOptions .ExplicitCapture);

Regex RegxFunction2 = new Regex (

@"(?pow|max|min)

/((?[/+/-]?(?(?=0x)0x[0-9a-f]+|/d+)),

(?[/+/-]?(?(?=0x)0x[0-9a-f]+|/d+))/)" ,

RegexOptions .IgnoreCase | RegexOptions .ExplicitCapture);

进行函数计算时,首先,将函数括号内的表达式按照 2.4 节进行计算,并得到最终仅剩下不含表达式的基本数值形式,然后再使用函数正则表达式进行匹配。如

max(3*4,1<<3) 先将()内的表达式化简为( 12,8 ),再计算 max(12,8) 的结果。

2.6 复合表达式计算

好了,目前表达式计算器已经可以较好的工作了,但是针对超复杂的表达式还无能为例,例如,要计算

max(abs(7<<2+3*(4-1)^3),min(18*sign(0xAA-0x55),0x35^0x18^(~0x20&0xFF)))+100

实际上,任何一个复杂表达式都可以化简为简单表达式的组合,根据这个原理,采用如下方式进行计算:

1)  令整个表达式为 E

2)  先取出最右边的一个括号内的表达式(设为 E1 );

3)  E1 2.4 节的定义进行计算;

4)  检查 E 中是否有一个参数的函数,若有则进行计算;

5)  检查 E 是否有两个参数的函数,若有则进行计算;

6)  若第 3 4 5 中存在有效匹配,则重复进行 3 4 5 操作,直到没有有效匹配

7)  若第 3 4 5 中都没有任何有效匹配,则证明表达式 E1 已计算到最简状态,此时移除 E1 外的括号,并寻找表达式 E 中的下一个最右边的括号中的表达式。

8)  若寻找到含有括号的最右边表达式,则重复进行 3 4 5 6 7 操作;

9)  如果已经没有括号了,证明表达式已经到达最基本状态,此时执行 3 即可得出最终结果。

对于为何要从最右侧开始的括号开始取子表达式,主要原因是因为好定位。因为最后一个左括号其匹配的右括号一定是在该左括号后的第一个右括号。

处理函数如下:

public long Calc(string exp)

        {

            bool result;

            string res;

            StringBuilder sb = new StringBuilder (exp);

            sb .Replace(" " , "" );

            int leftPos = -1, rightPos = -1;

 

            leftPos = sb .ToString() .LastIndexOf('(' );

            if (leftPos != -1)

                rightPos = sb .ToString() .IndexOf(')' , leftPos);

 

            while ((leftPos != -1) && (rightPos != -1))

            {

                string sub = sb .ToString() .Substring(leftPos + 1, rightPos - leftPos - 1);

 

                result = false ;

 

                if (CalcSub(sub, out res))

                {

                    sb .Replace(sub, res, leftPos, rightPos - leftPos);

                   result = true ;

                }

 

                if (CalcFunc(sb))

                {

                    result = true ;

                }

 

                if (CalcFunc2(sb))

                {

                    result = true ;

                }

 

                if ( !result)

                {

                        sb .Remove(rightPos, 1);

                        sb .Remove(leftPos, 1);

                }

 

                leftPos = sb .ToString() .LastIndexOf('(' );

                if (leftPos != -1)

                    rightPos = sb .ToString() .IndexOf(')' , leftPos);

 

             }

 

            CalcSub(sb .ToString(), out res);

            return GetIntValue(res);

        }

2.7 一点小提升

事实上,整数表达式计算器已经基本上可以很正确的计算出所有表达式了,但是还是存在一些不太人性化的地方。主要问题是没有异常处理操作,也没有表达式错误提示。

考虑以下表达式:假设由于输入错误,输入了 3(5+2) 。对于该表达式目前的表达式计算器会得出一个很莫名其妙的结果 37 。事实上,该问题是将 5+2 的计算结果去掉括号后,与 3 进行了连接操作,这根本不是我们希望得到的结果。实际上,对于这种错误表达式,我们希望提示一个错误,并告知在 3(5 之间缺少一个运算符。同样,这个问题可以使用正则表达式来解决。可以先用正则表达式来匹配这些不合理的表达式,并将其筛选出来,以提醒用户表达式输入有误。

另外,再考虑一个表达式 1+5/ 3%2-1 )。实际上是一个很简单的问题,由于出现了 0 除,在计算 5/0 时得到的返回结果为正无穷大(或者是非数字),此时如果还有未计算完的表达式,则运算过程将抛出一个异常。因此,应该设置一个错误追踪功能,当在计算过程中发生异常时,要提示用户在哪个地方发生了异常。

笔者实现的表达式计算器中考虑了该问题,当输入错误的表达式时,会给出一个提示,指出错误的为止;当出现左右括号数量不一致时,会提示用户数量不一致。

 

3.     浮点表达式运算的实现

 

浮点表达式的计算与整数表达式运算的处理方式大致相同。其差别主要在于:

1)  浮点表达式计算不支持 16 进制数,不支持位操作;

2)  浮点表达式计算支持更多函数的计算,包括开方、三角函数、对数等运算;

3)  可以用 e 表示自然对数,可以用 pi 表示圆周率;

4)  支持科学计数法输入,如 1.5e-3

浮点数的匹配采用以下正则表达式:

(?[/+/-]?/d+(/./d+)?(e[/+/-]?/d+)?)

浮点运算实现的一个有趣的问题在于 e 字母的使用。 exp 表示以 e 为底的幂函数; e 表示 2.71828 ;而 1.5e-3 表示科学计数法。实际上,要识别指示 e 2.71828 e 字母,简单的使用 /be/b 即可以匹配。

因为浮点运算与整数运算的处理方式基本相同,因此此处不再列出具体的浮点运算实现。

 

4.     说明

 

笔者的表达式计算器执行程序可以从笔者的 115 网盘上下载。若有需要完整的源代码, 请email至 [email protected] [email protected] 与笔者联系。欢迎各位提出宝贵的意见和建议。

你可能感兴趣的:(C#应用)