
·您现在的位置: 云翼网络 >> 文章中心 >> 网站建设 >> 网站建设开发 >> ASP.NET网站开发 >> 基于语法分析器GOLDParser开发的数学表达式计算器
最近发现一款文法分析神器,看完官网(http://goldparser.org/)的介绍后感觉很犀利的样子,于是就拿来测试了一番,写了一个数学表达式分析的小程序,支持的数学运算符如下所示:
常规运算:+ - * / ^ sqrt sqrt2(a,b) pow2(a) pow(a,b)
三角函数:sin cos tan cot asin acos atan acot
指数对数:log2(a) log10(a) ln(a) logn(a,b) e^
最大最小:max(a,b,...) min(a,b,...)
一、 GOLD Parser简介
GOLD Parser是一款强大的文法分析工具,支持c++, c, c#, java, Python, Pascal等多种语言,详细信息请参见官网 http://goldparser.org/
使用该工具主要包括三个步骤:
二、 数学表达式语法定义
从官网上下载GOLD Parser Builder Tool,按照提示进行安装,安装完成后就可以编写语法定义了。主界面如下,工具中附带的测试工具十分强大,写完语法定义后,可以直接对语法进行测试,生成语法树状图。

在编写语法描述之前,首先我们先熟悉一下GOLD Meta-Language的基本特性,该语言主要由以下几部分组成:
1. 语法文件属性描述
这部分是用来描述我们即将编写的语法文件的相关信息的,如语法名称、作者、版本号等等。格式如下:
"Name" = 'My PRogramming Language'
"Version" = '1.0 beta'
"Author" = 'John Q. Public'
"Start Symbol" = <Statement> //必不可少,表示定义的开始,上面的可以不写
2. 字符集定义
这部分是用来描述我们语言中所要用到的字符集,GOLD Meta-Language中预先定义了很多字符集,如常用的数字集合{Number}、字母集合{Letter}、可打印字符集合{Printable}等等,也可以使用Unicode码指定字符集范围{&4F00..&99E0},表示从4F00到99E0之间的所有字符。格式如下:
{String Char} = {Printable} – [”] //表示从可打印的字符中减去”字符
我们可以定义多个字符集供我们定义的语言使用
3. 终结符(Terminal)定义
终结符是指我们定义的语言中能被语法分析器识别的最小单元,举例说明一下,比如下面一个数学表达式:3.3+sin(a+b1),终结符为“3.3”“+”“sin”“(”“a”“b1”“)”,终结符通常是采用正则表达式定义的,如果我们对正则表达式不了解,那么强烈建议我们去补补正则表达式的相关知识了。在语法文件中,变量及数字的终结符采用如下方式定义
Variable = {Letter}{Number}* //表示一个字母后面跟0个或多个数字,如a,b,x1,y34
NumberValue = {Number}+ | ({Number}+'.'{Number}*) //表示整数或小数
4. Productions定义(这个不好翻译o(╯□╰)o,就用英文表示吧)
我们所描述的语言的语法是由一系列Production定义的,而一个Production是由若干个终结符(Terminal)和非终结符(Nonterminal)组成,非终结符通常是由尖括号<>界定,并由若干个终结符及非终节符定义。下图表示的是一个Production,表示语言中的if-then-end语句,其中<Stm>, <Exp>, <Stmts>是非终结符,if, then, end是终结符。

一系列相同类型的Production组成一个规则集(Role),我们所描述的语言的语法就是由规则集定义,下面两幅图两种表示是等价的,是同一个规则集。


在熟悉了GOLD Meta-Language的语法之后,就可以着手编写数学表达式的语法定义了。本人定义的语法文件如下:
! Welcome to GOLD Parser Builder 5.2
"Name" = 'Calculator'
"Version" = 'v1.0'
"Author" = 'xxchen'
"Start Symbol" = <Exp>
Variable = {Letter}{Number}*
NumberValue = {Number}+ | ({Number}+'.'{Number}*)
<Exp> ::= <Exp> '+' <Exp Mult>
| <Exp> '-' <Exp Mult>
| <Exp Mult>
<Exp Mult> ::= <Exp Mult> '*' <Value>
| <Exp Mult> Variable
| <Exp Mult> '/' <Value>
| <Value>
<Exp Func> ::= <Exp Func1>
| <Exp Func2>
| <Exp Funcn>
<Exp Func1> ::= 'sin' <Value>
| 'cos' <Value>
| 'tan' <Value>
| 'cot' <Value>
| 'asin' <Value>
| 'acos' <Value>
| 'atan' <Value>
| 'acot' <Value>
| 'sqrt' <Value>
| 'log2(' <Value> ')'
| 'log10(' <Value> ')'
| 'pow2(' <Value> ')'
| 'e^' <Value>
| 'ln' <Value>
<Exp Func2> ::= <ExpValue> '^' <Value>
| 'pow(' <Exp> ',' <Exp> ')'
| 'sqrt2(' <Exp> ',' <Exp> ')'
| 'logn(' <Exp> ',' <Exp> ')'
<Params> ::= <Params> ',' <Exp>
| <Exp>
<Exp Funcn> ::= 'max(' <Params> ')'
| 'min(' <Params> ')'
<Param> ::= NumberValue
| Variable
<ExpValue> ::= <Param>
| '-' <Param>
| '(' <Exp> ')'
| '|' <Exp> '|'
<Value> ::= <ExpValue>
| <Exp Func>
View Code
写完后,直接点软件右下角的Next按钮,在没有提示错误后会生成一个.egt文法表文件,该文件在后面的程序编写过程中需要用到。
三、 利用解析引擎编写代码
由于个人比较熟悉c#语言,故采用了c#语言版本的解析引擎,其它语言版本的引擎在官网上也有提供。在正式编写代码之前,还可以利用Builder Tool来生成对应引擎的解析框架,在Project-Create a Skeleton Program菜单下可以打开向导进行设置,选择对应的语言及解析引擎,就可以生成相应的解析框架了。

自动生成出来的解析框架非常简单,如下所示,主要有两个函数需要注意,第一个是Parse函数,该函数接受一个TextReader类型的参数,用来读取需要解析的内容,里面的解析逻辑都已自动生成;第二个是CreateNewObject函数,我们需要修改的就是这个函数,在引擎解析过程中,我们需要根据每个步骤的解析结果生成我们需要的对象,以实现我们需要的逻辑。在不影响整体框架的前提下,其它部分可以任意修改,在这里我添加了一个带参数的构造函数,参数是文法表文件的路径,然后在构造函数中初始化解析引擎。
为了实现计算逻辑,这里定义了一个简单的表达式类,该类的构造函数可以接受一个常数,或者一个变量,或者接受若干个表达式。
/// <summary>
/// 表达式类
/// </summary>
public class Expression
{
/// <summary>
/// Initializes a new instance of the <see cref="Expression"/> class.
/// </summary>
/// <param name="value">接受一个常数</param>
public Expression(double value)
{
_value = t => value;
}
/// <summary>
/// Initializes a new instance of the <see cref="Expression"/> class.
/// </summary>
/// <param name="variable">接受一个变量</param>
public Expression(string variable)
{
_value = t => t[variable];
_varList = new List<string> { variable };
}
/// <summary>
/// Initializes a new instance of the <see cref="Expression"/> class.
/// </summary>
/// <param name="func">表达式计算函数</param>
/// <param name="exps">接受若干个表达式</param>
public Expression(Func<double[], double> func, params Expression[] exps)
{
_value = t => func(exps.Select(e => e._value(t)).ToArray());
foreach (var exp in exps)
{
if(exp._varList == null)
continue;
if(_varList == null)
_varList = new List<string>();
_varList.AddRange(exp._varList);
}
if (_varList != null)
_varList = _varList.Distinct().ToList();
}
/// <summary>
/// 存储变量名称的链表
/// </summary>
private readonly List<string> _varList;
/// <summary>
/// 获取表达式中的变量
/// </summary>
/// <returns></returns>
public IEnumerable<string> GetVariables()
{
if(_varList == null)
yield break;
foreach (var var in _varList)
yield return var;
}
/// <summary>
/// The _value
/// </summary>
private readonly Func<Dictionary<string, double>, double> _value;
/// <summary>
/// 获取表达式的值,用于计算没有变量的表达式
/// </summary>
/// <returns>System.Double.</returns>
public double GetValue()
{
return GetValue(null);
}
/// <summary>
/// 获取表达式的值,用于计算有变量的表达式
/// </summary>
/// <param name="varTable">参数表</param>
/// <returns>System.Double.</returns>
public double GetValue(Dictionary<string, double> varTable)
{
try
{
return _value(varTable);
}
catch (Exception)
{
return double.NaN;
}
}
}
View Code
再来看一下解析引擎中生成的CreateNewObject函数,下面只截取了部分代码,里面的逻辑也很简单,比如引擎在解析完数字后,可以根据注释,这里是// <Param> ::= NumberValue ,表示r中数据的个数为1,其中r[0].Data对应的就是NumberValue的值,这时我们只需要返回一个常数表达式即可。在解析完变量后,注释的代码是// <Param> ::= Variable,返回一个变量表达式即可。在解析完+号时,对应的注释代码是// <Exp> ::= <Exp> '+' <Exp Mult> 表明r中数据的个数是3,r[0].Data及r[2].Data是我们之前的数据解析完时返回的表达式,对应于解析树中的<Exp>及<Exp Mult>,r[1].Data是”+”号,故在这个节点我们需要生成一个新的加法表达式,然后返回该表达式即可。
Expression exp1, exp2;
switch ((ProductionIndex)r.Parent.TableIndex())
{
case ProductionIndex.Exp_Plus:
// <Exp> ::= <Exp> '+' <Exp Mult>
exp1 = r[0].Data as Expression;
exp2 = r[2].Data as Expression;
result = new Expression(t => t[0] + t[1], exp1, exp2);
break;
case ProductionIndex.Exp_Minus:
// <Exp> ::= <Exp> '-' <Exp Mult>
exp1 = r[0].Data as Expression;
exp2 = r[2].Data as Expression;
result = new Expression(t => t[0] - t[1], exp1, exp2);
break;
case ProductionIndex.Param_Numbervalue:
// <Param> ::= NumberValue
result = new Expression(double.Parse(r[0].Data.ToString()));
break;
case ProductionIndex.Param_Variable:
// <Param> ::= Variable
result = new Expression(r[0].Data.ToString());
break;
……省略类似部分
View Code
至此,数学表达式的解析引擎已经构造完成,使用方法如下:
//根据文发表文件构造解析引擎 var filePath = Path.Combine(Directory.GetCurrentDirectory(), "calculator.egt"); var parser = new CalculatorParser(filePath); //解析读入的字符串 parser.Parse(new StringReader(line)); //读取解析结果,即一个表达式 var exp = parser.Exp; //计算表达式的值 result = exp.GetValue();
四、 实验效果
程序可以计算用户任意输入的表达式,如果发现表达式有误,则会提示用户在哪个位置出现了错误。程序还可以识别变量,并且对数字后面紧接变量的表达方式理解为乘法运算,如3d表示3*d。图中的cos-3-4.d会理解为cos(-3)-4.0xd,其中d为变量
五、 总结
总的来说GOLD Parser是一个非常强大的文法分析工具,可以解析任意有规律的文本文件,如xml, json, html, c, c++, java, c#等等,这些语言的语法描述文件在官网上也都能找得到(不用自己重头再写了)。如果要想解析一门新的语言或者数据描述文件,那么就得自己写语法描述文件,对于语法不是很复杂的语言,在官网上找点资料,然后照着例子写两遍就能搞定了(从刚接触GOLD Parser到完成这个小程序一共花了不到1天时间)。语法写完后,借助现有的解析引擎,程序的编写就非常简单了。
源代码下载地址:http://vdisk.weibo.com/s/yVSnUWjONKKp0
【原创】转载请说明出处!