65.9K
CodeProject 正在变化。 阅读更多。
Home

CalcStar:C++数学表达式求值器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.86/5 (17投票s)

2014 年 3 月 24 日

LGPL3

5分钟阅读

viewsIcon

70520

downloadIcon

4007

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大于或等于>=
Log2log2小于<
Log10log10小于或等于<=
绝对值absCeilingceil
平方根sqrtFloorfloor
 Truncatetrunc

三角函数

名称签名名称签名
正弦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 替换运算符和输入。建议用户遵循此模式(验证、计算、替换)。还建议在创建新函数时,只需复制现有函数并更改名称和内部工作。这样更简单,用户也不太可能忘记某些内容或犯错误。

© . All rights reserved.