# mathematical-expression 实现 数学表达式解析 C++ 篇 *C++ 技术栏* 使用 ME( mathematical-expression)数学表达式解析库 实现 C++ 中 数学表达式 的解析和计算。 ## 目录 [TOC]  ## mathematical-expression 介绍 在计算机中,本身并不存在数学表达式的概念,数学表达式其本身是在人类世界中对于逻辑的一种总结规范,计算机并不了解这些公式中的符号,因此对于数学表达式也需要一个编译器,就像编程语言到机器码之间的编译器一样,mathematical-expression 是一种针对数学公式解析的有效工具,能够解析包含嵌套函数,包含函数,数列步长累加等数学公式,返回值是一个数值的结果对象,同时也可以进行比较运算的操作,再进行比较的时候,返回值是一个布尔值结果对象。 ### 获取到 ME 组件 ME 组件的获取 在 C++ 中是以动态库的形式存在的,因此您可以直接使用引入 dll 的方式来实现 C++ 中库的调用,下面就是一个示例。 #### ME 组件存储库 您可以在 [ME 组件存储库 历史存档馆](https://github.com/BeardedManZhao/mathematical-expression-cpp/releases "ME 组件存储库 历史存档馆") 中获取到您需要的版本对应的动态库文件,当然,您也可以直接使用源码进行编译,使得其可以在您的操作系统中使用! #### 编译 ME 组件库 ##### 下载源码 在这里我们以 Windows 系统中进行编译为例子,首先需要执行下面的命令来下载这个文件,其中的版本号可以更换! ``` wget https://github.com/BeardedManZhao/mathematical-expression-cpp/archive/refs/tags/1.0.1.1.zip ``` 这样之后就可以获取到库的源码了,解压之后源码结构如下所示。 ``` G:\My_Project\CLION\mathematical-expression-cpp>tree 卷 Barracuda 的文件夹 PATH 列表 卷序列号为 8CC1-0CB8 G:. ├─.idea ├─cmake-build-debug │ ├─.cmake │ │ └─api │ │ └─v1 │ │ ├─query │ │ └─reply │ ├─CMakeFiles │ │ ├─3.23.2 │ │ │ ├─CompilerIdC │ │ │ └─CompilerIdCXX │ │ ├─mathematical_expression_cpp.dir │ │ └─Progress │ └─Testing │ └─Temporary ├─include ├─src │ ├─core │ │ ├─calculation │ │ └─container │ ├─dataContainer │ ├─exceptional │ └─utils └─update ``` 接下来我们开始进行项目构建和编译,在这里我们使用的C++开发工具是 Clion,编译的日志如下所示。 ``` ====================[ 清理 | Debug ]============================================== "D:\Liming\MyApplication\CLion\CLion 2022.2.1\bin\cmake\win\bin\cmake.exe" --build G:\My_Project\CLION\mathematical-expression-cpp\cmake-build-debug --target clean -- -j 16 清理 已完成 ====================[ 构建 | mathematical_expression_cpp | Debug ]================ "D:\Liming\MyApplication\CLion\CLion 2022.2.1\bin\cmake\win\bin\cmake.exe" --build G:\My_Project\CLION\mathematical-expression-cpp\cmake-build-debug --target mathematical_expression_cpp -- -j 16 [ 16%] Building CXX object CMakeFiles/mathematical_expression_cpp.dir/src/core/container/CalculationResults.cpp.obj [ 22%] Building CXX object CMakeFiles/mathematical_expression_cpp.dir/src/core/calculation/PrefixExpressionOperation.cpp.obj [ 44%] Building CXX object CMakeFiles/mathematical_expression_cpp.dir/src/core/calculation/Calculation.cpp.obj [ 44%] Building CXX object CMakeFiles/mathematical_expression_cpp.dir/src/utils/StrUtils.cpp.obj [ 50%] Building CXX object CMakeFiles/mathematical_expression_cpp.dir/src/dataContainer/MEStack.cpp.obj [ 72%] Building CXX object CMakeFiles/mathematical_expression_cpp.dir/src/core/calculation/BooleanCalculationTwo.cpp.obj [ 66%] Building CXX object CMakeFiles/mathematical_expression_cpp.dir/src/core/calculation/BracketsCalculationTwo.cpp.obj [ 66%] Building CXX object CMakeFiles/mathematical_expression_cpp.dir/src/core/calculation/BracketsCalculation.cpp.obj [ 88%] Building CXX object CMakeFiles/mathematical_expression_cpp.dir/src/core/calculation/BooleanCalculation.cpp.obj [ 88%] Building CXX object CMakeFiles/mathematical_expression_cpp.dir/src/exceptional/MExceptional.cpp.obj [ 88%] Building CXX object CMakeFiles/mathematical_expression_cpp.dir/src/core/mathematical_expression.cpp.obj [ 88%] Building CXX object CMakeFiles/mathematical_expression_cpp.dir/src/core/calculation/NumberCalculation.cpp.obj [ 88%] Building CXX object CMakeFiles/mathematical_expression_cpp.dir/src/core/calculation/FunctionManager.cpp.obj [ 88%] Building CXX object CMakeFiles/mathematical_expression_cpp.dir/src/core/calculation/FunctionFormulaCalculation.cpp.obj [ 88%] Building CXX object CMakeFiles/mathematical_expression_cpp.dir/src/utils/NumberUtils.cpp.obj [ 88%] Building CXX object CMakeFiles/mathematical_expression_cpp.dir/src/core/calculation/CalculationConstant.cpp.obj [ 94%] Building CXX object CMakeFiles/mathematical_expression_cpp.dir/src/core/calculation/CumulativeCalculation.cpp.obj [100%] Linking CXX shared library libmathematical_expression_cpp.dll [100%] Built target mathematical_expression_cpp 构建 已完成 ``` 编译操作结束,我们执行下面的命令就可以看到 “libmathematical_expression_cpp.dll” 文件了,这个文件就是我们编译好的动态库。 ``` G:\My_Project\CLION\mathematical-expression-cpp>cd cmake-build-debug G:\My_Project\CLION\mathematical-expression-cpp\cmake-build-debug>dir 驱动器 G 中的卷是 Barracuda 卷的序列号是 8CC1-0CB8 G:\My_Project\CLION\mathematical-expression-cpp\cmake-build-debug 的目录 2023/12/02 15:01 <DIR> . 2023/12/02 14:51 <DIR> .. 2023/12/02 14:51 <DIR> .cmake 2023/06/23 06:33 285,824 .ninja_deps 2023/06/23 06:33 10,360 .ninja_log 2023/06/23 06:33 18,474 build.ninja 2023/12/02 14:53 53,705 CMakeCache.txt 2023/12/02 15:01 <DIR> CMakeFiles 2023/12/02 14:53 1,691 cmake_install.cmake 2023/12/02 15:01 16,956,659 libmathematical_expression_cpp.dll 2023/12/02 15:01 3,027,692 libmathematical_expression_cpp.dll.a 2023/12/02 14:53 30,288 Makefile 2023/12/02 14:53 14,155 mathematical_expression_cpp.cbp 2023/12/02 14:51 <DIR> Testing 9 个文件 20,398,848 字节 5 个目录 2,684,821,766,144 可用字节 ``` #### 装载 ME 库 您可以将您下载或者您手动编译好的库文件拷贝到您的 exe 文件的目录中,并且将头文件拷贝到您的项目中,头文件就是 ME 源码包中的 “include”目录,接下来将介绍一些导入 ME dll文件的方法。 ##### Cmake 依赖脚本 ###### 编写 cmake 脚本 如果您选择使用 cmake 那么下面就是一个 cmake 脚本的例子 ```cmake cmake_minimum_required(VERSION 3.23) project(CppTest) set(CMAKE_CXX_STANDARD 14) add_executable(CppTest main.cpp) # 设置头文件目录 include_directories(head include) # 将链接库直接与程序文件存储在一起,使得程序文件不需要依赖额外的库,避免缺失dll set(CMAKE_EXE_LINKER_FLAGS "-static-libgcc -static-libstdc++") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") set(CMAKE_EXE_LINKER_FLAGS "-static") # 链接动态库 target_link_libraries(${PROJECT_NAME} G:/My_Project/CLION/CppTest/cmake-build-debug/libmathematical_expression_cpp.dll) ``` ###### 将头文件以及dll库放置到对应的位置中 下面将分别展示项目结构,头文件目录结构,以及编译目录结构 ``` PS G:\My_Project\CLION\CppTest> tree # 查看项目结构 卷 Barracuda 的文件夹 PATH 列表 卷序列号为 8CC1-0CB8 G:. ├─.idea ├─cmake-build-debug │ ├─.cmake │ │ └─api │ │ └─v1 │ │ ├─query │ │ └─reply │ ├─CMakeFiles │ │ ├─3.23.2 │ │ │ ├─CompilerIdC │ │ │ │ └─tmp │ │ │ └─CompilerIdCXX │ │ │ └─tmp │ │ ├─CMakeTmp │ │ └─CppTest.dir │ └─Testing │ └─Temporary └─head PS G:\My_Project\CLION\CppTest> dir head # 查看头文件目录 目录: G:\My_Project\CLION\CppTest\head Mode LastWriteTime Length Name ---- ------------- ------ ---- -a---- 2023/6/23 6:33 1110 BooleanCalculation.h -a---- 2023/6/23 6:33 1168 BooleanCalculationTwo.h -a---- 2023/6/23 6:33 2025 BracketsCalculation.h -a---- 2023/6/23 6:33 1188 BracketsCalculationTwo.h -a---- 2023/6/23 6:33 1739 Calculation.h -a---- 2023/6/23 6:33 1172 CalculationConstant.h -a---- 2023/6/23 6:33 9518 CalculationResults.h -a---- 2023/6/23 6:33 2857 ConstantRegion.h -a---- 2023/6/23 6:33 1157 CumulativeCalculation.h -a---- 2023/6/23 6:33 824 FunctionFormulaCalculation.h -a---- 2023/6/23 6:33 2510 FunctionManager.h -a---- 2023/6/23 6:33 3087 mathematical_expression.h -a---- 2023/6/23 6:33 1308 MEStack.h -a---- 2023/6/23 6:33 767 MExceptional.h -a---- 2023/6/23 6:33 2382 NumberCalculation.h -a---- 2023/6/23 6:33 1202 PrefixExpressionOperation.h -a---- 2023/6/23 6:33 3425 Utils.h PS G:\My_Project\CLION\CppTest> dir .\cmake-build-debug\ # 查看编译结果目录 里面有 libmathematical_expression_cpp.dll 目录: G:\My_Project\CLION\CppTest\cmake-build-debug Mode LastWriteTime Length Name ---- ------------- ------ ---- d----- 2023/12/2 14:41 .cmake d----- 2023/12/2 15:05 CMakeFiles d----- 2023/12/2 14:42 Testing -a---- 2023/12/2 15:05 10278 build.ninja -a---- 2023/12/2 14:42 16919 CMakeCache.txt -a---- 2023/12/2 14:42 1631 cmake_install.cmake -a---- 2023/12/2 15:01 16956659 libmathematical_expression_cpp.dll PS G:\My_Project\CLION\CppTest> ``` ##### 查看 ME 库的版本 测试是否成功导入文件 ```c++ #include "mathematical_expression.h" int main(){ system("chcp 65001"); // 打印 mathematical_expression 的版本信息 cout << mathematical_expression::getVERSION() << endl; } ``` 下面是运行结果,成功打印出来了版本信息,代表库文件导入成功!!! ``` Active code page: 65001 1.0.1.1-mathematical_expression-C++ 进程已结束,退出代码0 ``` ### ME 组件结构 ME 组件主要由三个部分组成,分别是数据存储对象,以及,解析与计算对象,还有计算管理者,他们分别负责不同的任务和位置,共同配合,实现强大的计算操作。 ```flow st=>start: 数学表达式 op0=>operation: 从管理者获取到计算组件 op1=>operation: 检查表达式 op2=>operation: 使用解析与计算对象-解析表达式 op3=>operation: 使用解析与计算对象-计算表达式 op4=>operation: 封装计算结果与计算信息 error=>operation: 发生异常 ok=>operation: 计算完毕,封装结果 cond0=>condition: 检查是否通过? cond1=>condition: 是否能够支持这类数学表达式的解析? cond2=>condition: 是否实现共享池? cond3=>condition: 解析是否通过? cond4=>condition: 计算操作正常? e=>end: 得出结果或抛出异常 st->op0->op1->cond0->e cond0(no)->error cond1(no)->error cond2(no)->cond3->opt2 cond3(no)->error cond4(no)->error cond0(yes)->cond1 cond1(yes)->cond2 cond2(yes)->cond4 cond3(yes)->cond4 cond4(yes)->op4->ok ``` 接下来我们将进行实际的演示,讲述如何在 Java 中快速且方便的处理数学表达式。 #### 无括号数学表达式计算组件 此组件顾名思义,就是用于没有括号的数学表达式的解析和计算中,此计算组件是所有计算组件的祖先,具有更简单的结构以及计算性能 ```flow st=>start: 开始计算 cond1=>condition: 是否为高优先级 cond2=>condition: 是否提取完毕 opt1=>operation: 提取运算符 opt2=>operation: 提取操作数 opt3=>operation: 计算临时结果 opt4=>operation: 结果汇总 opt5=>operation: 临时存储 e=>end: 数值结果封装 st->cond2 cond2(no)->opt2->opt1->cond1 cond2(yes)->opt4 cond1(yes)->opt3->opt5->opt2->opt1->cond1 cond1(no)->opt5->opt2->opt1->cond1 ``` 但面对复杂的计算表达式,此计算组件将无法体现出其灵活性,下面是有关该表达式的一个操作示例。 ```java #include "mathematical_expression.h" int main(){ system("chcp 65001"); // 准备一个数学表达式 string s1 = "1 + 20 * 2 + 4"; // 准备 ME 组件对象 的门户类 mathematical_expression me; // 获取到无括号表达式计算组件 ME::PrefixExpressionOperation prefixExpressionOperation = me.getPrefixExpressionOperation(); // 开始进行检查 prefixExpressionOperation.check(s1); // 开始进行计算 这里不仅仅可以使用 calculation 还可以使用 左移运算符 ME::CalculationNumberResults r1 = prefixExpressionOperation.calculation(s1); r1 = prefixExpressionOperation. << s1; // 打印计算结果 cout << "计算层数:" << r1.getResultLayers() << "\t计算结果:" << r1 << "\t计算来源:" << r1.getCalculationSourceName() << endl; } ``` 最终计算结果 ``` Active code page: 65001 计算层数:1 计算结果:45 计算来源:PrefixExpressionOperation 进程已结束,退出代码0 ``` #### 有括号的数学表达式计算 带有括号的数学计算表达式,是最常用的一种计算组件,其能够实现有效的数学表达式优先级计算操作,计算的复杂度也不会太高,下面就是结果 ```flow st=>start: 开始计算 cond1=>condition: 是否包含括号 opt1=>operation: 提取子表达式 opt2=>operation: 无括号表达式计算 1or2 级计算 opt3=>operation: 结果汇总 e=>end: 数值结果封装 st->cond1->e cond1(yes)->opt1->cond1 cond1(no)->opt2->opt3->e ``` 在库中诸多的计算组件都是基于此组件进行拓展的,下面就是一个使用示例。 ```java #include "mathematical_expression.h" int main(){ system("chcp 65001"); // 准备一个数学表达式 string s1 = "1 + 20 * (2 + 4)"; // 准备 ME 组件对象 的门户类 mathematical_expression me; // 获取到无括号表达式计算组件 ME::BracketsCalculationTwo bracketsCalculationTwo = me.getBracketsCalculation2(); // 开始进行检查 bracketsCalculationTwo.check(s1); // 开始进行计算 ME::CalculationNumberResults r1 = bracketsCalculationTwo << s1; // 打印计算结果 cout << "计算层数:" << r1.getResultLayers() << "\t计算结果:" << r1 << "\t计算来源:" << r1.getCalculationSourceName() << endl; } ``` 最终计算结果 ``` Active code page: 65001 计算层数:2 计算结果:121 计算来源:BracketsCalculation 进程已结束,退出代码0 ``` #### 比较运算表达式 基于 有括号 计算组件的一种拓展,在这里我们可以针对两个数学表达式进行比较,在这里的比较操作中,能够实现有效的比较运算,返回值是 布尔 类型的结果对象,它的计算过程如下所示。 ```flow st=>start: 开始计算 cond1=>condition: 左右解析(yes代表左) opt1=>operation: 获取左边表达式 opt2=>operation: 获取右边表达式 opt3=>operation: 有括号表达式计算 N级计算 e=>end: 布尔比较计算 N+1级计算 st->cond1 cond1(yes)->opt1->opt3->e cond1(no)->opt2->opt3->e ``` ```java public class MAIN { public static void main(String[] args) throws WrongFormat { // 获取一个计算数学比较表达式的组件 final Calculation instance = Mathematical_Expression.getInstance(Mathematical_Expression.booleanCalculation2); // 创建3个表达式 String s1 = "1 + 2 + 4 * (10 - 3)"; String s2 = "2 + 30 + (2 * 3) - 1"; String s3 = "1 + 3 * 10"; extracted(instance, s1 + " > " + s2);// false extracted(instance, s1 + " < " + s2);// true extracted(instance, s1 + " = " + s3);// true extracted(instance, s1 + " == " + s3);// true extracted(instance, s1 + " != " + s3);// false extracted(instance, s1 + " <> " + s3);// false extracted(instance, s1 + " <= " + s3);// true extracted(instance, s1 + " >= " + s3);// true extracted(instance, s1 + " != " + s2);// true extracted(instance, s1 + " <> " + s2);// true } private static void extracted(Calculation booleanCalculation2, String s) throws WrongFormat { // 检查表达式是否有错误 booleanCalculation2.check(s); // 开始计算结果 CalculationBooleanResults calculation = (CalculationBooleanResults) booleanCalculation2.calculation(s); // 打印结果数值 System.out.println( "计算层数:" + calculation.getResultLayers() + "\t计算结果:" + calculation.getResult() + "\t计算来源:" + calculation.getCalculationSourceName() ); } } ``` 下面就是计算结果,可以看到这里返回的结果是一个 布尔 类型的结果对象 ``` 计算层数:4 计算结果:false 计算来源:booleanCalculation2 计算层数:4 计算结果:true 计算来源:booleanCalculation2 计算层数:3 计算结果:true 计算来源:booleanCalculation2 计算层数:3 计算结果:true 计算来源:booleanCalculation2 计算层数:3 计算结果:false 计算来源:booleanCalculation2 计算层数:3 计算结果:false 计算来源:booleanCalculation2 计算层数:3 计算结果:true 计算来源:booleanCalculation2 计算层数:3 计算结果:true 计算来源:booleanCalculation2 计算层数:4 计算结果:true 计算来源:booleanCalculation2 计算层数:4 计算结果:true 计算来源:booleanCalculation2 进程已结束,退出代码0 ``` #### 函数计算表达式 在 ME 库中,允许使用一些函数来进行计算操作,数学表达式中的函数在这里也可以实现,通过自定义实现函数的方式,能够封装计算逻辑,且有效的提升灵活性,其计算复杂度并不高,也是比较常用的计算组件。 ```flow st=>start: 开始计算 cond0=>condition: 函数是否提取完毕 cond1=>condition: 函数是否存在 opt1=>operation: 提取函数 opt2=>operation: 提取函数形参 opt3=>operation: 有括号表达式计算 N级计算 opt4=>operation: 计算结果传递 opt5=>operation: 函数内部计算 opt6=>operation: 结果存储 opt7=>operation: 有括号表达式计算 结果汇总 error=>operation: 发生异常 ok=>operation: 包装结果 st->opt1->cond1 cond1(yes)->opt2->opt3->opt4->opt5->opt6->cond0 cond1(no)->error cond0(yes)->opt7 cond0(no)->opt1 ``` 接下来就是相关的使用演示!!! ```c++ #include <mathematical_expression.h> #include "FunctionManager.h" int main() { system("chcp 65001"); // 准备函数 这里的函数的作用是将参数 * 2 auto myFun = [](const double *v) { return *v * 2; }; // 注册函数 将我们的函数注册成为 DoubleValue 的名称 ME::FunctionManager::append("DoubleValue", myFun); // 构建一个数学表达式,表达式中使用到了函数 DoubleValue string s = "2 * DoubleValue(2 + 3) + 1"; // 获取到 数学表达式解析库 mathematical_expression me; // 获取到函数表达式计算组件 auto functionFormulaCalculation = me.getFunctionFormulaCalculation(); // 检查数学表达式 functionFormulaCalculation.check(s); // 计算出结果 ME::CalculationNumberResults results = functionFormulaCalculation << s; // 将结果打印出来 cout << "计算层数:" << results.getResultLayers() << "\t计算结果:" << results << "\t计算来源:" << results.getCalculationSourceName() << endl; } ``` - 运行结果 ``` Active code page: 65001 计算层数:1 计算结果:21 计算来源:BracketsCalculation 进程已结束,退出代码0 ``` #### 注意事项 1.0.2 版本中 针对函数的注册操作不能向后兼容,如果是在1.0.2版本以以后的版本 请使用下面的方式注册函数 ``` // 准备函数 将函数的形参类型 由 double* 更改为 ME::MEStack<double> 即可 因为 ME::MEStack<double> 具有更大的灵活性 auto myFun =[](const ME::MEStack<double>& v) { double res = 0; for (int i = 0; i < v.size(); ++i){ res += v.get(i); } return res; }; // 注册函数 将我们的函数注册成为 DoubleValue 的名称 ME::FunctionManager::append("sum", myFun); ``` #### 多参函数计算组件 从 ME 库 C++ API 1.0.2 版本开始 针对函数数学表达式的计算将不再限制于只能传递一个参数,而是可以传递无数个参数,每个参数会在函数中以函数形参的格式进入到函数对象并进行计算,接下来就是相关的实际演示。 ```java #include <mathematical_expression.h> #include "FunctionManager.h" int main() { system("chcp 65001"); // 准备函数 这里的函数的作用是将 3 个参数求和 auto myFun = [](const ME::MEStack<double>& v) { double res = 0; for (int i = 0; i < v.size(); ++i){ res += v.get(i); } return res; }; // 注册函数 将我们的函数注册成为 DoubleValue 的名称 ME::FunctionManager::append("sum", myFun); // 构建一个数学表达式,表达式中使用到了函数 DoubleValue string s = "2 * sum(2 + 3, 1 + 20, 10 + (1 - 2)) + 1"; // 获取到 数学表达式解析库 mathematical_expression me; // 获取到函数表达式计算组件 auto functionFormulaCalculation = me.getFunctionFormulaCalculation2(); // 检查数学表达式 functionFormulaCalculation.check(s); // 计算出结果 ME::CalculationNumberResults results = functionFormulaCalculation << s; // 将结果打印出来 cout << "计算层数:" << results.getResultLayers() << "\t计算结果:" << results << "\t计算来源:" << results.getCalculationSourceName() << endl; } ``` 在这里就是相关的计算结果 ``` Active code page: 65001 计算层数:1 计算结果:71 计算来源:BracketsCalculation 进程已结束,退出代码0 ``` =未完待续= ---- 相关文章 - [《mathematical-expression 实现 数学表达式解析 Java 篇》](https://www.lingyuzhao.top/?/linkController=/articleController&link=94267819 "mathematical-expression 实现 数学表达式解析 Java 篇") ------ ***操作记录*** 作者:[root](https://www.lingyuzhao.top//index.html?search=1 "root") 操作时间:2024-02-07 19:19:37 星期三 事件描述备注:保存/发布 中国 天津 [](如果不需要此记录可以手动删除,每次保存都会自动的追加记录)