基于语法分析器GOLD Parser开发的数学表达式计算器
最近发现一款文法分析神器,看完官网(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 Meta-Language编写)
- 使用GOLD Builder工具编译我们的语法文件,生成egt格式的表文件,该文件中存储了编译完成的文法表,供后面解析引擎使用
- 选择一种我们熟悉的编程语言,下载对应的与文法表解析引擎,然后在我们的程序中调用该引擎对我们需要解析的语言进行解析即可
二、 数学表达式语法定义
从官网上下载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>
写完后,直接点软件右下角的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;
}
}
}
再来看一下解析引擎中生成的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[].Data as Expression;
exp2 = r[].Data as Expression;
result = new Expression(t => t[] + t[], exp1, exp2);
break;
case ProductionIndex.Exp_Minus:
// <Exp> ::= <Exp> '-' <Exp Mult>
exp1 = r[].Data as Expression;
exp2 = r[].Data as Expression;
result = new Expression(t => t[] - t[], exp1, exp2);
break;
case ProductionIndex.Param_Numbervalue:
// <Param> ::= NumberValue
result = new Expression(double.Parse(r[].Data.ToString()));
break;
case ProductionIndex.Param_Variable:
// <Param> ::= Variable
result = new Expression(r[].Data.ToString());
break;
……省略类似部分
至此,数学表达式的解析引擎已经构造完成,使用方法如下:
//根据文发表文件构造解析引擎 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
【原创】转载请说明出处!
基于语法分析器GOLD Parser开发的数学表达式计算器的更多相关文章
- [Swift]LeetCode385. 迷你语法分析器 | Mini Parser
Given a nested list of integers represented as a string, implement a parser to deserialize it. Each ...
- java面向对象课程设计-数学表达式计算器
项目简介 设计一个计算器,其能够: 1)由用户输入一个简单的四则运算表达式,求出其计算结果后显示. 2)特殊数学函数,如:绝对值.取整.三角函数.倒数.平方根.平方.立方等. 3)对一定范围内的数字将 ...
- 开源语法分析器--ANTLR
序言 有的时候,我还真是怀疑过上本科时候学的那些原理课究竟是不是在浪费时间.比方学完操作系统原理之后我们并不能自己动手实现一个操作系统:学完数据库原理我们也不能弄出个像样的DBMS出来:相同,学完 ...
- 基于webpack的前端工程化开发解决方案探索(一):动态生成HTML(转)
1.什么是工程化开发 软件工程的工程化开发概念由来已久,但对于前端开发来说,我们没有像VS或者eclipse这样量身打造的IDE,因为在大多数人眼中,前端代码无需编译,因此只要一个浏览器来运行调试就行 ...
- 关于基于.net的WEB程序开发所需要的一些技术归纳
前提: 最近公司里有一个同事,年龄比我大几岁,但是由于是转行来做开发的,许多的关于.net开发技术不是很入行,所以总是会问我一些东西,基于自己以前的一些 经验,总是会愿意给他讲一些总结性的东西,希望他 ...
- Qt计算器开发(二):信号槽实现数学表达式合法性检查
表达式的合法性 由于我们的计算器不是单步计算的,所以我们能够一次性输入一个长表达式.然而假设用户输入的长表达式不合法的话,那么就会引发灾难.所以有必要对于用户的输入做一个限制. 一些限制举例: 比方, ...
- SQLite Lemon 语法分析器学习与使用
本文是浙江大学出版社的<LEMON语法分析生成器(LALR 1类型)源代码情景分析>学习笔记. 用到的Windows下的编译器介绍MinGW(http://www.mingw.org/): ...
- 《Flask Web开发——基于Python的Web应用开发实践》一字一句上机实践(上)
目录 前言 第1章 安装 第2章 程序的基本结构 第3章 模板 第4章 Web表单 第5章 数据库 第6章 电子邮件 第7章 大型程序的结构 前言 学习Python也有一个半月时间了,学到现在感觉 ...
- 实例讲解基于 React+Redux 的前端开发流程
原文地址:https://segmentfault.com/a/1190000005356568 前言:在当下的前端界,react 和 redux 发展得如火如荼,react 在 github 的 s ...
随机推荐
- jquery遍历-filter()
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
- [Swift实际操作]八、实用进阶-(4)通过protocol在两个对象中进行消息传递
本文将演示如何借助协议,实现视图控制器对象和其内部的自定义视图对象之间的数据传递. 首先创建一个自定义视图对象.在项目名称文件夹点击鼠标右键New File ->Cocoa Touch Clas ...
- centos6安装mysql5.7
RPM包安装与卸载mysql 建议:装完mysql后立刻创建一个密码,不然下次登录的时候会有问题.原因是mysql 5.7会自动创建一个临时密码,过期失效,可以到grep "password ...
- Java学习--多态
1. 多态 多态:同一个对象(实物),在不同时刻体现出来的不同状态 多态的前提: A:要有继承关系 B:要有方法重写 C:要有父类引用指向子类对象 父类 f = new 子类() 多态中的成员访问特点 ...
- Windows下Jmeter安装出现Not able to find Java executable or version问题解决方案
安装好java1.8.jmeter4.0,并java -version正常,jmeter也能正常使用.某一次使用突然出现Not able to find Java executable or vers ...
- Python导入模块Import和from+Import区别
在我们使用python的时候会发现使用Import可以导入模块,from+Import也可以,那么他们之间有什么区别,该用哪一种呢?让我们来看看 1.首先在demo.py中创建一个变量a,定义一个函数 ...
- jquery 实现省市二级联动
效果: 源码: <!DOCTYPE html> <html lang="en"> <head> <meta charset="U ...
- 20165224 陆艺杰 Exp6 信息搜集与漏洞扫描
Exp6 信息搜集与漏洞扫描 (1)哪些组织负责DNS,IP的管理. 全球根服务器均由美国政府授权的ICANN统一管理,负责全球的域名根服务器.DNS和IP地址管理. (2)什么是3R信息. 注册人 ...
- php 多字节编码转换
PHP 支持的编码 mb_convert_encoding — 转换字符的编码 string mb_convert_encoding ( string $str , string $to_encodi ...
- CSAPP阅读笔记-数组分配与访问-来自第三章3.8的笔记-P176-P183
这一节比较简单,仅记录几个比较重要的点: 1.C语言允许对指针进行运算,计算出的值会根据该指针引用的数据类型大小进行伸缩. 例子: 其中,xE是数组的起始地址.注意,指针运算时,若最终结果为指针,则指 ...