Functional plotter

本文最后更新于:7 个月前

面向对象
空教室,录音放。先继承,再封装。
写模板,搞工厂。郑成伟,凌精望。
谢谢你,非常棒!精品书,快推广!

函数绘图器

效果呈现

用户入界面

函数绘图器的用户输入 \(\mbox{ui}\) 界面如下,其中点击 θ 按钮之后,函数表达式文本字符串会增加 \(\theta\) 字符串,用户在该界面中输入表达式、精度、自变量 \(x\) 取值范围等信息,输入结束之后,绘图器会自动识别函数表达式类型,结合步长以及定义域生成对应的图像。

有理函数

本函数绘图器首先实现只与加减乘除相关的基本函数,如多项式函数、带括号的多项式函数、有理分式函数。三者统一表达式为 \(f(x)=\dfrac{P(x)}{Q(x)}\) ,其中 \(P(x)\)\(Q(x)\) 均为多项式且 \(Q(x)\neq0\),绘制一些函数图像如下图所示:

\[ \large 多项式函数:\ y=2x^3-x^2-3x+2,x\in[-1.5,1.5] \] \[ \large 含括号多项式函数:\ y=(x+1)\cdot (x-1)\cdot (x-2),x\in[-1.5,2.5] \]

\[ \large 有理分式函数:\ y=\dfrac{x^2+1}{x^4+1},x\in[-2,2] \]

其余初等函数

同时,该函数绘图器实现了三角函数、指数函数、对数函数的绘制

\[ \large 三角混合函数:\ y=\sin x +\cos x,x\in[-5,5] \] \[ \large 指数函数:\ y=e^x,x\in[-2,1.5] \] \[ \large 对数函数:\ y=\ln x,x\in[0.05,2] \]

极坐标函数

当点击 \(\theta\) 按钮时,绘图器会自动在表达式后面增加 \(\theta\) 字符串,同时定义域会自动修改为 \([0,2\pi]\)(可修改),以绘制极坐标函数:

\[ \large 阿基米德螺旋线:\ \rho=\theta,\theta\in[0,\dfrac{3}{2}\pi] \] \[ \large 星形线:\ \rho=1-\sin \theta,\theta\in[0,2\pi] \] \[ \large 极坐标函数(有理分式):\ \rho=\dfrac{\theta-1}{\theta+1},\theta\in[0,2\pi] \]

混合函数

上述函数可以组合成更一般的初等函数,一些比较优雅的函数如下图所示

\[ \large 三角函数与多项式混合:\ y=(x+1)\cdot (\sin x+1),x\in[-2,2] \] \[ \large 三角函数与指数混合:\ y=\cos x\cdot e^x,\theta\in[0,2\pi] \] \[ \large 指数函数和多项式混合:\ y=(x^2-1)e^x,x\in[-1,1] \]

极限修复函数

除了一些定义域上连续可微的函数,本函数绘图器实现极限修复功能,可以绘制存在第一类间断点以及第二类间断点的函数,例如:

\[ \large 与e相关的函数:\ y=x\ln(1+\dfrac{1}{x}),x\in[0,2] \] \[ \large 一次函数:\ y=\dfrac{2x^2}{x},x\in[-2,2] \] \[ \large 正弦极限函数:\ y=\dfrac{\sin x}{x},x\in[-5,5] \]

框架构建

该函数绘图器大作业使用 \(\mbox{QT}\) 编写,共编写 \(18\) 个文件( \(8\) 个头文件,\(8\)\(\mbox{C++}\) 文件,\(2\)\(\mbox{ui}\) 文件)

其中 \(\mbox{Utils.h}\) 头文件中定义一些数学常数(如 \(\pi,e\) ),以及一些内联函数,形成一种函数工具包便于调用,其余除生成实例对象的 \(\mbox{main.cpp}\) 之外均分为头文件和源文件 \((\mbox{.h/cpp})\) ,且每对文件只针对一个类,各种类之间尽量解耦以及模块化,各文件之间的层级关系以及功能如下图所示:


