计算器软件其实有很多种,但是基本上都是模仿计算器,用鼠标点击按键来操作,这次我们反其道而行之,采用类似文本输入的操作方式。
功能
1.键盘输入算式,回车后计算结果。
2.根据当前输入的函数的一部分,自动找到备选函数。这时可以用上/下键选择需要的函数后,按空格键确定输入。在整个过程中一直可以表示函数的帮助信息。我们可以参考帮助信息,选择合适的函数。
3.支持三角函数,反三角函数,求和,平均值,乘方,开方,对数,当然还有包含嵌套的四则运算。
执行画面如下
技术要点:
除了操作界面以外,实际我们是要做这样一个解析器,就面临这一个如何描述我们所面的需求的问题。在这里我们使用EBNF的一种形式,它由W3C定义。我们可以在XML Path Language (XPath) 2.0 (Second Edition)中找到它的细节。
一下是我们在计算器中输入的表达式的描述。
[1]Expr::= AdditiveExpr
[2] AdditiveExpr::=MultiplicativeExpr ( ("+" | "-") MultiplicativeExpr )*
[3]MultiplicativeExpr::= UnaryExpr ( ("*" | "/" | "%" ) UnaryExpr)*
[4]UnaryExpr::=("-" | "+")* PrimaryExpr
[5]PrimaryExpr::= NumericLiteral | ParenthesizedExpr | FunctionCall
[6]NumericLiteral::=IntegerLiteral | DecimalLiteral | DoubleLiteral
[7]ParenthesizedExpr::="(" Expr? ")"
[8]FunctionCall::=FunctionName "(" (Expr(","Expr)*)? ")"
[9]IntegerLiteral ::=Digits
[10]DecimalLiteral ::=("." Digits) | (Digits "." [0-9]*)
[11]DoubleLiteral::=(("." Digits) | (Digits ("." [0-9]*)?)) [eE] [+-]? Digits
[12]Digits ::=[0-9]+
[13] FunctionName=sinr
|sind
|cosr
|sind
|tanr
|tand
|asinr
|asind
|acosr
|acosd
|atanr
|atand
|power
|root
Token解析
解析算式也好,脚本也罢,在分析语法之前,一般都会引入词法分析的过程。简单说比如我们有如下算式
acosd(2)+cosd(30)*12+1.23E-21 |
为了处理简单,在真正计算之前都会先将输入分解成一个个的单词(Token)。比如上面的内容经过处理,如果能变成下面的形式,就容易处理了。
acosd,(,2,),+,cosd,(,30,),*,12+1.23E-21 |
这里,类似于acosd,cosd和(,),*之类的要素可以通过简单的字符串比较来解决。但是数字还有最后e指数就没有那么简单了。当然如果你认为这正好是展现编程基本功的机会的话,没有人拦着你,但是在本软件中我们采用了正则表达式来解决这个问题。
例如e指数可以如下定义:
"((\\.[0-9]+)|([0-9]+(\\.[0-9]*)?))[eE][+-]?[0-9]+" |
对于一般形式的小数:
"(\\.[0-9]+)|([0-9]+\\.[0-9]*)" |
整数就更简单了。
"[0-9]+" |
这样一来我们就可以象处理+/-号一样处理其他要素了。可以很明显的看出,这里的内容和EBNF的内容很相似,也不知道是谁跟谁学的。
表达式解析
表达式解析的程序结果基本上就是参照EBNF的内容在加上一些归纳。主要功能有:
1.表达式解析通过buildExpr实现
2.表达式计算通过evaluate实现。
这里只提供类图,详细可以参照代码。
如何扩展自己的函数
计算器功能强大与否,主要是看函数功能是否强大。所以在设计的最初就把能够简单的扩展函数功能作为关键的设计目标之一。有没有做到呢?实际看一下代码应该最能说明问题。
先是头文件。
#ifndefACOSDFUN_H
ifndefACOSDFUN_H
#define ACOSDFUN_H
#include"calculatefunction.h"
class AcosdFun : public CalculateFunction
{
public:
AcosdFun();
virtual QString getName();
virtual QString getInstruction();
virtual bool execute(QList<double> paraList, double& result, QString& message);
};
#endif // ACOSDFUN_H
代码是16行,实际上需要动的,只有类名和防止重复引用的宏定义,算4行吧。
然后是实现部分。
#include"acosdfun.h"
include "functionmanager.h"
#include <math.h>
static FunctionManager::FunctionRegister acosRegister(new AcosdFun());
AcosdFun::AcosdFun()
{
}
QString AcosdFun::getName()
{
return "acosd";
}
QString AcosdFun::getInstruction()
{
QString strInstruction;
strInstruction += "acosd(x) -\r\n";
strInstruction += "Arccosine (inverse cosine) function. Result in degrees.\r\n";
strInstruction += "Argument type and attributes\r\n";
strInstruction += "x must be of type real. Its value must satisfy the inequality |X| <= 1.\r\n";
return strInstruction;
}
bool AcosdFun::execute(QList<double> paraList, double& result, QString& message)
{
if(paraList.count() != 1)
{
message = "Acosd:Invalid parameter count";
return false;
}
double para = paraList.first();
clearError();
result =acos(para) * 180 / M_PI;
return checkError(message);
}
这个类是角度单位的反余弦函数的实现。空行算在内也只有38行。
第6行可能比较费解,是用来登录函数的。有了这一行,就能在函数列表中看到自己定义的函数了。
如果是简单的函数的话,相信15分种可以搞定。
只要在工程文件加入两个文件就可以扩展函数功能,不能再简单了吧。
阅读最新文章请扫描下面二维码,关注公众号【面向对象思考】。