项目仓库:https://github.com/AndyVirginia/Arithmetic
项目成员:王锴(题目生成),邵伟源(计算实现)
一、项目需求:实现一个自动生成小学四则运算题目的命令行程序。
1. 使用 -n 参数控制生成题目的个数,例如
Myapp.exe -n 10
将生成10个题目。
2. 使用 -r 参数控制题目中数值(自然数、真分数和真分数分母)的范围,例如
Myapp.exe -r 10
将生成10以内(不包括10)的四则运算题目。该参数可以设置为1或其他自然数。该参数必须给定,否则程序报错并给出帮助信息。
3. 生成的题目中计算过程不能产生负数,也就是说算术表达式中如果存在形如e1 − e2的子表达式,那么e1 ≥ e2。
4. 生成的题目中如果存在形如e1 ÷ e2的子表达式,那么其结果应是真分数。
5. 每道题目中出现的运算符个数不超过3个。
6. 程序一次运行生成的题目不能重复,即任何两道题目不能通过有限次交换+和×左右的算术表达式变换为同一道题目。例如,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. 四则运算题目1
2. 四则运算题目2
……
其中真分数在输入输出时采用如下格式,真分数五分之三表示为3/5,真分数二又八分之三表示为2’3/8。
7. 在生成题目的同时,计算出所有题目的答案,并存入执行程序的当前目录下的Answers.txt文件,格式如下:
1. 答案1
2. 答案2
特别的,真分数的运算如下例所示:1/6 + 1/8 = 7/24。
8. 程序应能支持一万道题目的生成。
9. 程序支持对给定的题目文件和答案文件,判定答案中的对错并进行数量统计,输入参数如下:
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 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
Planning | 计划 | 30 | 45 |
· Estimate | · 估计这个任务需要多少时间 | 30 | 45 |
Development | 开发 | 570 | 630 |
· Analysis | · 需求分析 (包括学习新技术) | 60 | 60 |
· Design Spec | · 生成设计文档 | 60 | 60 |
· Design Review | · 设计复审 (和同事审核设计文档) | 30 | 30 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 30 | 30 |
· Design | · 具体设计 | 120 | 120 |
· Coding | · 具体编码 | 180 | 240 |
· Code Review | · 代码复审 | 60 | 60 |
· Test | · 测试(自我测试,修改代码,提交修改) | 30 | 30 |
Reporting | 报告 | 90 | 90 |
· Test Report | · 测试报告 | 30 | 30 |
· Size Measurement | · 计算工作量 | 30 | 30 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 | 30 |
合计 |
| 690 | 765 |
三、项目实现过程和代码实现
1.项目文件结构如下:
模块 | 功能 |
main.py | 主函数 |
stack.py | 栈实现 |
generator.py | 生成表达式 |
infixTosuffix.py | 将中缀表达式转成后缀表达式 |
Calculate.py | 计算 |
2.分析与设计
本设计涉及到的有逆波兰表达式和栈。
表达式的生成:
考虑到了以下几点:
- 表达式的操作数比四则运算符数目多一个
- 操作数的个数可以随机
- 0 不能作为除数
- 计算过程中不能在某一步中出现负数(负数检测最终在计算器中实现)
利用列表先随机生成1~3个运算符存入一个列表,通过列表长度生成对应个数的操作数,如果除号后的操作数等于0则重新生成一个。
操作数的类型统一为分数类型(Fraction类),整数的分母为1,但由于整数,带分数,真分数的显示格式不同,所以定义了一个str_num()的方法将分数转换为符合标准的字符串。
括号的插入:
插空法在运算符数组中随机成对插入括号,通过检测前后的运算符优先级判断是否需要插入括号。
计算答案:
通过将字符串表达式转换成逆波兰表达式和栈的操作实现表达式的优先级区分和具体计算。(具体的四则运算集成在Fraction类中不需要自己实现)
3.具体实现:
(1)表达式的生成:
from fractions import Fraction import random class Ari_Expression(): '''算术表达式的生成''' def __init__(self, max_num): self.init_operators() self.init_nums(max_num) self.init_expression() def init_num(self, max_num): '''随机生成数''' denominator = random.randint(1, max_num) numerator = random.randint(0, max_num) return Fraction(numerator, denominator) def insert_bracket(self): '''插入括号''' bracket = ['(', 'null', ')'] if len(self.operators) > 1: x = random.randint(0, len(self.operators)) while x < len(self.operators): y = random.randint(x, len(self.operators)) low = False for a in self.operators[x:y+1]: if a in ['+', '-']: low = True break try: if self.operators[y+1] in ['×', '÷'] and low: self.operators.insert(x, '(') self.operators.insert(y+2,')') except IndexError: pass x = y+2 def init_operators(self): '''随机生成一个运算符并随机插入括号''' self.operators = [] operator = ['+', '-', '×', '÷', 'null'] for x in range(3): if x == 1: self.operators.append(random.choice(operator[:-2])) else: y = random.choice(operator) if y != 'null': self.operators.append(y) self.insert_bracket() def init_nums(self, max_num): self.nums = [] self.nums.append(self.init_num(max_num)) for x in range(len(self.operators)): y = self.init_num(max_num) if self.operators[x] == '÷': while y.numerator == 0: y = self.init_num(max_num) self.nums.append(y) def str_num(self, num): '''字符串化一个分数''' inter = int(num.numerator / num.denominator) numerator = int(num.numerator % num.denominator) str_num = '' if numerator != 0: str_num += str(numerator) + '/' + str(num.denominator) if not str_num: '''如果为空''' str_num += str(inter) else: if inter == 0: return str_num else: str_num = str(inter) + '`' + str_num return str_num def init_expression(self): '''生成一个算术表达式的字符串形式''' self.str = '' i = 0 self.exp = [] again = False for x in self.operators: if again: self.str += x + ' ' elif x == '(': self.str += x + ' ' elif x == ')': self.str += self.str_num(self.nums[i]) + ' ' i += 1 self.str += x + ' ' again = True else: self.str += self.str_num(self.nums[i]) + ' ' self.str += x + ' ' i += 1 self.str += self.str_num(self.nums[-1]) + ' ='
(2)栈实现
class Stack: """模拟栈""" def __init__(self): self.items = [] def isEmpty(self): return len(self.items) == 0 def push(self, item): self.items.append(item) def pop(self): return self.items.pop() def peek(self): if not self.isEmpty(): return self.items[len(self.items) - 1] def size(self): return len(self.items)
(3)生成逆波兰表达式:
import Stack from fractions import Fraction class infix_to_suffix: def __init__(self): self.list_operators = ["+", "-", "×", "÷", "(", ")", "="] self.pri_operators = {"+": 0, "-": 0, "×": 1, "÷": 1} def to_suffix_expression(self, expression): '''生成逆波兰表达式''' stack_operator = Stack.Stack() suffix_expression = [] list_temp = [] expression = expression + "=" for element in expression: if element not in self.list_operators: list_temp.append(element) elif element == "=": if len(list_temp) != 0: str_temp = "" for i in range(0, len(list_temp)): str_temp = str_temp+list_temp.pop(0) suffix_expression.append(str_temp) else: if len(list_temp) != 0: str_temp = "" for i in range(0, len(list_temp)): str_temp = str_temp+list_temp.pop(0) suffix_expression.append(str_temp) if stack_operator.isEmpty() or element == "(": stack_operator.push(element) elif element != "(" and element != ")": if stack_operator.peek() != "(" and self.pri_operators[element] > self.pri_operators[ stack_operator.peek()]: stack_operator.push(element) else: while True: if stack_operator.peek() == "(": stack_operator.push(element) break elif self.pri_operators[element] < self.pri_operators[stack_operator.peek()]: while not stack_operator.isEmpty() and True: str_operator = stack_operator.peek() if str_operator == "(" or self.pri_operators[str_operator] < self.pri_operators[ element]: break else: stack_operator.pop() suffix_expression.append(str_operator) else: suffix_expression.append(stack_operator.pop()) if stack_operator.isEmpty(): stack_operator.push(element) break elif element == ")": while True: if stack_operator.peek() != "(": suffix_expression.append(stack_operator.pop()) else: stack_operator.pop() break else: stack_operator.push(element) if not stack_operator.isEmpty(): while not stack_operator.isEmpty(): suffix_expression.append(stack_operator.pop()) return suffix_expression def str_to_fraction(self, suf): '''字符串转换为fraction类''' for x in range(len(suf)): if suf[x] not in self.list_operators: y = suf[x].strip() if y.find('`') == -1: if y.find('/') == -1: numerator = int(y) denominator = 1 else: a = y.split('/') numerator = int(a[0]) denominator = int(a[1]) else: a = y.split('`') inter = int(a[0]) b = a[1].split('/') denominator = int(b[1]) numerator = int(b[0]) + inter * denominator new_num = Fraction(numerator,denominator) suf[x] = new_num return suf
(4)具体计算:
from fractions import Fraction import Stack def getResult(expression): stackValue = [] for item in expression: if item in ["+", "-", "×", "÷"]: n2 = stackValue.pop() n1 = stackValue.pop() result = cal(n1, n2, item) if result < 0: return -1 stackValue.append(result) else: stackValue.append(item) return stackValue[0] def cal(n1, n2, op): if op == "+": return n1 + n2 if op == "-": return n1 - n2 if op == "×": return n1 * n2 if op == "÷": return n1 / n2
(5)主函数(获取功能参数、文件写入):
from generator import Ari_Expression from infixTosuffix import infix_to_suffix import Calculate def main(): max_num = int(input('请输入操作数的最大值:')) problem = int(input('请输入需要的题目数量:')) i = 1 correct = [] wrong = [] p = open('Exercises.txt', 'w') r = open('Answer.txt', 'w') while i < problem + 1: ari = Ari_Expression(max_num) inf = infix_to_suffix() real_res = Calculate.getResult(inf.str_to_fraction(inf.to_suffix_expression(ari.str))) if real_res >= 0: real_res = ari.str_num(real_res) print(str(i)+'. ' + ari.str, end = '') p.write(str(i)+'. ' + ari.str + '\n') r.write(str(i)+'. ' + real_res + '\n') res = input() if res == real_res: correct.append(i) else: wrong.append(i) i += 1 p.close() r.close() print('题目正确率:' + str(len(correct)/problem)) g = open('Grade.txt','w') g.write('Correct:' + str(len(correct))+'(') for x in correct: g.write(str(x)+', ') g.write(')\n') g.write('Wrong:' + str(len(wrong))+'(') for x in wrong: g.write(str(x)+', ') g.write(')\n') if __name__ == '__main__': main()
4.运行测试
一万题挑战:
5.项目小结:
本次结对编程我和邵伟源结队,各自负责一部分逻辑,思考如何实现具体的功能,中间感谢大佬给的思路参考,有了大致的方向之后我们开始实现具体的功能,尽管在中间我们遇到了一些问题,但是当我们在无法解决的时候向对方提供帮助,在结对编程中向对方学习到不少知识,同时也发现了自身的不足,实现了1+1大于2。希望有机会再次合作。