flowchart TD
    A([main.cpp]) --实例对象--> B([mainwindow.h/cpp])
    C((mainwindow.ui)) --用户输入--> B
    B -->|后端图像| D([latexImg.h/cpp])
    B --->|前端展示| E([dialog.h/cpp])
    B --后端数据--> F([Expression.h/cpp])
    F --直角坐标系--> G([recExp.h/cpp])
    F --极坐标系--> H([polarExp.h/cpp])
    G --表达式以及异常处理--> I[(vector< double > x,y)]
    H --表达式以及异常处理--> I
    D --转换latex格式--> J[downloader.h/cpp]
	J --URL网络请求--> K[(QPixmap img)]
	K --图像背景更换--> L{graph}
	I --线性重排以及调整--> L
	E --> M((dialog.ui))
	M --高精度计时器-->L
	N[Utils.h] --内联函数-->B

对于绘制函数图像这一需求,设计上先分为前端和后端,后端处理分为数据双列表的计算以及 \(\LaTeX\) 图像的获取,前端将后端得到的内容进行重排以及处理,最终展示得到相应的含 \(\LaTeX\) 表达式的图像

实现细节

\(\mbox{Utils.h}\) 工具函数

该头文件实现一些工具函数,有字符串处理、背景颜色更换、获取 \(\mbox{vector}\) 对象中的最大值最小值、使用 \(\mbox{C++}\) 文件流的操作转换 \(\mbox{double}\) 类型数据和 \(\mbox{string}\) 类型数据几种函数。例如其中更改背景颜色的工具函数中使用 \(\mbox{QColor}\) 类创建白色和灰色实例对象,将从网络上获取到的 \(\LaTeX\) 图像中的白色背景转换为灰色,以适应函数图像的背景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//更改背景颜色
inline QPixmap changeBackGroundColor(QPixmap img){
QColor white(255, 255, 255);
QColor grey(240, 240, 240);
QImage image = img.toImage();
for (int w = 0;w < image.width();w++){
for(int h = 0; h < image.height();h++){
QRgb rgb = image.pixel(w, h);
if (rgb == white.rgb())
image.setPixel(w, h, grey.rgba());
}
}
return QPixmap::fromImage(image);
}

该函数可以将以白色为背景的图片转换为以灰色为背景的图片:

内联函数

\(\mbox{Utils.g}\) 文件中均使用内联函数,\(\mbox{wiki}\) 上对 inline function 的定义如下:

It serves as a compiler directive that suggests (but does not require) that the compiler substitute the body of the function inline by performing inline expansion, i.e. by inserting the function code at the address of each function call, thereby saving the overhead of a function call.

可见,内联函数通过直接在调用点展开的方式节约函数调用的开销,这对于一些工具函数是合理的,毕竟这些工具函数不需要专门的类加以封装,对于所有类中的方法,均可以直接进行调用展开即可。

\(\mbox{C++}\) 中也可以使用 \(\mbox{lambda}\) 匿名函数实现函数的插入。 \(\mbox{lambda}\) 表达式是 \(\mbox{C++}\) 发展史上的一个重大事件,也是 \(\mbox{C++}\) 支持函数式编程的重要一环。

\(\mbox{C++}\) 既融合了面向对象编程的约束,也融合了函数式编程的自由,一张一弛,乃编程之道\(\mbox{lambda}\) 表达式在数学上可以证明为图灵完备的,其开创了 \(\mbox{C++}\) 的一个崭新编程范式。

具体来说,\(\mbox{lambda}\) 表达式可以看做是一个临时使用的、嵌入在几乎任何地方的函数,简单示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main(void)
{
auto maxVec = [](vector<double> x){ return *max_element(x.begin(), x.end()); };
// maxVec 为 lambda 表达式,代表一个函数
vector<double> a{1, 3, 5, 2, 4};
cout << maxVec(a) << endl; // 使用起来相当于一个函数,可重复调用多个 Vector
return 0;
}

在编写代码过程中,对于一些不需要专门声明的函数,可以使用上述 \(\mbox{lambda}\) 表达式,\(\mbox{lambda}\) 表达式还可以使将函数内部的局部变量进行捕获,防止函数作用完成之后原始数据丢失的情况,由于该特性没有在代码编写中使用,这里不再展开。

后端数据处理

