作业课程 | |
---|---|
Github项目地址 | Github项目地址 |
姓名、学号 | 龚超富 3121005163 |
姓名、学号 | 林嘉灏 3121005175 |
题目需求
题目
实现一个自动生成小学四则运算题目的命令行程序(也可以用图像界面,具有相似功能)。
自然数:0, 1, 2, …。
- 真分数:1/2, 1/3, 2/3, 1/4, 1’1/2, …。
- 运算符:+, −, ×, ÷。
- 括号:(, )。
- 等号:=。
- 分隔符:空格(用于四则运算符和等号前后)。
- 算术表达式:
e = n | e1 + e2 | e1 − e2 | e1 × e2 | e1 ÷ e2 | (e),
其中e, e1和e2为表达式,n为自然数或真分数。
- 四则运算题目:e = ,其中e为算术表达式。
需求
- 使用 -n 参数控制生成题目的个数,例如
Myapp.exe -n 10
将生成10个题目。
- 使用 -r 参数控制题目中数值(自然数、真分数和真分数分母)的范围,例如
Myapp.exe -r 10
将生成10以内(不包括10)的四则运算题目。该参数可以设置为1或其他自然数。该参数必须给定,否则程序报错并给出帮助信息。
- 生成的题目中计算过程不能产生负数,也就是说算术表达式中如果存在形如e1− e2的子表达式,那么e1≥ e2。
- 生成的题目中如果存在形如e1÷ e2的子表达式,那么*其结果应是真分数*。
- *每道题目中出现的运算符个数不超过3个。*
- 程序一次运行生成的题目不能重复,*即任何两道题目不能通过有限次交换+和×左右的算术表达式变换为同一道题目*。例如,23 + 45 = 和45 + 23 = 是重复的题目,6 × 8 = 和8 × 6 = 也是重复的题目。*3+(2+1)和1+2+3这两个题目是重复的,由于+是左结合的,1+2+3等价于(1+2)+3,也就是3+(1+2),也就是3+(2+1)。但是1+2+3和3+2+1是不重复的两道题,因为1+2+3等价于(1+2)+3,而3+2+1等价于(3+2)+1,它们之间不能通过有限次交换变成同一个题目。*
生成的题目存入执行程序的当前目录下的Exercises.txt文件,格式如下:
- 四则运算题目1
- 四则运算题目2
……
其中真分数在输入输出时采用如下格式,真分数五分之三表示为3/5,真分数二又八分之三表示为2’3/8。
-
在生成题目的同时,计算出所有题目的答案,并存入执行程序的当前目录下的Answers.txt文件,格式如下:
-
答案1
-
答案2
特别的,真分数的运算如下例所示:1/6 + 1/8 = 7/24。
- 程序应能支持一万道题目的生成。
- 程序支持对给定的题目文件和答案文件,判定答案中的对错并进行数量统计,输入参数如下:
Myapp.exe -e <exercisefile>.txt -a <answerfile>.txt
统计结果输出到文件Grade.txt,格式如下:
Correct: 5 (1, 3, 5, 7, 9)
Wrong: 5 (2, 4, 6, 8, 10)
其中“:”后面的数字5表示对/错的题目的数量,括号内的是对/错题目的编号。为简单起见,假设输入的题目都是按照顺序编号的符合规范的题目。
PSP表格
PSP2.1 | 个人软件流程阶段 | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 30 |
Estimate | 估计这个任务需要多少时间 | 900 | 975 |
Development | 开发 | 180 | 240 |
Analysis | 需求分析 (包括学习新技术) | 45 | 60 |
Design Spec | 生成设计文档 | 120 | 90 |
Design Review | 设计复审 | 30 | 45 |
Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 30 | 45 |
Design | 具体设计 | 45 | 60 |
Coding | 具体编码 | 150 | 180 |
Code Review | 代码复审 | 30 | 45 |
Test | 测试(自我测试,修改代码,提交修改) | 30 | 60 |
Reporting | 报告 | 30 | 60 |
Test Repor | 测试报告 | 30 | 30 |
Size Measurement | 计算工作量 | 50 | 40 |
Postmortem & Process Improvement Plan | 事后总结, 并提出过程改进计划 | 20 | 20 |
合计 | 790 | 975 |
效能分析
程序中消耗最大的函数是getTopicAnswer函数
设计实现过程
程序一共有八个类:Calculator、Core、IO、Expression、Query、Examine、Item Random、Main
方法关系组织
代码说明
随机构建中缀表达式。
-
初始化变量和数据结构:
- 定义整数变量
i
,用于遍历运算符数组。 - 创建一个
ArrayList
类型的列表inf
,用于存储中缀表达式的各个元素(数字和运算符)。
- 定义整数变量
-
遍历运算符数组和数字数组:
- 使用
for
循环,遍历运算符数组ope
中的每个元素。 - 在循环中,首先将数字数组
num
中的对应元素添加到inf
列表中。 - 然后将当前运算符转换为字符串,并将其添加到
inf
列表中。
- 使用
-
根据运算符数组的长度进行不同的处理:
- 使用
switch
语句,根据运算符数组的长度(可能为3、2或其他),执行不同的分支。 - 在某些分支中,根据特定条件,向
inf
列表的开头和结尾添加括号,以确保正确的运算顺序。
- 使用
-
遍历
inf
列表,修正运算符周围的空格:- 使用
for
循环遍历inf
列表中的每个元素。 - 如果元素是运算符(+、-、×、÷),则在其前后添加空格,以确保表达式的格式正确。
- 使用
-
构建最终的中缀表达式字符串:
- 创建一个
StringBuilder
对象infix
,用于将inf
列表中的元素连接起来,形成最终的中缀表达式字符串。
- 创建一个
-
返回中缀表达式字符串:
- 将
infix
转换为字符串,并返回作为方法的结果。
- 将
public String infixExpression(char[] ope, String[] num) {
int i = 0;
ArrayList<String> inf = new ArrayList<>();
for (; i < ope.length; i++) {
inf.add(num[i]);
inf.add(String.valueOf(ope[i]));
}
inf.add(num[i]);
switch (ope.length) {
case 3 -> {
if ((ope[0] == '+' || ope[0] == '-') && (ope[1] == '+' || ope[1] == '-') && (ope[2] == '×' || ope[2] == '÷')) {
inf.add(0, "(");
inf.add(6, ")");
}
if ((ope[0] == '+' || ope[0] == '-') && (ope[1] == '×' || ope[1] == '÷')) {
inf.add(0, "(");
inf.add(4, ")");
}
}
case 2 -> {
if ((ope[0] == '+' || ope[0] == '-') && (ope[1] == '×' || ope[1] == '÷')) {
inf.add(0, "(");
inf.add(4, ")");
}
}
default -> {
}
}
for (i = 0; i < inf.size(); i++) {
if (inf.get(i).equals("+") || inf.get(i).equals("-") || inf.get(i).equals("×") || inf.get(i).equals("÷"))
inf.set(i, " " + inf.get(i) + " ");
}
StringBuilder infix = new StringBuilder(inf.get(0));
for (i = 1; i < inf.size(); i++) {
infix.append(inf.get(i));
}
return infix.toString();
}
中缀表达式转化成后缀表达式
-
初始化数据结构:
- 创建一个栈
stack
用于暂时存储运算符和左括号。 - 创建一个列表
list
用于存储后缀表达式的各个元素。 - 使用
split
方法将输入的中缀表达式字符串string
按空格分割成字符串数组splitString
,以便逐个处理表达式的元素。
- 创建一个栈
-
遍历中缀表达式的元素:
- 使用
for-each
循环遍历splitString
中的每个元素str
。 - 如果
str
是左括号,将其右边的内容加入到list
中,并将左括号入栈。这是为了处理括号内的子表达式。 - 如果
str
是右括号,将左括号到右括号之间的内容依次加入到list
中,并将栈中的元素出栈,直到遇到左括号。这是为了处理括号内的子表达式。 - 如果
str
是运算符(+、-、×、÷):- 如果栈为空,直接将当前运算符入栈。
- 如果运算符是加法或减法,优先级最低,将栈中的运算符出栈并加入到
list
中,直到栈为空或栈顶是左括号。然后将当前运算符入栈。 - 如果运算符是乘法或除法,其优先级较高。如果栈不为空且栈顶元素也是乘法或除法,将栈顶元素出栈并加入到
list
中,然后将当前运算符入栈。否则,直接将当前运算符入栈。
- 如果
str
是数字或其他字符,直接将其加入到list
中。
- 使用
-
处理剩余栈内元素:
- 循环将栈内的所有元素出栈并加入到
list
中,以确保所有元素都被处理。
- 循环将栈内的所有元素出栈并加入到
-
将列表
list
中的元素转换为字符串数组postfixString
,并返回该数组作为方法的结果。
public String[] postfixExpression(String string) {
// 符号栈
Stack<String> stack = new Stack<>();
// 后缀表达式
List<String> list = new LinkedList<>();
// 将中缀表达式按空格分开
String[] splitString = string.split(" ");
for (String str : splitString) {
// 如果是左括号就入栈
if (str.matches("\\(.*")) {
list.add(str.split("\\(")[1]);
stack.push("(");
}
// 如果是右括号就把栈顶元素依次加入到列表,直到读取到左括号,将其出栈。
else if (str.matches(".*\\)")) {
list.add(str.split("\\)")[0]);
while (!stack.peek().equals("(")) {
list.add(stack.pop());
}
stack.pop();
} else if (str.matches("[+\\-×÷]")) {
// 栈为空将运算符入栈
if (stack.isEmpty()) stack.push(str);
// 如果运算符是加减,优先级最低,将栈顶元素加入到列表,如果读取到左括号或栈为空将运算符入栈
else if (str.matches("[+\\-]")) {
while (!stack.isEmpty() && !stack.peek().equals("(")) {
list.add(stack.pop());
}
stack.push(str);
}
// 如果运算符是乘除
else {
// 如果栈不为空且栈顶元素是乘除,将其出栈加入到列表。
while (!stack.isEmpty() && stack.peek().matches("[×÷]")) {
list.add(stack.pop());
}
// 栈顶元素是加减或为空,将运算符入栈。
stack.push(str);
}
}
// 其余符号都是表示数字,将其入栈。
else {
list.add(str);
}
}
// 最后把栈内元素全部加入到列表
while (!stack.isEmpty()) {
list.add(stack.pop());
}
String[] postfixString = new String[list.size()];
// 将列表元素转变为字符串数组
for (int i = list.size() - 1; i >= 0; i--) {
postfixString[i] = list.remove(i);
}
return postfixString;
}
测试运行
-
测试四则运算,设置不同输入,如果和理论的输出一致则通过。
@Test public void calculate() { //加法 assertThat(Calculate.calculate("1","3",1),equalTo("4")); assertThat(Calculate.calculate("1","1/5",1),equalTo("1'1/5")); //减法 assertThat(Calculate.calculate("2","1",2),equalTo("1")); assertThat(Calculate.calculate("1/3","1",2),equalTo("-2/3")); assertThat(Calculate.calculate("3/5","2/5",2),equalTo("1/5")); //乘法 assertThat(Calculate.calculate("1/2","2",3),equalTo("1")); assertThat(Calculate.calculate("4","2",3),equalTo("8")); assertThat(Calculate.calculate("3/4","5/6",3),equalTo("5/8")); assertThat(Calculate.calculate("1'1/2","1'1/2",3),equalTo("2'1/4")); //除法 assertThat(Calculate.calculate("1/2","2",4),equalTo("1/4")); assertThat(Calculate.calculate("4","2",4),equalTo("2")); assertThat(Calculate.calculate("1'1/6","6",4),equalTo("7/36")); assertThat(Calculate.calculate("1'1/3","1'1/3",4),equalTo("1")); }
-
测试分数转换成int型分子分母,分别测试带分数真分数自然数
@Test public void conversion() { Calculate cal = new Calculate(); assertThat(cal.conversion("1'1/3"),equalTo(new int[]{4,3})); assertThat(cal.conversion("1"),equalTo(new int[]{1,1})); assertThat(cal.conversion("5/4"),equalTo(new int[]{5,4})); }
-
测试分子分母化简
@Test public void reduction() { Calculate cal = new Calculate(); assertThat(cal.reduction(9,7),equalTo("1'2/7")); assertThat(cal.reduction(3,4),equalTo("3/4")); assertThat(cal.reduction(9,2),equalTo("4'1/2")); assertThat(cal.reduction(20,10),equalTo("2")); }
-
测试两个表达式是否相同
@Test public void sameExpression() { String a1="2 ÷ 1"; String b1="1 - 2"; Examine examine = new Examine(); assertFalse(examine.sameExpression(a1, b1)); String a2="1 + 2"; String b2="2 + 1"; assertTrue(examine.sameExpression(a2, b2)); String a3="(1 ÷ 2) × 3"; String b3="(2 ÷ 1) × 3"; assertFalse(examine.sameExpression(a3, b3)); }
-
测试生成中缀表达式
@Test public void infixExpression(){ char[] ope={'+','-','×'}; String[] num={"32","5","9","3/4"}; Core core = new Core(); assertThat(core.infixExpression(ope,num),equalTo("(32 + 5 - 9) × 3/4")); char[] ope1={'+','×'}; String[] num1={"3","6","21","3/7"}; assertThat(core.infixExpression(ope1,num1),equalTo("(3 + 6) × 21")); char[] ope2={'×'}; String[] num2={"32","5","9","3/4"}; assertThat(core.infixExpression(ope2,num2),equalTo("32 × 5")); }
-
测试将中缀表达式转换成后缀表达式
@Test public void postfixExpression(){ String string="9 + (3 - 1) × 3 + 10 ÷ 2"; Core core = new Core(); String[] strings= core.postfixExpression(string); assertThat(strings[0],equalTo("9")); assertThat(strings[1],equalTo("3")); assertThat(strings[2],equalTo("1")); assertThat(strings[3],equalTo("-")); assertThat(strings[4],equalTo("3")); assertThat(strings[5],equalTo("×")); assertThat(strings[6],equalTo("+")); assertThat(strings[7],equalTo("10")); assertThat(strings[8],equalTo("2")); assertThat(strings[9],equalTo("÷")); assertThat(strings[10],equalTo("+")); String string1="a + b × c + (d × e + f) × g"; String[] strings1= core.postfixExpression(string1); assertThat(strings1[0],equalTo("a")); assertThat(strings1[1],equalTo("b")); assertThat(strings1[2],equalTo("c")); assertThat(strings1[3],equalTo("×")); assertThat(strings1[4],equalTo("+")); assertThat(strings1[5],equalTo("d")); assertThat(strings1[6],equalTo("e")); assertThat(strings1[7],equalTo("×")); assertThat(strings1[8],equalTo("f")); assertThat(strings1[9],equalTo("+")); assertThat(strings1[10],equalTo("g")); assertThat(strings1[11],equalTo("×")); assertThat(strings1[12],equalTo("+")); }
-
测试计算后缀表达式
@Test public void generateAnswer() { String[] strings={"9","3","1","-","3","×","+","10","2","÷","+"}; Core core = new Core(); assertThat(core.generateAnswer(strings),equalTo("20")); }
-
测试利用栈计算
@Test public void identifyOperator() { Stack<String> stack=new Stack<>(); Core core = new Core(); stack.push("1"); stack.push("3"); core.identifyOperator(stack,"+"); assertThat(stack.pop(),equalTo("4")); stack.push("1"); stack.push("3"); core.identifyOperator(stack,"-"); assertThat(stack.pop(),equalTo("-2")); stack.push("1"); stack.push("5"); core.identifyOperator(stack,"×"); assertThat(stack.pop(),equalTo("5")); stack.push("2"); stack.push("2"); core.identifyOperator(stack,"÷"); assertThat(stack.pop(),equalTo("1")); core.identifyOperator(stack,"1'1/2"); assertThat(stack.pop(),equalTo("1'1/2")); }
-
测试主方法
@Test public void main() { String[] strings={"-n","300","-r","120"}; Main.main(strings); String[] strings1={"-e","exercise.txt","-a","answers.txt"}; Main.main(strings1); }
项目小结
成功之处
- 协作效率提高: 结对编程让我们可以即时解决问题,共同制定解决方案,并且迅速理解彼此的代码。这样做远比独自开发更高效。
- 代码质量提高: 由于有两个人审查代码,我们能够尽早发现和修复潜在的错误,提高了代码质量。
- 知识分享: 结对编程促进了我们之间的知识共享。
结对感受
- 沟通交流是十分重要的一个部分,有效的沟通能促进工作的进行
- 分工是一门学问,选择自己擅长的领域工作才是正确的选择
总的来说,结对编程是一个有益的实践,它加强了我们之间的合作,提高了项目质量。通过彼此分享经验,我们相信我们可以在未来的项目中取得更多的成功。