结对项目
项目要求
软工作业 | 结对编程 |
---|---|
作业目标 | 作业目标 |
队伍成员 | 3118005373刘世轩 3118005356曾春华 |
GitHub地址 | Pair-program |
作业要求 | 实现一个自动生成小学四则运算题目的命令行程序 |
PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 20 | 15 |
·Estimate | · 估计这个任务需要多少时间 | 10 | 10 |
Development | 开发 | 100 | 300 |
·Analysis | · 需求分析 (包括学习新技术) | 40 | 60 |
·Design Spec | · 生成设计文档 | 20 | 20 |
·Design Review | · 设计复审 (和同事审核设计文档) | 40 | 70 |
·Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 30 | 50 |
· Design | · 具体设计 | 50 | 50 |
· Coding | · 具体编码 | 80 | 120 |
· Code Review | · 代码复审 | 120 | 40 |
· Test | · 测试(自我测试,修改代码,提交修改) | 30 | 60 |
Reporting | 报告 | 90 | 240 |
· Test Report | · 测试报告 | 30 | 50 |
· Size Measurement | · 计算工作量 | 40 | 10 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 20 | 10 |
合计 | 720 | 1105 |
设计实现过程
主要需要两个类,分别为分数和表达式。在整数、带分数、真分数中,统一都存储为分数。并且重载+,-,*,/运算符方便表达式的计算;在计算表达式的答案时,需要用到两个栈,分别保存运算符、分数,每次轮流入栈运算符、分数。
其他的函数主要有两个,是generateExp()和checkAnswer()。分别用于生成表达式和检查答案。
生成表达式主要分为以下步骤:
1.随机生成 n(0< n < 4) 个运算符(不包括括号)和 n + 1 个分数,存进各自容器。
2.随机生成一个数,判断是否生成括号,若生成,则在运算符容器中插入括号。
3.计算表达式的答案,并且判断中间有无产生负数,以及答案是否重复,来判断此次生成表达式是否成功。
在生成表达式时,需要将分数类转换成字符串后再写入文件中,也需要其他辅助函数来完成。
代码说明
输入表达式
bool Expression::input(vector<char>& opArray, vector<Elem>& dArray)
{
int i = 0, j = 0;
if (opArray[i] == '(')
{
opStack.push(opArray[i++]);
}
//将运算符和数据的容器都读到空为止
while(i != opArray.size() || j != dArray.size())
{
//读取一个数据
dStack.push(dArray[j++]);
//读取运算符
if (!inputOp(opArray, i))
return false;
}
return true;
}
在遇到括号时,需要进行处理。在遇到优先级较低的符号入栈时,需要先将优先级较高的一次计算完,再将结果入栈。直到将所有的符号的分数都处理完后,就生成好一个表达式。
计算表达式答案
bool mainCal(Expression& exp, Elem& res)
{
Elem ldata, rdata;
while (!exp.opStack.empty())
{
//取出左右数据出栈后, 再将计算结果入栈
rdata = exp.dStack.top();
exp.dStack.pop();
ldata = exp.dStack.top();
exp.dStack.pop();
Elem result(opCal(ldata, rdata, exp.opStack.top()));
//计算过程是否产生负数
if (result.up < 0 || result.down < 0)
return false;
exp.dStack.push(result);
exp.opStack.pop();
}
res.up = exp.dStack.top().up;
res.down = exp.dStack.top().down;
return true;
}
在输入符号时,已经将优先级高的运算符、包含括号的都计算完了,剩下的只需要依次运算即可,每次取两个分数,与一个运算符,运算完后再将结果入栈,需要注意其中结果会不会产生负数。
生成题目
bool generateExp(const int& n, const int& range)
{
srand((int)time(NULL));
char oper[] = {'+', '-', '*', '/'};
vector<Elem> dArray;
vector<char> opArray;
vector<Elem> resArray;
Elem res;
ofstream exerc, ans;
exerc.open(".\\Exercise.txt");
ans.open(".\\Answer.txt");
int opNum = 0, opId = 0;
int up = 0, down = 0;
for (int i = 0; i < n; i++)
{
//清空两个容器
dArray.clear();
opArray.clear();
//该题的运算符个数
opNum = rand() % 3 + 1;
for (int j = 0; j < opNum; j++)
{
//生成opNum个符号和数据
opId = rand() % 4;
opArray.push_back(oper[opId]);
down = rand() % (range - 1) + 1;
up = rand() % (down * range - 1) + 1;
dArray.push_back(Elem (up, down));
}
down = rand() % (range - 1) + 1;
up = rand() % (down * range - 1) + 1;
dArray.push_back(Elem (up, down));
//生成括号
if (opId < 2 && opNum > 1)
{
int pos = rand() % (opNum - 1);
opArray.insert(opArray.begin() + pos, '(');
opArray.insert(opArray.begin() + pos + 2, ')');
}
Expression exp;
//判断该表达式合不合格
if (!exp.input(opArray, dArray) || !mainCal(exp, res) ||
(find(resArray.begin(), resArray.end(), res) != resArray.end()))
{
i--;
continue;
}
else
{
//计算结果写入文件, 答案存入容器
exerc << i + 1 << ". " << expToString(opArray, dArray) << endl;
resArray.push_back(res);
ans << i + 1 << ". " << elemToString(res) << endl;
}
}
ans.close();
exerc.close();
return true;
}
需要控制分母、分值都不能大于range,在判断运算符个数小于4,分子不为0,生不生成括号以及括号的位置,都通过随机数来完成。而生成表达式的思路也已经在上面讲过了。其中需要将表达式写入文件中,因此需要一些将辅助函数来将分数容器与符号容器拼接成一个字符串。
判断题目与答案的对错
bool check(string& formula, string& answer)
{
//删除题目标号
int t = find(formula.begin(), formula.end(), '.') - formula.begin();
formula.erase(0, t + 2);
answer.erase(0, t + 2);
//将题目分割并进行计算
vector<char> opArray;
vector<Elem> dArray;
vector<string> partVec = splitWithExp(formula, " ");
int i = 0;
while (i < partVec.size() - 1)
{
//处理数据
if (i % 2 == 0)
{
auto lPos = partVec[i].find("(");
auto rPos = partVec[i].find(")");
if (lPos != string::npos)
{
opArray.push_back('(');
partVec[i].erase(partVec[i].begin() + lPos);
}
else if (rPos != string::npos)
{
opArray.push_back(')');
partVec[i].erase(partVec[i].begin() + rPos);
}
//查找带分数的符号与分号
auto mixedPos = partVec[i].find("'");
auto fracPos = partVec[i].find("/");
auto dSize = partVec[i].size();
int up, down;
//带分数
if (mixedPos != string::npos)
{
int intNum = atoi(partVec[i].substr(0, mixedPos).c_str());
int upNum = atoi(partVec[i].substr(mixedPos + 1, fracPos).c_str());
down = atoi(partVec[i].substr(fracPos + 1, dSize).c_str());
up = intNum * down + upNum;
}
//真分数
else if (fracPos != string::npos)
{
up = atoi(partVec[i].substr(0, fracPos).c_str());
down = atoi(partVec[i].substr(fracPos + 1, dSize).c_str());
}
//整数
else
{
up = atoi(partVec[i].c_str());
down = 1;
}
dArray.push_back(Elem(up, down));
}
//处理符号
if (i % 2 == 1)
{
opArray.push_back(partVec[i][0]);
}
i++;
}
//计算答案
Expression exp;
Elem res;
exp.input(opArray, dArray);
mainCal(exp, res);
string s = elemToString(res);
if (answer.compare(s) == 0)
{
return true;
}
else
{
return false;
}
}
因为传入的题目与答案还有题目标号等,需要先将其中的题目标号删除,再将题目分割成一小块,经过处理后分好放入运算符容器与分数容器中。最后在计算正确答案后转成字符串,与传入的文件答案进行比较,返回 true 还是 false。
统计题目,答案文件中的对错
int checkAnswer(const string& exercPath, const string& ansPath)
{
ifstream exerc, ans;
ofstream grade;
exerc.open(exercPath);
ans.open(ansPath);
grade.open(".\\Grade.txt");
string formula, answer;
//记录题号
int no = 1;
//保存正确和错误的题号
vector<int> correct, wrong;
//判断 每道题的正确与错误
while (getline(exerc, formula) && getline(ans, answer))
{
if (check(formula, answer))
{
correct.push_back(no++);
}
else
{
wrong.push_back(no++);
}
}
//将结果写到文件Grade.txt中
grade << "Correct: " << correct.size();
if (correct.size() > 0)
{
grade << " (";
for (auto it = correct.begin(); it != correct.end() - 1; it++)
{
grade << *it << ", ";
}
grade << correct[correct.size() - 1] << ")" << endl;
}
grade << "Wrong: " << wrong.size();
if (wrong.size() > 0)
{
grade << " (";
for (auto it = wrong.begin(); it != wrong.end() - 1; it++)
{
grade << *it << ", ";
}
grade << wrong[wrong.size() - 1] << ")" << endl;
}
exerc.close();
ans.close();
grade.close();
return correct.size();
}
将输入的题目、答案文件打开后,每次读取一行,在判断该题的对错,并将题号进行统计。最后在输出到文件Grade.txt中。
效能分析
可以看出辗转相除法函数 gcd() 与分数的重载运算符 == 所占的时间比较多。
测试运行
使用的是VS自带的单元测试。
代码如下:
TEST_METHOD(TestMethod1)
{
bool res = check("2 + 3 * 2 = ", "10");
Assert::AreEqual(res, false);
}
TEST_METHOD(TestMethod2)
{
generateExp(10, 5);
int num = checkAnswer("Exercise.txt", "Answer.txt");
Assert::AreEqual(10, num);
}
结果:
测试结果都符合预期。
最终效果
命令行运行程序截图
生成文件截图
项目小结
曾春华
在结对编程中,由于在前期没有进行合理的分工,导致在完成的过程中出现了一些小问题。一开始看需求分析的时候,感觉应该还不太难。但后面设计思路上,还是卡了挺久的,但两人在网上查资料后,讨论过后才解决了。中间也出现了一些 bug,一开始也没发现,在整合时才发现并修改的。总的来说,结对编程人多了,但是对两个人的配合、分工还是较为考验的,做好需求分析与模块设,并分工好,才能够在后面的开发中变得更有效率,节省更多的时间。
刘世轩
在本次项目中,我们进行了结对编程,各方面的磨合并不顺利,我们觉得最终并没有达到“1+1>2”的效果,但此次的经验也让我们认识到了诸多不足之处并尽量一一完善改正,这是个人项目无法给予的宝贵经验。