采用正则表达式的方法对表达式中的对各种运算进行逐一搜索递归计算,下面给出所有正则表达式运算优先级代码,在初等函数的分类下,共 \(17\)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
std::regex Expression::priority[17]{
std::regex("(.*)\\+(.*)"), //0,加
std::regex("(.*)\\-(.*)"), //1,减
std::regex("(.*)\\*(.*)"), //2,乘
std::regex("(.*)/(.*)"), //3,除
std::regex("(.*)\\^(.*)"), //4,次方
std::regex("sin(.*)"), //5,正弦
std::regex("cos(.*)"), //6,余弦
std::regex("tan(.*)"), //7,正切
std::regex("arcsin(.*)"), //8,反正弦
std::regex("arccos(.*)"), //9,反余弦
std::regex("arctan(.*)"), //10,反正切
std::regex("log\\((.*),(.*)\\)"), //11,以 10 为底对数
std::regex("ln(.*)"), //12,以 e 为底对数
std::regex("(^(-?\\d+)(\\.\\d+)?$)"), //13,单个常数(包括任何小数)
std::regex("x"), //14,自变量符号 x
std::regex("e"), //15,自然底数 e
std::regex("π") //16,圆周率 π
};

运算级较低的符号应该放在最后计算,由于递归的特点,其应该在前面进行检测,在运算时,只需要依次遍历每个正则表达式,若符合要求,则将表达式拆成几个部分进行递归计算,其流程图(仅有 \(2\) 次递归)如下:


flowchart TD
	A[A operator B] --> B[A]
	A[A operator B] --- C(operator)
	A[A operator B] --> D[B]
	B <--> O[a1]
	B <--> P[operator]
	B <--> Q[a2]
	B --data--> C
	D --data--> C
	D <--> R[b1]
	D <--> S[operator]
	D <--> T[b2]
	C -->Z[result]

对于输入表达式有误进行异常处理:

1
2
3
//合法、括号不匹配、分子不为0且分母为0、分子分母均为0(可能存在极限)、非括号运算符错误、常数错误(小数点出错)、函数无定义
enum judge{isLegal, mismatch, divisorZero, perhapsLim, operatorInlegal, constantInlegal, undefined};
std::map<double, judge> judges;

采用枚举各种错误的方式,对于括号不匹配,分母为 \(0\) 等一些异常处理进行相应的判断。使用标准库中的 \(\mathbf{map}\) 数据结构,将对应自变量 \(x\) 与相关判断构成映射关系,方便之后统一处理,例如对于 / 的处理如下,其中由于计算机浮点数误差,采用比较 \(\mbox{tolerance}\) 误差的方法进行判定:

1
2
3
4
5
6
7
8
9
10
11
12
13
   //正则表达式以 / 为界切分为两部分,调用 str() 函数构造两个新的 Expression 实例对象
if (std::regex_search(this->exp, m, priority[3])){
Expression e1{ m[1].str(), x };
Expression e2{ m[2].str(), x };
//分子"不为0",分母"为0"
if (fabs(e2) < tol && fabs(e1) > tol)
judges[x] = judge::divisorZero;
//分子"为0",分母也"为0",有可能存在极限
else if(fabs(e2) < tol && fabs(e1) < tol)
judges[x] = judge::perhapsLim;
cout << errorStrings[judges[x]] << endl;
return (int(judges[x]) == 0)? e1 / e2 : 0;
}

