首先,附上所有代码:https://github.com/mwl0811/Arithmetic(内含GUI部分的可执行程序和代码)
队友博客地址:https://blog.csdn.net/wen_zihan/article/details/86479724
一、预计的PSP表格
PSP2.1 | Personal Software Progress Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 60 | 90 |
· Estimate | · 估计这个任务需要多少时间 | 20 | 30 |
Development | 开发 | 60 | 90 |
· Analysis | · 需求分析(包括学习新技术) | 180 | 150 |
· Design Spec | · 生成设计文档 | 120 | 90 |
·Design Review | · 设计复审(和同事审核设计文档) | 60 | 90 |
· Coding Standard | · 代码规范(为目前的开发制定合适的规范) | 120 | 90 |
· Design | · 具体设计 | 180 | 180 |
· Coding | · 具体编码 | 1500 | 1820 |
· Code Review | · 代码复审 | 300 | 240 |
· Test | · 测试(自我测试,修改代码,提交更改) | 300 | 300 |
Reporting | 报告 | 60 | 60 |
· Test Report | · 测试报告 | 60 | 60 |
· Size Measurement | · 计算工作量 | 30 | 20 |
· Postmortem & Process Improvement | · 事后总结,并提出过程改进计划 | 180 | 120 |
合计 | 3030 | 3430 |
二、解题思路
- 需求分析:
这一部分主要是分析要求,我总结出了如下的要求:
- 生成不重复的N道四则运算题目(含乘方)至文件(0<N<=1000)
- 计算出题目正确结果(结果不展示),并对输入结果(包含真分数)判断对错,最后给出正确和错误题数
- 读取文件内的数独问题,求解并将结果输出到文件(0<N<=1000000)
- GUI部分:
–设置倒计时功能,每道题20s内完成,没有完成计0分到下一题
–点击“历史记录”可以查看做题的历史记录,每道题是否正确
- 初期思路:
我们的程序在设计时,主要分成了两个部分,第一部分是生成题目部分,这一过程主要需要注意的是生成题目的格式(后文会仔细介绍),这一部分主要由我负责,后一过程是由队友负责的,对于生成的题目进行计算得出一个结果,并且保存。而我们避免生成重复题目的方法是所有计算式的结果不同,所以在生成题目时,就需要调用生成答案的函数,如果有重复的,再重新随机生成。最后的GUI就是主要调用我们已经生成好的程序,这些程序主要是队友对一些代码和读取方式进行调整来生成的,这一部分的图形界面代码主要由我来完成。
以下是整体的流程:
- 查阅资料以及方法确定:
- 生成四则运算题目:这一部分比较难的点在于需要生成的题目不重复,这一点比较麻烦,因为不重复的定义很细致,起初我是想逐条分析,避免不重复情况,但是后来看到有人写的博客中,选择的思想是:只要题目的答案不重复,题目一定不重复,所以在这里我们决定采用这种保险的方法。
- 解四则运算题目:这一部分我们选择的方法是比较实用的后缀表达式算法,存到栈中,然后进行计算,这一部分的难点在于对于乘方的计算,因为乘方是比较特殊的算法,优先级最高,所以乘方上花了一些时间。
三、设计实现及代码说明
我们的代码主要就是分为两个部分,generate.cpp和solve.cpp两个部分,为了更加清晰,我们设置了一个大类Operator.h,下面是两部分的流程:
- Arithmetic.cpp是主函数,主要是用于对命令行输入错误的判断,并给出提示显示如何正确输入:
if (flag == 1) //输入有误时
{
cout << "Please check your input!" << endl;
cout << "Just input like this: Arithmetic.exe N mode" << endl;
cout << "N means the number of questions, mode means the power." << endl;
cout << "mode=1:^ mode=2:**" << endl;
return 0;
}
注:命令行格式应该是Arithmetic.exe 生成题数 乘方模式
- generate.cpp用来随机生成四则运算题目,因为这个需要用到随机函数,随机函数是用来产生数字的,为了能做到随机产生运算符,我就把运算符存在了一个字符串中,随机函数取出第几个位置的字符
大致流程如下:
但是这一部分有很多细节需要注意,我总结出的注意点如下:
1.避免产生除数为0的情况
2.避免产生0^0的情况
3.避免结果过难计算或过大,对^后的数字限制在3以内
4.避免同一数字两边都有括号的情况:例如(10)
5.避免在同一部分两边加了多次括号
接下来是部分重要的代码:
----这一部分是产生随机括号的位置:
do
{
bracket1[j] = (rand() % (symbolnum - 1));
bracket2[j] = (rand() % (symbolnum - bracket1[j] - 1)) + bracket1[j] + 2;
} while (sym[bracket1[j] - 1] == '^' || sym[bracket1[j] - 1] == '/' || sym[bracket2[j]] == '^');
//乘方的情况下不加括号,防止过大
//除法后不加括号,防止除数为0的情况
----以下是删除重复括号的部分:
if (bracket2[m] == bracket2[n])
{
if (bracket1[k] != 20 && bracket2[n] != 20)
{
bracket1[k] = 20;
bracket2[n] = 20; //删除重复括号
}
}
注:bracket数组用来存储括号加的位置,bracket1[]存储前括号的位置,即第bracket1[]个数字的前面,同理,bracket2[]存储后括号的位置,即第bracket2[]个数字的后面。
- solve.cpp是用来求解题目过程,主要分为两个部分,先是将算式化成后缀表达式,存到栈中,然后对这个后缀表达式求解:
其中主要的函数就是计算函数,calculate如下:
//计算后缀表达式
void Operation::calculate(deque<char>& coll3, stack<double>& coll4)
{
double num = 0;
while (!coll3.empty())
{
int flag = 0;
char c = coll3.front();
coll3.pop_front();
char d = 0;
if (!coll3.empty())
{
d = coll3.front();
}
else
{
flag = 1;
}
//如果是操作数,压入栈中
if (c >= '0'&&c <= '9')
{
num = num * 10 + c - '0';
if ((d == ' '&&flag == 0) || flag == 1)
{
coll4.push(num);
num = 0;
if (flag == 0)
coll3.pop_front();
}
}
else //如果是操作符,从栈中弹出元素进行计算
{
double op1 = coll4.top();
coll4.pop();
double op2 = coll4.top();
coll4.pop();
switch (c)
{
case '+':
coll4.push(op2 + op1);
break;
case '-':
coll4.push(op2 - op1);
break;
case '*':
coll4.push(op2*op1);
break;
case '/':
coll4.push(op2 / op1);
break;
case '^':
coll4.push(pow(op2, op1));
break;
}
}
}
}
四、单元测试及代码说明
单元测试的部分
- 模式判断的测试:
方法:读取生成题目字符串,是否出现不对应字符
TEST_METHOD(TestMethod3) //检查输入模式1时是否使用的是"^"
{
argv[1] = "100";
argv[2] = "1";
int result = main(argc, argv);
assert(result == 3);
}
TEST_METHOD(TestMethod4) //检查输入模式2时是否使用的是“**”
{
argv[1] = "100";
argv[2] = "2";
int result = main(argc, argv);
assert(result == 4);
}
- 题目是否重复的测试:
对于题目的测试方法:检查输入重复的四则运算题目,结果是否提示重新生成。
TEST_METHOD(TestMethod5)//检查输入重复的四则运算题目,结果是否提示重新生成
{
Operation M;
string str = "3+(2+1)=";
string str2 = "1+2+3=";
int result1 = M.solve(str);
int result2 = M.solve(str2);
assert((result1!=result2)==1);
}
- 题目计算结果判断的测试:
我们主要给出了如下几个用例:
1.检查输入带括号的四则运算题目: 3+(2+1)=
2.检查输入带^四则运算题目: 3^(2+1)=
3.检查输入带**型的乘方的四则运算题目
4.检查输入复杂的四则运算题目: 3^(2+1)/3*2=
5.检查结果不是整数的运算: 3^(2+1)/4=
6.检查输入结果为真分数的运算
7.检查输入结果为负数的运算
下面是单元测试的结果,证明我们检查的部分都正确了:
- 覆盖率展示:
可以看出,测试代码的覆盖还是比较好的。
五、程序性能及质量分析
由于我们选择的方法是判断答案是否重复,所以并不浪费时间,下面是我们的性能分析:
这里可以看到,比较费时间的是Generate和solve部分,这也是整个代码的核心部分。
运行结果没有警告产生:
六、GUI
对于GUI的设计,我认为是这个项目中较为重要的部分,因为之前的程序是一个整体,不是很适用于GUI直接调用,所以很多函数需要单独生成一个程序便于GUI部分的调用,这里我们一共用到了三个程序:
- Generator_wm.exe:用于生成给定数目的不重复题目
- Solve.exe:用于计算输入的结果是否准确,并且记录到历史记录中
- Count_num.exe:用于最后给出整体的正误个数
接下来是关于代码的解释和成果展示:
- 第一个窗口:设置题目数量和乘方模式
重要代码:
private void button1_Click(object sender, EventArgs e)
{
string para = quenum + " " + mode;
System.Diagnostics.Process.Start("Generator_wm.exe", para).WaitForExit();
Form2 f2 = new Form2();
this.Hide();
f2.Show(); //调用做题窗口
}
注:quenum和mode都是从textbox中提取出来的结果。
- 第二个窗口:做题并反馈
重要代码:
void FillGrid()
{
ts = new TimeSpan(0, 0, 20); //设置时间最多20s
this.timer1.Enabled = true;
StreamReader str1 = new StreamReader(@"question.txt");
string quebefore;
for (int i = 0; i < count; i++)
{
quebefore = str1.ReadLine();
}
string que = str1.ReadLine();
if(que==null)
{
System.Diagnostics.Process.Start("Count_num.exe");
MessageBox.Show("no question left! Please quit!", "警告", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
label5.Text = que;
if (que != null)
{
count++;
}
label6.Text = count.ToString();
textBox3.Text = "Input your answer here";
label7.Text = ts.Seconds.ToString(); //显示还剩多长时间
}
注:以上是填空界面的函数。
private void button1_Click(object sender, EventArgs e)
{
string que = label5.Text;
string ans = textBox3.Text;
string para = que + " " + ans;
System.Diagnostics.Process.Start("Solve.exe", para).WaitForExit();
FillGrid();
}
注:以上是计算结果是否正确的部分,点击下一题同时计算前一题是否正确。
private void timer1_Tick(object sender, EventArgs e)
{
label7.Text = ts.Seconds.ToString();
ts = ts.Subtract(new TimeSpan(0, 0, 1)); //隔一秒
if(ts.TotalSeconds<0.0)
{
timer1.Enabled = false;
string que = label5.Text;
string ans = textBox3.Text;
string para = que + " " + ans;
System.Diagnostics.Process.Start("Solve.exe", para).WaitForExit();
MessageBox.Show("You have used out of the time!", "超时警告", MessageBoxButtons.OK, MessageBoxIcon.Error);
FillGrid();
}
}
注:以上是计时器部分的代码,用到了timer控件,这一部分是经过查阅资料学会的。
- 第三个窗口:显示历史记录
重要代码:
private void Form3_Load(object sender, EventArgs e)
{
StreamReader sr = new StreamReader("Judgements.txt", Encoding.Default);//将选中的文件在textBox2中显示
richTextBox1.Text = sr.ReadToEnd();
sr.Close();
}
注:以上是读取历史记录文件。
七、总结
这一次的项目,我学到了很多,有很大的收获,也有过一些不足之处,也是我们日后再开发软件可以用到的经验,下面是我通过这次经历学到的:
- 这次最不同的一点是合作项目,过程中我明白了一个项目的成功离不开配合,好在我和队友的配合十分默契。
- 我深刻体会到了软件开发过程中团队合作的重要性,多人的力量一个胜过一个人,也会使得结果更加周到全面。
- 这次的项目让我知道了软件前期设计的重要性,我们分配好了模块,目标十分清晰,这大大提高了我们的效率。
- 这次明确了代码规范的重要性,因为我和队友的模块有数据交互的过程,而代码的规范使得我们能十分清晰地明确对方需要什么。
- 在代码交接的过程中,我意识到了一定要保证已完成模块的正确性,再做交接,因为在交接合并过后,程序一旦出现问题,很难发现究竟是那一部分出现的错误。
- 但是我们在最后总结时,共同认为我们有一点没有考虑到的是,我们对于乘方的模式,在生成题目选择2模式时,我将^换成了**,然而队友在计算中又为了方便换了回去,这样就相当于重复做了无用功。我们总结造成这样的结果主要原因还是我们没有将模块再细化划分,造成了内部功能的重复,产生了代码坏味道。