一、项目说明
成员:林立新 林俊博
项目github地址:https://github.com/06linxi/topicgennerator
实现一个自动生成小学四则运算题目的命令行程序。
一、需求:
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)。
二、未实现的功能
1、 程序一次运行生成的题目不能重复
三、PSP
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
Planning | 计划 | 30 | 30 |
· Estimate | · 估计这个任务需要多少时间 | 10 | 30 |
Development | 开发 | 600 | 900 |
· Analysis | · 需求分析 (包括学习新技术) | 60 | 120 |
· Design Spec | · 生成设计文档 | 10 | 10 |
· Design Review | · 设计复审 (和同事审核设计文档) | 60 | 60 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 30 | 30 |
· Design | · 具体设计 | 30 | 60 |
· Coding | · 具体编码 | 1200 | 900 |
· Code Review | · 代码复审 | 60 | 60 |
· Test | · 测试(自我测试,修改代码,提交修改) | 30 | 30 |
Reporting | 报告 | 60 | 60 |
· Test Report | · 测试报告 | 20 | 10 |
· Size Measurement | · 计算工作量 | 10 | 20 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 | 60 |
合计 |
|
|
四、设计思路
1、写一个随机生成整数或者分数的函数,写一个随机生成运算符的函数,写一个随机生成表达式组成部分的列表。
2、利用二叉树对表达式组成部分的列表进行处理,返回一个规范的表达式组成部分的列表,利用join()把列表元素连接在一起,输出标准表达式。
3、利用二叉树以及定义的加减乘除运算的法则,输出假分数,再调用假分数转化为真分数的方法,将结果转化为正分数。
4、对输入题目文件,逐行读取,将每一行转化为列表的形式,并利用方法第2点,第3点,输出其结果与答案文件中每一行答案对比,判断对错。
五、具体代码实现
这是自己定义的加减乘除函数:
def calculate(a, b, c):
if len(a.split('/')) == 2:
m = a.split('/')
fzm = m[0]
fmm = m[1]
else:
fzm = int(a)
fmm = 1
if len(b.split('/')) == 2:
n = b.split('/')
fzn = n[0]
fmn = n[1]
else:
fzn = int(b)
fmn = 1
if c == '+':
Fz = int(fzm) * int(fmn) + int(fmm) * int(fzn)
Fm = int(fmm) * int(fmn)
return str(Fz) + '/' + str(Fm)
if c == '-':
Fz = int(fzm) * int(fmn) - int(fmm) * int(fzn)
Fm = int(fmm) * int(fmn)
return str(Fz) + '/' + str(Fm)
if c == '×':
Fz = int(fzm) * int(fzn)
Fm = int(fmm) * int(fmn)
return str(Fz) + '/' + str(Fm)
if c == '÷':
Fz = int(fzm) * int(fmn)
Fm = int(fmm) * int(fzn)
return str(Fz) + '/' + str(Fm)
这是对给定的题目文件和答案文件,判定答案中对错的数量和具体的题目实现函数。
def compare(exercisefile ,answerfile ): correct = [] wrong = [] with open(answerfile, 'r') as a: text = a.readlines() hangshu = len(text) for i in range(hangshu): # 读取答案文件的行数,循环对比 # 第i行 with open(exercisefile, 'r') as problem: timuhang = problem.readlines() a = [] question = timuhang[i].split() b='' for size in question[1]: if size not in "+-×÷()": b=b+size else: if b!='': a.append(b) b='' a.append(size) if b!='': a.append(b) with open(answerfile, 'r') as filea: text = filea.readlines() l = len(text) answer=text[i].split() result = answer[1] if str(result) == str(exchangesize(check(create_expression_tree(postfix_convert(a))))): correct.append(i + 1) else: wrong.append(i + 1) print('Correct: {}{}'.format(len(correct), tuple(correct)) + '\n' + 'Wrong: {}{}'.format(len(wrong), tuple(wrong)))
使用表达式树进行计算并避免运算过程出现假分数和负数
def postfix_convert(exp):
'''
将表达式字符串,转为后缀表达式
如exp = "1+2*(3-1)-4"
转换为:postfix = ['1', '2', '3', '1', '-', '*', '+', '4', '-']
'''
stack = [] # 运算符栈,存放运算符
postfix = [] # 后缀表达式栈
for char in exp:
# print char, stack, postfix
if char not in operator_precedence: # 非符号,直接进栈
postfix.append(char)
else:
if len(stack) == 0: # 若是运算符栈啥也没有,直接将运算符进栈
stack.append(char)
else:
if char == "(":
stack.append(char)
elif char == ")": # 遇到了右括号,运算符出栈到postfix中,并且将左括号出栈
while stack[-1] != "(":
postfix.append(stack.pop())
stack.pop()
elif operator_precedence[char] > operator_precedence[stack[-1]]:
# 只要优先级数字大,那么就继续追加
stack.append(char)
else:
while len(stack) != 0:
if stack[-1] == "(": # 运算符栈一直出栈,直到遇到了左括号或者长度为0
break
postfix.append(stack.pop()) # 将运算符栈的运算符,依次出栈放到表达式栈里面
stack.append(char) # 并且将当前符号追放到符号栈里面
while len(stack) != 0: # 如果符号站里面还有元素,就直接将其出栈到表达式栈里面
postfix.append(stack.pop())
return postfix
# ===========================这部分用于构造表达式树================================#
class Node(object):
def __init__(self, val):
self.val = val
self.left = None
self.right = None
def create_expression_tree(postfix):
"""
利用后缀表达式,构造二叉树
"1+2*(3-1)-4"
['1', '2', '3', '1', '-', '*', '+', '4', '-']
"""
stack = []
# print postfix
for char in postfix:
if char not in operator_precedence:
# 非操作符,即叶子节点
node = Node(char)
stack.append(node)
else:
# 遇到了运算符,出两个,进一个。
node = Node(char)
node.right = stack.pop()
node.left = stack.pop()
stack.append(node)
# 将最后一个出了即可。
return stack.pop()
# 避免出现负数,假分数
def check(tree):
if tree:
if tree.left == None:
return tree.val
num1 = check(tree.left)
num2 = check(tree.right)
if tree.val == '-':
if eval(num1 + '-' + num2) < 0:
supporter = tree.left
tree.left = tree.right
tree.right = supporter
return calculate(num2, num1, '-')
else:
return calculate(num1, num2, '-')
elif tree.val == '÷':
if eval(num1 + '/' + num2) > 1:
supporter = tree.left
tree.left = tree.right
tree.right = supporter
return calculate(num2, num1, '÷')
else:
return calculate(num1, num2, '÷')
elif tree.val == '+':
return calculate(num1, num2, '+')
elif tree.val == '×':
return calculate(num1, num2, '×')
# 将树输出为字符串
def exchangetree(tree, equation):
if tree:
if tree.left == None:
equation.append(tree.val)
else:
equation.append('(')
exchangetree(tree.left, equation)
equation.append(tree.val)
exchangetree(tree.right, equation)
equation.append(')')
六、测试结果
1、生成10000道题目的截图
2、生成10000道题目的答案截图
3、校对题目答案的截图
七、项目小结
- 本次任务分工实现,事先和队友分析了整个思路,确定了各自实现的部分,出现问题,相互讨论,最后汇总,一起实现剩下的功能板块以及优化。
- 在完成整个项目后,回顾整个项目,早期各自想思路时花费时间较多,也比较困难,在讨论之后的实现过程效率较单人思考会更高。首次合作,配合度也还不错,分工明确,效率高,但是由于单人思考时间花费的多了一些,导致后面的交流讨论不是很够,希望以后可以更合理的分配时间。