CalcStar:C++数学表达式求值器
CalcStar 是一款可扩展、快速的 C++ 函数求值器,它赋予用户与科学计算器相同的计算能力。
以下是使用 GT 图形用户界面系统的示例实现。
下载 GtCalculatorApp_20170114.zip
版本 1.1.0
日期:2016年12月1日
作者:Anthony Daniels
电子邮件: AnthonyDaniels99@gmail.com
引言
CalcStar
是一款可扩展、快速的 C++ 函数求值器,它赋予用户与科学计算器相同的计算能力。该库可用于为任何 C++ 项目提供函数求值功能。该函数求值器将任何输入函数分词,并将其编译成 RPN 堆栈,然后再进行求值,从而为用户提供答案。函数以 C++ 语法格式输入,并以中缀表示法(即 2 + 2)输入。CalcStar
支持在函数中使用变量。使用 CalcStar
的起始步骤是包含库头文件。
#include <CalcStar.h>
然后,用户声明一个函数求值器,设置要计算的表达式和变量,然后计算函数得到答案。一旦编译了一个方程式,用户就可以使用现有的 RPN 堆栈进行求值,以避免每次执行都进行编译的额外计算。但是,应该注意的是,编译速度非常快。下面是一个可以在提供的 TestApp
中找到的函数求值器的示例用法。
Std::string strFunc;
strFunc = "(2 + 2)/3 * pow(3,2) - 2";
this->m_objFuncEvaluator.Set_strExpression(strFunc);
m_objFuncEvaluator.Evaluate();
dblAnswer = m_objFuncEvaluator.Get_dblCurrValue();
设置变量很简单,就像
CSVariable objVarTemp;
objVarTemp.m_strVarName = "X1";
objVarTemp.m_dblCurrVal = varValue;
m_objFuncEvaluator.AddVariable(objVarTemp);
更新变量值同样很简单。
m_objFuncEvaluator.UpdateVariable(strVarName,dblNewVal);
CalcStar 设计
CalcStar
是一款通用计算器。本质上,它与任何中缀表示法的科学计算器(例如 TI-83)相当。当 CalcStar
计算表达式的答案时,它会经历三个阶段:分词、编译和执行。在分词阶段,string
数学表达式被解析成一组“标记”或数学单词。标记是数学表达式中的基本操作单元。下面是 CalcStar
中标记类型的列表:
enum CSTAR_DLLAPI CSTokenType
{ //重要提示!!!这些运算符的顺序表示运算优先级
//数字越大,优先级越高
//请勿更改此顺序,基于枚举类型进行硬编码
//
NOOP = -1,
OPENPAREN = 0,
OPENBRACKET,
OPENBLOCK,
CLOSEPAREN,
CLOSEBRACKET,
CLOSEBLOCK,
COMMA,
LT,
GT,
LTE,
GTE,
NEQ,
EQ,
AND,
OR,
NOT,
SUB,
ADD,
DIV,
MULT,
POW,
NEG,
ASSIGN,
NUMBER,
VAR,
FUNC
};
FUNC 用于任何类型的用户定义函数,例如表示幂的 pow()
。函数有一个“签名”,后面跟着一个“(”。需要注意的是,如果 CalcStar
在其注册表中找不到函数签名,它会将该标记视为一个变量。CSToken
对象具有以下成员变量:
//This is a thin class, so member
variables are public
//like in a typical struct
//!The token type of this RPN Unit
CSTokenType m_objTokenType;
//!The string representation of the token, also the signature
std::string m_strToken;
//!Token's associativity
CSAssociativity m_objAssociativity;
//!Numeric version of token if applicable
double m_dblToken;
表达式被分词后,该堆栈被编译成逆波兰表示法(RPN)堆栈,以便进行计算。它使用一种改进版的 Shunting Yard 算法(一种在计算机科学中广为人知的算法)来创建 RPN 堆栈。RPN 堆栈保留运算顺序,并允许计算机只需扫描一次堆栈即可执行运算。随着运算的执行,它们的输入和运算符会被一个 CSRpnUnit
替换,该 CSRpnUnit
是该部分计算的输出数字。该过程一直持续到最后只剩下一个输出数字。这就是表达式的答案。
CalcStar 表达式
用户可以编写任何中缀表示法的 C++ 风格数学表达式,该表达式求值为一个单一数字。例如:( 2 + 3) * X – 1,其中 X 是一个等于 3.1415926 的变量。CalcStar
的内置运算符和函数如下:
基本函数
名称 | 签名 | 名称 | 签名 |
Add | + | 布尔与 | && |
Subtract | - | 布尔等于 | == |
Multiply | * | 布尔非 | ! |
Divide | / | 布尔不等于 | != |
指数 | exp | 布尔或 | || |
幂 | pow | 大于 | > |
自然对数 | ln | 大于或等于 | >= |
Log2 | log2 | 小于 | < |
Log10 | log10 | 小于或等于 | <= |
绝对值 | abs | Ceiling | ceil |
平方根 | sqrt | Floor | floor |
Truncate | trunc |
三角函数
名称 | 签名 | 名称 | 签名 |
正弦 | sin | 反正弦 | asin |
余弦 | cos | 反余弦 | acos |
正切 | tan | 反正切 | atan |
双曲正弦 | sinh | 反双曲正弦 | asinh |
双曲余弦 | cosh | 反双曲余弦 | acosh |
双曲正切 | tanh | 反双曲正切 | atanh |
以下是基于 GT 的计算器应用程序的屏幕截图。有关 GT 的更多信息,请参阅本文:
https://codeproject.org.cn/tips/897483/gt-a-cplusplus-graphical-user-interface-library-and-system
Test Menu 运行 ExprTK 的 Prashtow 测试系列。它大约有 6600 个测试,答案生成在 Calculator\TestSuite 文件夹的 Excel 文件中。结果显示在生成的 "TestCalcStar_Results.txt" 文件中。目前,在 6600 个测试中,CalcStar 只失败了 20 个测试。其中大部分失败与 CSV 生成的答案文件中的大数精度问题有关。其他的则与 log 函数有关。这个问题正在调查中。
扩展 CalcStar
CalcStar
最重要的特性之一是其易于扩展性。用户可以使用现有的运算符和函数作为示例,以最小的努力向系统添加新函数。每个 CSRpnUnit
都有一个指向 CSOpBase
对象的指针,该对象负责在调用 Evaluate()
时执行实际计算。CSOpBase
是 CalcStar 中所有运算符和函数的基础类。它本质上是一种函数式设计,带有一个虚函数:
virtual int OpEval(std::vector<CSRpnUnit> &
arrObjCalcStack, bool & blnCalcSuccessful,int intCurrPos) = 0;
每当用户继承 CSOpBase
时,都会覆盖此函数。为了说明这一点,让我们详细看看 CSOpPower
函数。
class CSOpPower: public CSOpBase
{
public:
//!Default Constructor
CSOpPower();
//!Default Destructor
~CSOpPower();
//!Perform the operation on the stack
virtual int OpEval(std::vector<CSRpnUnit> & arrObjCalcStack,
bool & blnCalcSuccessful,int intCurrPos);
};
//OBJECT FACTORY REGISTRATION CODE
static bool blnCSOpPower_Registered =
CSTAR::GetOpBaseFactoryPtr()->Register<CSOpPower>("pow");
CSOpPower
继承自 CSOpBase
,并实现虚函数 OpEval
来执行实际的幂计算。在类定义之外和之后,新函数通过 GetOpBaseFactoryPtr()
… 代码行注册到 OpBase
工厂。模板参数是要注册的类。提供给函数的 string
是函数的签名。注册是用户安装新函数所需做的全部工作。当库编译时,它会以这种方式注册所有函数。在 OpEval
的实现中:
//!Perform the operation on the stack
int CSOpPower::OpEval(std::vector<CSRpnUnit> & arrObjCalcStack,
bool & blnCalcSuccessful,int intCurrPos)
{
//FUNCTION: pow(Number, numdecimal)
//first determine if the necessary inputs are valid, if they are,
//then get the input numbers and perform the calculation,
// Once the calculation is made
// then replace the operation and input tokens
// with the output number token
char chrOutput[256];
for (int i = 0; i < 256; chrOutput[i++] = '\0');
blnCalcSuccessful = true;
bool blnValid;
double dblOutput;
CSRpnUnit
objOutput;
try{
this->ValidOpInputs(arrObjCalcStack,blnValid,intCurrPos,2);
if(blnValid)
{
//then valid inputs, perform the power calculation
//then replace the sin calculation results with the single number
double dblNum, dblPower;
this->GetOpNumber(&(arrObjCalcStack.at(intCurrPos - 1)),dblPower);
this->GetOpNumber(&(arrObjCalcStack.at(intCurrPos - 2)),dblNum);
dblOutput = pow(dblNum,dblPower);
objOutput.m_objTokenType = NUMBER;
objOutput.m_dblToken = dblOutput;
sprintf_s(chrOutput,"%f",dblOutput);
objOutput.m_strToken = chrOutput;
this->ReplaceOp(arrObjCalcStack,intCurrPos - 2,intCurrPos,objOutput);
blnCalcSuccessful = true;
return 1;
}else{
blnCalcSuccessful = false;
return 0;
}
}catch(...){
blnCalcSuccessful = false;
return -1;
}//end try catch
};
该操作首先执行 ValidOpInputs()
来检查堆栈中是否有正确的函数输入。如果有效,则执行幂计算。然后调用 ReplaceOp()
,用单个输出数字 CSRpnUnit
替换运算符和输入。建议用户遵循此模式(验证、计算、替换)。还建议在创建新函数时,只需复制现有函数并更改名称和内部工作。这样更简单,用户也不太可能忘记某些内容或犯错误。