最后在处理 vector<double> x,y 时若出现极限可能存在的情况,考虑其两侧的函数值,如果两者差异过大,则该处极限不存在,如果两者差异不大,则该处极限可以用两侧的点取平均得到近似解,写成数学表达式有 \[ \begin{gathered} |\lim_{x\to x_0^+}f(x)-\lim_{x\to x_0^-}f(x)|<\mbox{tolerance} , \lim_{x\to x_0}f(x)\ \ \mbox{exists}\\ |\lim_{x\to x_0^+}f(x)-\lim_{x\to x_0^-}f(x)|\geq \mbox{tolerance} , \lim_{x\to x_0}f(x)\ \ \mbox{doesn't exist} \end{gathered} \]

相关处理函数如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
for (int i = 0;i < int(x.size()); i++){
judges[x[i]] = judge::isLegal;
y.push_back(this->getValue(x[i]));
//如果这个点存在极限,判定两侧的差值是否较大,如果不大则使用平均值
if (judges[x[i]] == judge::perhapsLim && 0 < i && i + 1 < int(y.size()) ){
judges[x[i + 1]] = judge::isLegal;
y.push_back(this->getValue(y[i + 1]));
if (fabs(y[i + 1] - y[i - 1]) <= 100 * tol){
y[i] = (y[i + 1] + y[i - 1]) / 2;
judges[x[i]] = judge::isLegal;
}else{
judges[x[i]] = judge::divisorZero;
cout << x[i] << " " << y[i + 1] << endl;
}
i += 1;
}
}

处理之后得到的 \(\mbox{x,y}\) 两列表还需要单独针对极坐标系进行计算,相应计算公式如下: \[ \begin{cases} x=\rho \cos \theta\\ y=\rho \sin \theta \end{cases} \] 针对两个继承的类,使用函数重载对两种情况进行处理,在 \(\mbox{mainwindow.h/cpp}\) 中通过查找是否存在 "θ" 字符串判定需要定义的类,该类中的运算方法都继承自 class Expression复用相同的使用正则表达式计算对应的函数值。

迭代器

在处理一些冗余点时,使用 iterator 迭代器以及 \(\mbox{C++}\) 新特性对 \(\mbox{vector}\) 中的元素进行迭代, \(\mbox{wiki}\) 中对 \(\mbox{iterator}\) 的解释如下:

In computer programming, an iterator is an object that enables a programmer to traverse a container, particularly lists.

\(\mbox{C++}\) 语言在其标准库中广泛使用了迭代器,并描述了几类选代器,它们所允许的操作不同。这些包括前向选代器、双向选代器和随机访问选代器,在 \(\mbox{C++}\) 语言中还支持自定义迭代器,更加方便构建适用的类,例如机器学习中对数据预处理就可以使用迭代器的方式进行处理( 依照 \(\mbox{epoch,batch}\) 的大小分批次处理数据),这样可以在调用接口处使用 \(\mbox{auto}\) 语句更加简洁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class DataLoader{
private:
//乱序编码位置
vector<int> _rand_order;
//每个 batch 的最大长度,总组数
int batch_size = 0, endIndex = 0;
//所有句子编码(顺序排列)
vector<vector<int>> _data;
public:
//三维数组
vector<vector<vector<int>>> ans;
// 迭代器,取出 ans 对应下标结果
struct Iterator
{
//标签
using iterator_category = std::forward_iterator_tag;
using difference_type = std::ptrdiff_t;
using value_type = vector<vector<int>>;
using reference = value_type&; // 迭代器所指向的变量的引用类型
using pointer = value_type*; // 迭代器所指向的变量的指针类型
private:
pointer m_ptr;
public:
Iterator(pointer ptr) : m_ptr(ptr) {}
reference operator*() const { return *m_ptr; }
pointer operator->() { return m_ptr; }
//前缀 ++
Iterator& operator++() { m_ptr++; return *this; } // ++i
//后缀 ++
Iterator operator++(int) { Iterator tmp = *this; ++(*this); return tmp; }
};
//迭代器开始
Iterator begin() {
return Iterator(&ans[0]); // 返回迭代器
}
//迭代器结束
Iterator end() {
return Iterator(&ans[endIndex]);
}
};

auto batch : D 等同于 iteractor it = D.begin(); it < D.end();it++, batch = *it

1
2
3
4
for (auto batch : D){
string str = matrix_unicode(batch);
cout<< str <<endl;
}

由此可见,将迭代器和 \(\mbox{auto}\) 关键字可以简化代码的编写,对于本次函数绘图器而言,若出现一些需要去除的点(已经极限修复的点不算),则将 \(\mbox{vetcor<double> x,y}\) 中对应的 \((x,y)\) 均删除,实现代码如下:

1
2
3
4
5
6
7
8
for(auto i = x.begin(), j = y.begin(); i != x.end() && j != y.end(); i++, j++){
//如果这个点为特殊点,将其删除
if(int(judges[*i]) != 0){
// temp.erase返回迭代器,将不合法的元素全部删除
i = x.erase(i);
j = y.erase(j);
}
}

使用两个迭代器同向遍历,遇到出现问题的点就自动对应删除,达到删除不合理点的目的,例如 \(f(x)=\dfrac{1}{x}\)\(x=0\) 的点,在处理时便自动删除该间断点。

后端图像处理

在编写函数绘图器的过程中,曾经使用过与 \(\mbox{Qt 4.0}\) 版本兼容的排版库 \(\mbox{miktex}\),但由于当前 \(\mbox{Qt}\) 版本为 \(6.2.4\) 无法和该排版库兼容,另一方面,\(\mbox{miktex}\) 在数学公式渲染方面有一定缺陷,需要更换思路。

进而想到 \(\mbox{Qt}\) 有相应的网络包,可以通过给定的 \(\mbox{URL}\) 网址获取对应的图片,而 \(\mbox{latex.codecogs.com}\) 官网提供可以直接获取 \(\LaTeX\) 排版之后的图片,经过清晰化调整之后在以下网址 \[ \mbox{https://latex.codecogs.com/png.latex?\%5Cdpi\%7B300\%7D\%20\%5Cbg\_white\%20\%5Chuge\%20} \] 后面添加正确的 \(\LaTeX\) 公式(如 \(\mbox{y=\\dfrac\{\\sin x\}\{x}\small\wedge\normalsize \mbox{2+1\}}\)),便可以获取对应图片,相应的 \(y=\dfrac{\sin x}{x^2+1}\) 在该网址中便可得到以下高清图片:

将一般表达式转换到 \(\LaTeX\) 表达式单独列封装成一个类 \(\mbox{latexImg.h/cpp}\),针对各种表达式进行替换,如:

1
2
3
vector<pair<string, string>> replaceLatex = {{"θ","\\theta"}, {"π", "\\pi"}, {"(", "("}, {")", ")"}, {")*(", ") \\cdot ("}, {")(", ") \\cdot ("}, {"PI", " \\pi "}, {"Pi", " \\pi "}, {"pi", " \\pi "}, {"x*(","x \\cdot ("}, {"x(","x \\cdot ("}, {")*x", ") \\cdot x"}, {")x",") \\cdot x"}, {"sin", " \\Sin "}, {"cos", " \\Cos "}, {"tan", " \\Tan "}, {"ln", " \\Ln "}, {"*", " \\cdot "}};
for (auto pr : solveRepeations)
latexStr = replace_all(latexStr, pr.first, pr.second);

使用 \(\mbox{pair}\) 数据结构以及 \(\mbox{auto}\) 的使用,对成对的字符串进行替换,再编写 removeRedundant() 函数专门处理和分式相关的字符串,例如将 \(\mbox{1/x}\) 转换为 \(\mbox{\\dfrac\{1\}\{x\}}\) 依此类推

有关网络请求获取图片的代码如下,编写在 \(\mbox{mainwindow.cpp}\) 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void MainWindow::getImg(){
//在函数里面定义一个类的实例对象,需要在前面加上关键词 class
//使用 latexImg 对象获取对应表达式的 latex 图片
class latexImg latex{this->exp.toStdString(), isRec};
string latexURL = latex.getURL();
//构造 QNetworkAccessManage 实例模拟获取网页的过程
QUrl url(QString::fromStdString(latexURL));
QNetworkAccessManager manager;
QEventLoop loop;
QNetworkReply *reply = manager.get(QNetworkRequest(url));
//请求结束并下载完成后,退出子事件循环
QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
//开启子事件循环
loop.exec();
QByteArray jpegData = reply->readAll();
//将获取图片存储在 mainwindow 中
latexImg.loadFromData(jpegData);
}

在调试过程中发现,对于类中的方法,如果函数里面定义一个类的实例对象,需要在前面加上关键词 class,否则编译不通过

前端展示

对于主窗口,使用 \(\mbox{Qt}\) 中的设计师界面添加相应的按钮以及修改相应的初始值,设计相应的前端框架界面

对于从后端拿到的数据,分别进行等比例缩放以及在合适的位置处增加坐标轴和标度,从数学的角度来说,该步骤是将数据所在的线性空间线性且合适地映射到画布所在的线性空间。注意绘图原点在左上角,\(x\) 方向向右,\(y\) 方向向下,绘制的坐标轴处的箭头需要一定的偏置,本绘图器采用 \(45\degree\)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
   // x 放大倍数
int zoomOutX = this->width() * 0.9 / width;
// y 放大倍数
int zoomOutY = this->height() * 0.9 / height;
//放大倍数,取较小者
int zoomOut = zoomOutX < zoomOutY ? zoomOutX : zoomOutY;
//原点 x
double originX = offsetX * zoomOut + this->width() * 0.05;
//原点 y
double originY = offsetY * zoomOut + this->height() * 0.05;
minX = (minX + offsetX) * zoomOut + this->width() * 0.05;
maxX = (maxX + offsetX) * zoomOut + this->width() * 0.05;
minY = (minY + offsetY) * zoomOut + this->height() * 0.05;
maxY = (maxY + offsetY) * zoomOut + this->height() * 0.05;
// x 轴
QLine xLine(minX, originY, maxX, originY);
qPainter.drawLine(xLine);
// y 轴
QLine yLine(originX, minY, originX, maxY);
qPainter.drawLine(yLine);
//绘制原点、x轴、y轴处的字符
qPainter.drawText(QPointF(originX + 2, originY - 2), "O");
qPainter.drawText(QPointF(maxX, originY - 7), "x");
qPainter.drawText(QPointF(originX + 7, minY), "y");
//绘制箭头,45°方向绘制箭头
QLineF *arrows = new QLineF[4]{{maxX, originY, maxX - 5, originY - 5},
{maxX, originY, maxX - 5, originY + 5},
{originX, minY, originX - 5, minY + 5},
{originX, minY, originX + 5, minY + 5}};

而绘制刻度时,原点需要作为一个刻度,\(y\) 轴需要正负号分别计算:

1
2
3
4
5
6
7
8
9
10
11
12
for (int i = 1;originY + i * hSpacer <= maxScreenY;i++){
int screenY = originY + i * hSpacer;
qPainter.drawLine(originX, screenY, originX + 3, screenY);
double realY = (screenY - this->height() * 0.05) / static_cast<double>(zoomOut) - offsetY;
qPainter.drawText(QPoint(originX + 13, screenY + 5), QString::number(-realY, 'f', 2));
}
for (int i = 1;originY - i * hSpacer >= minScreenY;i++){
int screenY = originY - i * hSpacer;
qPainter.drawLine(originX, screenY, originX + 3, screenY);
double realY = (screenY - this->height() * 0.05) / static_cast<double>(zoomOut) - offsetY;
qPainter.drawText(QPoint(originX + 13, screenY + 5), QString::number(-realY, 'f', 2));
}

同理,对于从后端拿到的图像,进行背景颜色的替换再通过计算当前画大小得到相应的右上角位置,并最后设置画笔为蓝色,调用 \(\mbox{Qt}\) 中的 qPainter.drawPolyline(points, size) 函数平滑连接各点,得到相应的图像

1
2
3
4
5
6
7
8
9
   //获取当前画布宽度和高度
double imgWidth = originImg.width();
double imgHeight = originImg.height();
//比较得到图片的高度
int sizeImg = MIN(60, this->width() / 1.2 / imgWidth * imgHeight);
//将图片放置在合适的位置
qPainter.drawPixmap(this->width() - sizeImg * 1.2 * imgWidth / imgHeight, this->height() * 0.1, sizeImg * imgWidth / imgHeight, sizeImg, changeBackGroundColor(originImg));
qPainter.setPen(QColor(0, 0, 255));
qPainter.drawPolyline(points, size);

高精度计时

关于这部分,可以访问本人博客有关 \(\mbox{OOP}\) 第二次小作业报告中的探讨,网址如下面向对象编程第二次作业

大致为调用相关 \(\mbox{API}\) ,通过所在电脑的频率和计数器高精度计算相关时间,并将时间显示在图像标题处

单元测试

表达式函数

由于 \(\mbox{Qt}\) 相关的 \(\mbox{ui}\) 界面在编写过程中已经写好,对于该表达式函数,使用 \(\mbox{Visual studio}\) 单独定义该类并进行测试,通过的测试样例如下(其中大致可以分为几个部分):

单元测试分类 实例
乘法含负号 Expression e("2*(-2)");
除法含负号 Expression e("-1/1");
加减(含括号) Expression e("(-1+1)*(-1-1)");
加乘(含括号) Expression e("(1+2)*(2+3)");
加减乘(含括号) Expression e("(1+1)*(1-1)");
加乘(含括号和负号) Expression e{ "-(-(2*4-1*3))",1};
加减和幂次 Expression e("-1^3+3*1");
加减除幂次 Expression e("(x^2-1)/(x+1)");
三角函数(含负号) Expression e("sin(-1)"); Expression e("tan(-1000)");

调取之后输出相应的结果并口算进行验证,结果符合预测,验证代码如下,在表达式分段处加入 cout << Exp 等语句调试上述正则表达式相关过程,进行 \(\mbox{Debug}\)

1
2
cout << "The result is " << e.getValue(0) << endl;
cout << "Judge is " << e.judges[double(0)] << endl;

\(\mbox{Visual Studio}\) 中单元测试代码截屏如下:

处理 \(\LaTeX\) 公式

在中后期编写处理 \(\LaTeX\) 公式进行单元测试,采用 \(\mbox{wsl/Linux}\) 子系统控制台终端进行测试,摆脱大型 \(\mbox{IDE}\) 的束缚,让代码输入输出全部都在 \(\mbox{powershell}\) 中进行,能够达到快速 \(\mbox{Debug}\) 的效果,截图如下:

其中可以在控制台中使用重定向 > 运算符将输入输出结果保存到相应的 \(\mbox{txt}\) 文件中,测试通过的样例如下:

1
2
3
4
5
   1/x \dfrac{1}{x}
1/x+2/x \dfrac{1}{x}+\dfrac{2}{x}
(1+x)/x \dfrac{1+x}{x}
1/(x+1) \dfrac{1}{x+1}
(x+2)/(x+1) \dfrac{x+2}{x+1}

对于多重分式需要括号匹配,该公式转换会出现一些问题。但是,在函数绘制中很少出现这种情形,如 \(\dfrac{1+\dfrac{1}{x}}{x}=\dfrac{x+1}{x^2}\) 总可以通过手工化简的方式简化为只需要一个分号的形式,不需要考虑多重分式的情况。

从而该单元测试函数的测试至此为止,否则会极大影响开发进度

总结与反思

  • 该函数绘图器中正则表达式遍历查找的算法时间消耗比较大,对一般的函数基本消耗时间 \(\in[1s,2s]\) ,由于数据结构相关课程没有学习,猜测一些树的结构能够加快运算速度

  • 本次函数绘图器开发周期至少整整 \(7\) 天,从最开始的手足无措,到后面一点点列出需求文档不断进行实现和填充,在需求和实现之间追求高效率地开发,之前编写的函数绘图器需求文档截图如下:

  • 从上图中可以发现有些功能仍然没有实现,由于时间原因只能先搁置一段时间。但这种 \(\mbox{to-do-list}\) 的方式可以很大幅度提高大作业代码编写的效率

  • 单元测试在一个功能模块完成后便可以进行,针对不同的情况逐一进行测验,遇到相应的问题马上进行调整和修复,否则各种网状结构连接一起运行时,不容易定位 \(\mbox{bug}\)

  • 一些难以解决的问题可以考虑另辟蹊径(如使用发送网络请求的方式获取 \(\LaTeX\) 图片)

  • \(\mbox{C++}\) 新特性的使用可以省去了很多繁琐的语法表达式,加快代码的编写效率,虽然和 \(\mbox{pythonic}\)\(\mbox{python}\) 语言有一定差距,但毕竟前者更接触底层,效率较高(当然对一些特定问题也未必),两者各有千秋

  • \(\mbox{Qt}\) 的熟悉以及使用虽然让我踩过不少坑,不过,对报错信息的理解以及在各种网络资源的帮助之下,逐渐排开各种“雷”


Functional plotter
https://lr-tsinghua11.github.io/2022/05/25/%E7%BC%96%E7%A8%8B/%E5%87%BD%E6%95%B0%E7%BB%98%E5%9B%BE%E5%99%A8%E5%A4%A7%E4%BD%9C%E4%B8%9A%E6%8A%A5%E5%91%8A/
作者
Learning_rate
发布于
2022年5月25日
许可协议