软件工程基础结队项目——四则运算器生成及扩展
1.Github项目链接及队友博客链接
GitHub项目链接:
https://github.com/feimo49/four-operations
队友博客链接:https://blog.youkuaiyun.com/qq_37745978/article/details/86308941
2.填写表格中的预计时间
PSP2.1 | Personal Software Process Stages | 预估耗时 (分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | |
Estimate | 估计这个任务需要多少时间 | 30 | |
Development | 开发 | 1930 | |
Analysis | 需求分析(包括学习新技术) | 90 | |
Design Spec | 生成设计文档 | 60 | |
Design Review | 设计复审(和同事审核设计文档) | 30 | |
Coding Standard | 代码规范(为目前的开发制定合适的规范) | 30 | |
Design | 具体设计 | 120 | |
Coding | 具体编码 | 1200 | |
Code Review | 代码复审 | 60 | |
Test | 测试(自我测试,修改代码,提交修改) | 240 | |
Reporting | 报告 | 190 | |
Test Report | 测试报告 | 120 | |
Size Measurement | 计算工作量 | 10 | |
Postmortem & Process Improvement Plan | 事后总结,并提出过程改进计划 | 60 | |
Total | 合计 | 2150 |
3.解题思路描述
本次项目的核心算法分为生成四则运算题目和求解答案两个部分。下面分别对这两个部分的设计思路进行描述。
生成题目模块
生成题目模块包括生成随机的算式长度、生成随机的运算符以及生成随机个数的运算数。在算式生成之后,还需要进行括号匹配的检测以及对算式格式的调整。
其具体流程为首先生成一个随机的算式长度,其长度不超过10。之后逐个字符地生成该算式。当当前字符不是算式的最后一个字符时,生成一个随机数存入当前字符数组中,并在生成算式的过程中调整其格式,填入随机生成运算符,同时检测并避免乘方个数过多、乘方后数字过大以及分数分母为0等缺陷情况,同时,为方便同时对整数以及分数进行运算,在程序中设计num类同时表示两种数据类型,设计symbol变量对其进行标记区分,并在该类中设计对分数的化简机制;当当前字符是算式的最后一个字符时,主要流程与前述过程相同,增加了强制匹配右括号的情况。
求解题目模块
在求解题目模块中,主要的实现思想为“堆栈”。将表达式转换为后缀表达式后,通过判断将每一个字符存入操作数栈或操作符栈,存入后运算时分别按规则弹出两个栈中的字符即可。
运算符的优先级定义如下:(注:优先级数字越小优先级越高)
\ | 运算符优先级 |
---|---|
乘方、左括号 | 1 |
乘号、除号 | 2 |
加号、减号 | 3 |
右括号 | 4 |
在实际运算中,为随机生成方便起见,将所有的运算符对应于整型数字,其中101-104分别代表+,-,*,/,105为乘方,106为左括号,107为右括号。将算符、算数均存入堆栈后,直至操作符栈为空时,操作数栈的栈顶数字即为求解结果。
4.设计实现过程
需求分析
四则运算器的生成项目具体的需求可以分为如下三个阶段
-
第1阶段
一次性生成1000个不重复的四则运算式至txt文本文件中,通过命令行:可执行文件名.exe -i n(n为需要生成的题目数量)实现。
例:four_op.exe -i 1000
生成的四则运算式中最多有10个运算符,括号的数量不作限制。同时,除了整数之外,还要支持真分数的操作。在用户给出当前随机生成算式的答案后,程序能给出正误判断,并在用户完成全部题目之后给出其错误率统计。 -
第2阶段
增加可以支持乘方运算的运算符,其中乘方运算符的优先级最高,并且可以有两种表示方法:^/ **,用户可以通过在生成算式前的模式选择决定乘方算符的表示方式。 -
第3阶段
为程序设计一个基于Windows窗体程序的GUI界面,增加“倒计时”使得每个题目必须在20秒内完成,如果完不成则计0分进入下一题;同时增加历史记录功能记录用户答过的题及其正确答案。
在该阶段中本项目使用C#语言进行实现。
程序流程图说明
类和模块说明
-
Main.cpp
Main模块为该项目的主模块,用户在该模块中进行输入命令解析、参数设定以及模式选择,同时对于不合法的输入进行报错,并实现与输出模块之间的接口。 -
GenerateExp.cpp
GenerateExp模块为随机生成表达式模块。包含函数BuildExp()和PrintExp(),其中BuildExp()函数随机生成四则运算题目,将其对应整型符号存入整型数组中;PrintExp()函数将整型算式表达转换为字符型存入字符型数组中并将算式打印,同时返回字符型数组至主函数进行文件输出。 -
Num类
Num类模块中定义了 分子、分母、最大公约数、标记符号Symbol以及化简标志。同时在该模块中实现了各类算符的重载。 -
Solver.cpp
Solver模块为求解四则运算题目的模块,其主要函数为get_ans()函数,实现对算式操作数、操作符的入栈、出栈操作,以根据运算规则实现对算式的求解。 -
Judge.cpp
Judge模块为判断正误模块。该模块的主要函数为judge()函数和Check()函数,其中judge()函数实现用户输入答案与正确答案的比对并将正误信息反馈给用户,Check()函数用作检验用户输入答案的格式是否正确。
函数流程图说明
5.单元测试
- 单元测试用例设计如下
- 输入测试主要测试程序的合法输入以及不合法输入情况,保证程序的安全性,其设计如下:
编号 | 输入格式 | 预期输出 |
---|---|---|
1 | -i 10 | 正常处理,随机生成10个算式 |
2 | Please input TWO parameters! | |
3 | -i | Please input TWO parameters |
4 | -c 5 | Please input in the correct form! |
5 | -i abc | Please input a NUMBER! |
- 算式运算符测试,检测四则运算器中每一种算符的运算正确性,该层检测正确才可以进行进一步的程序分析,其中对于^运算符分别进行正常整型运算、幂指数为0以及底数为分数三种情况讨论,其设计如下:
编号 | 操作数1 | 操作数2 | 运算符 | 预期结果 |
---|---|---|---|---|
1 | 1 | 1/2 | + | 3/2 |
2 | 1 | 1/2 | - | 1/2 |
3 | 3 | 1/2 | * | 3/2 |
4 | 3 | 1/2 | / | 6 |
5 | 3 | 2 | ^ | 9 |
6 | 3 | 0 | ^ | 1 |
7 | 1/2 | 1 | ^ | 1/2 |
- 题目查重测试
在四则运算中,由于存在加法交换律、乘法交换律以及左右结合律,故算式之间存在形式不同但逻辑运算相同的情况,在本项目中需要对该类情况进行测试,其设计如下:
编号 | 算式1 | 算式2 | 预期结果 |
---|---|---|---|
1 | 1+2+3 | 3+(1+2) | 重复 |
2 | 1+2+3 | 3+2+1 | 不重复 |
3 | 3 * 4 | 4 * 3 | 重复 |
4 | 1 * 2 * 3 | 3 * 2 * 1 | 不重复 |
5 | (1+2)* 3 | 3 *(1+2) | 不重复 |
6 | 1 ** 2 ** 3 | 3 ** 2 ** 1 | 不重复 |
7 | (1+2)*(3+4) | (3+4)*(1+2) | 不重复 |
8 | (2-1)/(5-3) | (5-3)/(2-1) | 不重复 |
9 | (3+6)/(5-3) | (6+3)/(5-3) | 重复 |
10 | (1/2+2/3)+3/4 | 1/2+(2/3+3/4) | 重复 |
11 | (1/2+2/3)* 3/4 | 3/4 *(1/2+2/3) | 重复 |
6.程序性能分析及改进
在性能分析阶段,由于在诊断过程中需要不断与用户进行交互,故其诊断会话时间较长,达到了1分钟20秒。
由分析报告可见,在程序中主函数main()占用的CPU比例最大,其主要原因是在主函数中进行了文件的打开和读操作,占用CPU比例较大;将用户输入答案与正确结果进行比较的judge()函数其次,在其中进行对于用户输入答案的比对与反馈工作,需要与用户界面进行交互;打印当前生成的算式的PrintExp()函数再次之,其实现打印算式的同时还需要将生成的算式写入文件ques.txt中,需要消耗较多的CPU资源。这三个函数为项目中消耗CPU最多的三个主要函数。
7.代码说明
主函数说明
在主函数中实现了输入的解析与判断、参数设定以及主要的生成、求解、输出接口设计等,是该项目最为核心的函数:
int main(int argc, char * argv[])
{
if (argc < 3)
{
printf("Please input TWO parameters!\n");
system("pause");
return 0;
}
if (!strcmp(argv[1], "-i"))
{
srand((unsigned)time(NULL));
int n = atoi(argv[2]);
cout << "您希望使用哪种方式表示乘方?(输入1选择模式1,输入2选择模式2)mode-1:^/mode-2:**" << endl;
int m;
cin >>m;
getchar();
ofstream OutputFile("ques.txt");
int ac = 0;
for (int i = 0; i < n; i++)
{
char *t;
num useranswer;
int * save = BuildExp(3);
t = PrintExp(m);
OutputFile << t;
if(judge(get_ans(save)))
ac++;
}
printf("本轮题目正确率:%d/%d\n", ac, n);
}
if (strcmp(argv[1], "-i")) //输入格式不合理的情况
{
printf("Please input in the correct form!\n");
system("pause");
return 0;
}
int flag = 0;
for (int i = 0; i < strlen(argv[2]);i++)
{
if (argv[2][i]<'0' || argv[2][i]>'9')
flag = 1;
}
if (flag == 1) //输入非数字的情况
printf("Please input a NUMBER!\n");
system("pause");
return 0;
}
Num类
Num类定义了算式中数字的数据类型表示:
class num
{
private:
int numerator; //分子
int denominator; //分母
int gcd; //最大公约数
int symbol; //运算符
int flag; //设置化简标志,防止重复化简
void get_gcd(int x, int y) //求最大公约数
{
if (y == 0)
gcd = x;
else
get_gcd(y, x%y);
}
void reduction() //化简
{
if (numerator != 0) //分子不为0
{
symbol = symbol * (numerator / abs(numerator))*(denominator / abs(denominator));
numerator = abs(numerator);
denominator = abs(denominator);
get_gcd(numerator, denominator);
}
else //分子为0
{
denominator = 1;
gcd = 1;
symbol = 1;
}
flag = 1;
}
public:
num();
num(int x);
num(int x, int y, int sign);
void print();
void print(char * formula, ofstream & outtofile);
friend num operator +(num &a, num &b);
friend num operator -(num &a, num &b);
friend num operator *(num &a, num &b);
friend num operator /(num &a, num &b);
friend num operator ^(num &a, num &b); //保证b的分母为1
friend int operator == (num &a, num &b);
};
随机生成算式
随机生成指定个数的四则运算式代码如下:
//随机化设计
default_random_engine generator(time(NULL));
normal_distribution<double> lendis(5, 3);
normal_distribution<double> numdis(5, 2);
auto lendice = bind(lendis, generator);
auto numdice = bind(numdis, generator);
int Exp[50];
int p = 0;
//mode=1 基础,mode=2 包含分数,mode=3,包含乘方。
//随机生成式子长度
int RandExpLen()
{
int randnum = lround(lendice());
if (randnum < 2)
randnum = 2;
else if (randnum > 10)
randnum = 10;
return randnum;
}
//随机生成算符
int RandSymbol(int mode)
{
int randnum;
if (mode == 1)
randnum = rand() % 4 + 101;
else if (mode == 2)
randnum = rand() % 4 + 101;
else if (mode == 3)
randnum = rand() % 5 + 101;
else if (mode == 4)
randnum = rand() % 2;
return randnum;
}
//随机生成式子中数字个数
int RandExpNum(int maxnum)
{
int randnum = lround(numdice());
if (randnum < 0)
randnum = 0;
else if (randnum > maxnum)
randnum = maxnum;
return randnum;
}
//随机生成一个1~3的数字
int GetEasy()
{
return rand() % 3 + 1;
}
//生成算式
int* BuildExp(int mode)
{
memset(Exp, 0, sizeof(Exp));
bool HavePow = false;
int expnum = RandExpLen();
int lastbracket = 0;
p = 0;
for (int j = 1; j <= expnum; j++)
{
if (j == expnum)//最后一个数字的判断
{
Exp[p++] = RandExpNum(10);
if (Exp[p - 2] == 105)
Exp[p - 1] = GetEasy(); //返回一个1~3的数
if (Exp[p - 2] == 104 && Exp[p - 1] == 0)//判断分母0
Exp[p - 1] = 1;
if (lastbracket != 0)//若有未匹配左括号,则最后一位强制添加右括号
Exp[p++] = 107;
break;
}
else
{
Exp[p++] = RandExpNum(10);//生成随机数
if (Exp[p - 2] == 105)
Exp[p - 1] = GetEasy();
if (Exp[p - 2] == 104 && Exp[p - 1] == 0)//判断分母0
Exp[p - 1] = 1;
if (RandSymbol(4) && lastbracket > 2)//右括号
{
Exp[p++] = 107;
lastbracket = 0;
}
Exp[p++] = RandSymbol(mode);//生成随机符号
{//检查乘方个数
if (Exp[p - 1] == 105 && HavePow)
Exp[p - 1] = RandSymbol(1);
else if (Exp[p - 1] == 105)
HavePow = true;
}
if (RandSymbol(4) && j < expnum - 1 && lastbracket == 0 && Exp[p - 1] < 104)//左括号
{
Exp[p++] = 106;
lastbracket = 1;
}
}
if (lastbracket != 0)
lastbracket++;
}
return Exp;
}
求解四则运算式
求解算式应用堆栈方法进行算式答案的求解,具体说明在设计实现过程的类与模块说明中进行描述:
extern int p;
//运算符和可处理十进制数之间的转换
num cal(num n1, num n2, int opera)
{
if (opera == 101)
return n1 + n2;
else if (opera == 102)
return n1 - n2;
else if (opera == 103)
return n1 * n2;
else if (opera == 104)
return n1 / n2;
else if (opera == 105)
return n1 ^ n2;
}
//将四则运算映射到一串十进制数,0-100为运算数
//其中101-104分别代表+,-,*,/,105为乘方,106为左括号,107为右括号
num get_ans(int * operation)
{
stack <int> operators;
stack <num> operand;
for (int i = 0; i < p; i++)
{
if (operation[i] >= 0 && operation[i] <= 100)
{
num temp(operation[i]);
operand.push(temp);
}
else if (operation[i] == 105 || operation[i] == 106) //左括号与乘方必定入栈
operators.push(operation[i]);
else if (operation[i] == 103 || operation[i] == 104) //乘除会弹出乘方与乘除
{
while (!operators.empty() && (operators.top() == 103 || operators.top() == 104 || operators.top() == 105))
{
int opera = operators.top();
operators.pop();
num n1 = operand.top();
operand.pop();
num n2 = operand.top();
operand.pop();
operand.push(cal(n2, n1, opera));
}
operators.push(operation[i]);
}
else if (operation[i] == 101 || operation[i] == 102) //加减可能弹出乘除与乘方
{
while (!operators.empty() && (operators.top() != 106 && operators.top() != 107))
{
int opera = operators.top();
operators.pop();
num n1 = operand.top();
operand.pop();
num n2 = operand.top();
operand.pop();
operand.push(cal(n2, n1, opera));
}
operators.push(operation[i]);
}
else if (operation[i] == 107) //右括号会一直弹出直至左括号
{
while (operators.top() != 106)
{
int opera = operators.top();
operators.pop();
num n1 = operand.top();
operand.pop();
num n2 = operand.top();
operand.pop();
operand.push(cal(n2, n1, opera));
}
operators.pop();
}
}
while (!operators.empty())
{
int opera = operators.top();
operators.pop();
num n1 = operand.top();
operand.pop();
num n2 = operand.top();
operand.pop();
operand.push(cal(n2, n1, opera));
}
return operand.top();
}
8.程序扩展:GUI界面生成
GUI界面设计显示
在该项目中,我们小组选择了第一个扩展方向,即使用C#语言生成一个基于Windows窗体程序的GUI界面。
主界面呈现:
在主界面中点击START按钮进入答题界面,该按钮实现了倒计时、成绩记录、题目以及答题文本框显示等功能,同时进行算式的居中设计。
答题界面呈现:
在答题界面中,左上角实现倒计时功能,每一道题有20s的答题时间,右上角实现成绩记录功能,每答对一道题,Grade++,否则Grade数值不变。在文本框中输入当前显示算式的答案,点击SUBMIT按钮进行提交。如果回答正确,则弹出消息框 “Bingo!” 如下:
如果回答错误,则弹出消息框 “Wrong!” ,并在下方显示该道题目的正确答案,如下:
如果答题时间超过了20秒,则弹出超时消息框 “Time Up!” ,并重新开始下一道题:
在答题过程中,随时可以点击QUIT按钮退出应用程序,也可以点击HISTORY按钮查看历史记录,得以看到自己答过的题目及其正确答案,以及当前的正确率统计。
在历史记录窗体中可以点击BACK按钮返回答题界面。
GUI界面代码设计
使用Windows窗体项目实现GUI界面的设计。首先将之前撰写的C++项目代码转换为C#语言在该项目中进行重写。在重写过程中需要考虑语法的修改以及GUI中添加的相应功能的正确表述。
在将C++项目中的功能进行成功移接后,进行其与窗体界面的结合,主要在START按钮和SUBMIT按钮中实现窗体程序的功能。
START按钮实现如下:
private void Start_Click(object sender, EventArgs e)
{
if (IsFirstRound)
{
Ans.Visible = true; //点击开始答题按钮后显示答题文本框
Start.Visible = false;
label2.Visible = false;
Submit.Visible = true;
History.Visible = true;
ques.Visible = true;
Timer.Visible = true;
ggrade.Visible = true;
Quit.Visible = true;
ggrade.Text = "Grade: " + grade.ToString();
timer1.Start();
}
Ans.Focus();
for (int i = 0; i < 1; i++)
{
save = Generate.BuildExp(3);
Generate.PrintExp();
cnt++;
ques.Text = Generate.strsave;
int len = ques.Width;
int flen = this.Width;
int x = (flen - len) / 2;
int y = 100;
ques.Location = new Point(x, y);
}
}
SUBMIT按钮实现如下:
private void Submit_Click(object sender, EventArgs e)
{
if(Ans.Text=="")
{
Ans.Focus();
return;
}
Judge ans = new Judge();
Num correct_ans = solve.get_ans(save, Generate.p);
int ansflag = ans.judge(correct_ans, this.Ans.Text);
correct_ans_str = correct_ans.c_Tostring();
timu = Generate.C_Tostring();
f3.History_Add(timu, correct_ans_str);
if (ansflag==1)
{
timer1.Stop();
MessageBox.Show("Bingo!");
timer1.Start();
grade+=1;
correct_cnt++;
ggrade.Text = "Grade: "+grade.ToString();
Start_Click(null, null);
this.Ans.Text = "";
totaltime = 20;
}
else if(ansflag==0)
{
timer1.Stop();
MessageBox.Show("Wrong!\n" + "Correct Answer:" + correct_ans_str);
timer1.Start();
Start_Click(null, null);
this.Ans.Text = "";
totaltime = 20;
}
else
{
timer1.Stop();
MessageBox.Show("Error:Please input the correct form!");
timer1.Start();
this.Ans.Text = "";
this.Ans.Focus();
}
f3.Correct_Rate(correct_cnt, cnt-1);
}
HISTORY按钮实现如下:
//打开历史记录窗口
private void History_Click(object sender, EventArgs e)
{
f3.Show();
}
//每次生成题目时通过f3.History_Add(timu, correct_ans_str),将题目和正确答案传入f3
public void History_Add(String Text1,String Text2)
{
record.Text += Text1 + "=" + Text2+"\r\n";
}
public void Correct_Rate(int c_cnt, int cnt)
{
correct_rate.Text = "Correct Rate : " + c_cnt + "/" + cnt;
}
9.填写表格中的实际时间
PSP2.1 | Personal Software Process Stages | 预估耗时 (分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 35 |
Estimate | 估计这个任务需要多少时间 | 30 | 35 |
Development | 开发 | 1930 | 2005 |
Analysis | 需求分析(包括学习新技术) | 90 | 60 |
Design Spec | 生成设计文档 | 60 | 60 |
Design Review | 设计复审(和同事审核设计文档) | 30 | 30 |
Coding Standard | 代码规范(为目前的开发制定合适的规范) | 30 | 15 |
Design | 具体设计 | 120 | 120 |
Coding | 具体编码 | 1200 | 1345 |
Code Review | 代码复审 | 60 | 75 |
Test | 测试(自我测试,修改代码,提交修改) | 240 | 300 |
Reporting | 报告 | 190 | 175 |
Test Report | 测试报告 | 120 | 100 |
Size Measurement | 计算工作量 | 10 | 15 |
Postmortem & Process Improvement Plan | 事后总结,并提出过程改进计划 | 60 | 60 |
Total | 合计 | 2150 | 2215 |
10. 实验总结
在本次进行结对项目的完成中,我的收获很多。我掌握了随机生成正确的四则运算式、求解四则运算式以及设计实现完善的GUI界面的方法。同时,我意识到了与队友协作的重要性。之前在进行个人项目的完成时不涉及合作的问题,但是在结对项目中如何与自己的队友进行分工合作显得至关重要。我的队友与我是很熟悉的朋友,因此我们这次的合作十分顺利~
除此之外,在GUI界面设计实现的过程中,我感觉到作为计算机专业的学生,也需要培养自己的设计思想与审美意识,不能只会实现后端的功能而忽视前端界面的美观设计。这次我们在如何设计美观的界面上花了很多的时间,做出了我们认为较为美观的操作界面,希望我们今后可以在这方面有所提高。