目录
1.源代码的github链接:
2.PSP表格
3.题目要求
第一阶段:
第二阶段:
第三阶段:
4.解题思路
5.设计实现过程
第一二阶段:
第一部分:operation类的构建
第二部分:四则运算生成函数
第三部分:四则运算求解及判断函数
第四部分:main函数部分
在powershell上的运行结果:
单元测试部分
代码覆盖率部分
第三阶段:
6.性能分析
7.代码说明
四则运算题目生成部分:
求解函数部分:
判断用户输入结果部分:
GUI部分:
设置框的核心部分:
运算框的核心部分:
历史记录框的核心部分:
8.总结
1.期间遇到的问题
2.个人小结
https://github.com/mwl0811/Arithmetic
队友博客地址:https://blog.csdn.net/qq_41843511/article/details/86511573
personal software process stage |
预估耗时(分钟) | 实际耗时(分钟) | |
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 plan | 事后总结,并提出过程改进计划 | 180 | 120 |
合计 | 3030 | 3430 |
25 - 3 * 4 - 2 / 2 + 89 = ?
1/2 + 1/3 - 1/4 = ?
(5 - 4 ) * (3 +28) =?
首先第一阶段相对比较好实现,随机生成一千道题目,需要获取随机数以及随机运算符,并将其放在适当的位置,表达式求值部分稍微有点麻烦,但主要思路还是运用后缀表达式来进行运算,其中需要的辅助数据结构有栈,队列等。将随机生成的四则运算题目即中缀表达式利用堆栈转换成后缀表达式,再进行计算。在与用户输入交互部分,主要是对用户生成答案的判断,这里有一点就是要支持真分数的运算,以及小数,对于负数还要判定前面有没有负号。所以在跟正确答案的对比部分,必须要识别好用户所给出的答案到底是什么,主要是对小数点,‘/’,以及负号的识别处理。
第二阶段其实就是在第一阶段中首先生成四则运算题目部分中随机运算符产生部分加入‘^’,'**'这个可以在‘^’生成后将其替换。模式选择在命令行中实现,有模式一和模式二,模式一表示乘方用‘^’表示,模式二表示乘方用‘**’表示。在计算结果部分只要在运算部分加一个case:^就行,具体计算由pow()函数实现。
第三阶段我们选择的第一个任务也就是将其变成一个图形界面程序。这一部分就是使用前面生成的.exe可执行文件进行修改,分别变成三个.exe文件,第一个是题目生成的.exe,第二个是对用户输入结果判定的.exe,第三个计算正确题数和错误题数的.exe文件。最后用c#写成windows窗体界面,在其中调用我们所修改后形成的三个文件。
前两阶段中,我跟队友是分开实现题目生成和解题以及判别这两个部分的,最终合而为一拼起来。我负责的一部分是解题和判别答案部分,队友负责题目生成部分。在第一二阶段中,最后拼合好的程序主要封装在四部分中:
这一部分我将其封装在operation.h的头文件中,并命名一个名为Operation的类。类里面是关于四则运算项目所要实现的函数声明以及数独需要的私有变量。
公有函数:
私有变量:
这一部分我将其封装在Generate.cpp中,此部分主要是根据主函数传来的生成四则运算表达式题目的数目以及乘方的类型随机生成运算数和运算符,以及括号。为了防止最终得出的结果过大,我们对乘方的随机出现部位进行了限制,避免多个乘方的产生,并且还要避免除数为零的情况,以及0^0情况的产生,另外在生成括号时,括号需要成对出现,左括号和右括号要相匹配,但是避免不了最终生成的表达式中有重复括号的出现,这就需要对生成的表达式最终进行检验,去掉多余的括号。函数生成的四则运算式的质量直接决定了solve()函数能否成功调用。
这一部分我将其封装在Solve.cpp中,此部分是实现四则运算的求解以及对用户输入结果的判断。首先由generate()函数传入生成的四则运算表达式四则运算题目,deque
判断函数是judge()函数,此函数首先从problem.txt中一行一行的读入四则运算题目,提示用户输入答案,将其保存在字符串as中,由于用户输入的结果包括负数,小数和分数,所以字符串中可能出现‘-’,'/','.'。首先需要对用户输入结果的符号进行判定,将判定结果保存在syb中,接着读入数字放在as1中,如果碰到上述点或者除号,则继续读入的数据放在as2中,最后姜永辉输入的结果计算后保存在final_ans中并与ans[]数组中的正确答案进行对比,判断用户给出答案的正误。
注:这里我们简化两个表达式的判断相等,我们令生成表达式的结果不一样就保证了表达式的不等
main函数部分封装在Arithmetic,cpp中,这一部分主要是对命令行传入的数据进行分析,并调用generate()函数。
我们一共设置了十二个单元测试用例,其中前四个是对命令行传入参数的测试,第五个是针对题目要求不能生成重复的题目的测试,第六个到第十个是对solve()函数能否生成正确结果的测试,后两个是对judge()函数能否正确判断用户输入结果正误的测试。具体设置如下:
检查出"3+(2+1)="和 "1+2+3="表达式是重复的并能够提示generate()函数重新生成四则运算表达式
正确答案为0.25,用户输入"1/4"或者“0.25”都正确
测试结果:
可以看到,十二个测试用例全部通过
可以看到代码的覆盖率还是比较好的
这一部分使用c#写的图形界面程序,主要调用第一二阶段的可执行程序,但是一二阶段的可执行程序不能很好契合我们所需要实现的功能,因而在此基础上我们又重现实现了三个.exe可执行程序,分别是:
运行结果:
设置题目数量和乘方类型:
每题限时20秒,没有答完的超时警告:
如果题目做完,继续点击下一题时会弹出的警告:
历史记录查看:
首先是对生成1000个四则运算题目进行的性能分析(不包括与用户的交互输入判断部分),可以看到最耗时的是generte()函数和solve()函数,其中generate()函数的耗时还是由于调用的solve()函数,其自身的生成运算式部分并没有耗费太多时间,而solve()函数则是由于其调用了allocate()函数和calculate()函数及其所调用函数。
接着是对少数运算题目进行测试,这里用10 个,包括与用户的交互输入以及判别部分,可以看到judge()函数即判断用户输入答案是否正确最为耗时。
void Operation::Generate(int mode, int N)
{
int repeat;
char ch[5] = { '+','-','*','/','^' };
srand((unsigned)time(NULL)); //初始化随机数种子
for (int i = 0; i < N; i++)
{
char str[50]; //存放一行计算式
char sym[11]; //符号保存
int num[11]; //数字保存
int symbolnum; //符号个数
int bracket1[3], bracket2[3]; //符号随机加的位置
symbolnum = (rand() % 10) + 1;
//printf("%d", symbolnum);
for (int j = 0; j <= symbolnum; j++)
{
int symbol;
if (sym[j - 1] == '^') //为避免多次乘方而导致结果过大
{
if (num[j - 1] == 0)
{
num[j] = (rand() % 50) + 1;
}
else
{
num[j] = (rand() % 4); //乘方后的数字小于等于3
}
symbol = (rand() % 4); //不再生成乘方
}
else if (sym[j - 1] == '/')
{
symbol = (rand() % 5);
num[j] = (rand() % 50) + 1;
}
else
{
symbol = (rand() % 5);
num[j] = (rand() % 50);
}
sym[j] = ch[symbol];
}
sym[symbolnum] = '=';
int btemp = 0;
if (symbolnum >= 2)
{
btemp = (rand() % 4); //加括号的个数0-3个
for (int j = 0; j < btemp; j++)
{
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的情况
}
for (int j = 0; j < btemp; j++)
{
for (int k = 0; k < btemp; k++)
{
if (bracket1[j] == bracket2[k])
{
bracket1[j] = 20;
bracket2[k] = 20; //设置一个较大的数,使同一数字两旁括号其不输出
}
}
}
for (int j = 0; j < btemp; j++)
{
for (int k = j + 1; k < btemp; k++)
{
if (bracket1[j] == bracket1[k])
{
for (int m = 0; m < btemp; m++)
{
for (int n = m + 1; n < btemp; n++)
{
if (bracket2[m] == bracket2[n])
{
if (bracket1[k] != 20 && bracket2[n] != 20)
{
bracket1[k] = 20;
bracket2[n] = 20; //删除重复括号
}
}
}
}
}
}
}
}
int len = 0;
for (int j = 0; j <= symbolnum; j++)
{
for (int k = 0; k < btemp; k++)
{
if (bracket1[k] == j)
{
str[len] = '(';
len++;
}
}
if (num[j] <= 9 && num[j] >= 0)
{
str[len] = num[j] + '0';
len++;
}
else
{
str[len] = num[j] / 10 + '0'; len++;
str[len] = num[j] % 10 + '0'; len++;
}
for (int k = 0; k < btemp; k++)
{
if (bracket2[k] == j)
{
str[len] = ')';
len++;
}
}
if (mode == 2 && sym[j] == '^')
{
str[len] = '*'; len++;
str[len] = '*'; len++;
}
else
{
str[len] = sym[j]; len++;
}
}
str[len] = '\0';
//printf("%s", str);
//solve(str, out);
repeat = solve(str);
if (repeat == 1)
N++;
}
}
void Operation::allocate(deque& coll1, stack& coll2, deque& coll3)
{
while (!coll1.empty())
{
char c;
c = coll1.front();
coll1.pop_front();
if (c >= '0'&&c <= '9')
{
coll3.push_back(c);
if ((!coll1.empty() && !isdigit(coll1.front())) || coll1.empty())//由于一个数字不一定只有一位长,故整个数字结尾加空格
coll3.push_back(' ');
}
else
{
char d = 0;
if (!coll1.empty())
d = coll1.front();
if (c == '*'&&d == '*') //**转换为^
{
coll1.pop_front();
coll2.push('^');
}
else
check(c, coll2, coll3);//调用check函数,针对不同情况作出不同操作
}
}
//如果输入结束,将coll2的元素全部弹出,加入后缀表达式中
while (!coll2.empty())
{
char c = coll2.top();
coll3.push_back(c);
coll2.pop();
}
}
void Operation::calculate(deque& coll3, stack& 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;
}
}
}
}
void Operation::judge()
{
string as;
int syb;
ifstream problem;
double as1, as2;
double final_ans;
problem.open("question.txt");
string str;
int num = 0, wrong_num = 0, right_num = 0;
while (getline(problem, str))
{
as1 = 0;
as2 = 0;
final_ans = 0;
syb = 0;//初始值为正数
cout << str << endl;
cout << "Your answer:";
cin >> as;
int flag = 1;
int dot = 0;
//处理输入结果
if (as[0] == '-')//负数
{
syb = 1;
}
for (int i = 0; i < as.length(); i++)
{
if (i == 0 && syb == 1)
continue;
if (as[i] <= '9'&&as[i] >= '0'&&flag == 1)
as1 = as1 * 10 + (as[i] - '0');
else if (as[i] == '.')
flag = 2;
else if (as[i] <= '9'&&as[i] >= '0'&&flag == 2)
{
as2 = as2 * 10 + (as[i] - '0');
dot++;
}
else if (as[i] == '/')
flag = 3;
else if (as[i] <= '9'&&as[i] >= '0'&&flag == 3)
as2 = as2 * 10 + (as[i] - '0');
else
{
flag = 0;
break;
}
}
//计算输入的最终结果
if (flag == 1)
final_ans = as1;
else if (flag == 2)
final_ans = as1 + as2 / pow(10, dot);
else if (flag == 3)
final_ans = as1 / as2;
if (syb == 1)
final_ans = 0 - final_ans;
//判断输入结果是否正确
if (flag == 0 || final_ans != ans[num])
{
cout << "wrong!" << endl;
wrong_num++;
}
else
{
cout << "right!" << endl;
right_num++;
}
num++;
}
cout << "正确数:" << right_num << endl;
cout << "错误数:" << wrong_num << endl;
}
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(); //调用做题窗口
}
public partial class Form2 : Form
{
private int count = 0;
TimeSpan ts = new TimeSpan(0, 0, 20);
public Form2()
{
InitializeComponent();
timer1.Interval = 1000;
}
private void Form2_Load(object sender, EventArgs e)
{
FillGrid();
}
void FillGrid()
{
ts = new TimeSpan(0, 0, 20);
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 button3_Click(object sender, EventArgs e)
{
System.Environment.Exit(0);
}
private void textBox3_TextChanged(object sender, EventArgs e)
{
}
private void label6_Click(object sender, EventArgs e)
{
}
private void button2_Click(object sender, EventArgs e)
{
Form3 f3 = new Form3();
f3.Show(); //调用做题窗口
}
private void label7_Click(object sender, EventArgs e)
{
}
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();
}
}
private void label8_Click(object sender, EventArgs e)
{
}
}
private void Form3_Load(object sender, EventArgs e)
{
StreamReader sr = new StreamReader("Judgements.txt", Encoding.Default);//将选中的文件在textBox2中显示
richTextBox1.Text = sr.ReadToEnd();
sr.Close();
}
1、在第三阶段的编码过程中,我们总共需要打开三个txt文件一个是problem.txt这个实在生成题目的时候直接写入的,而另外两个judgements.txt和ansJug.txt则需要追加写入,因为我们是对用户的输入结果一个一个进行判断的,最开始在solve.cpp中每次调用一次打开judgements.txt,导致上一次写入的结果都被第二次所覆盖,所以最终我们在四则运算题目生成的generate.cpp中一次性打开这三个文件,另外两个进行追加写入就可以解决这个问题了。
2.在第一第二阶段我们两部分代码合并的时候,总是跳出白框提示错误,由于题目每次是随机生成的因此我们不能针对具体的四则运算式子找bug,最终决定还是回到合并前,两部分代码分开的时候各自调试,检查,最终发现是由于题目生成部分在删除多余括号时出现的问题,最终修改成功。
通过这次的结对项目,过程中比第一次更加容易一些了,而且我跟队友两个人配合也比较默契,分别实现的部分能够比较好的拼接,节约了很多的时间,但是在第二阶段乘方‘**’这个的转换上其实还是做了一些无用的工作的,就是队友把随机生成的四则运算式中的‘^’换为‘**’,而我又把“**”在我的那一部分又换为‘^’,事实上,是可以她的第一次生成的原始运算式直接给我,我就可以直接计算了,这一点还是团队沟通不够充分导致。另外这次项目开始之前我与队友进行了代码模块的前期设计,为了提高模块的内聚性和减少模块之间的耦合度我们将每一部分的代码细化再细化,每一个函数都有一个独立的功能。最重要的是我与队友配合十分默契,能够清晰地表达想要什么,做成的产品能实现什么功能。这次的结对项目相对于第一次明显轻松了很多,但是也避免不了遇到新的问题,不管怎么说,每次都是一次知识学习的过程,做出最后产品来也很令人愉快,希望以后在其他项目中能够吸取前几次项目的教训,更加出色的完成项目!