作业9.14:结对项目(四则运算)
这个作业属于哪个课程 | 班级链接 |
---|---|
这个作业要求在哪里 | 作业要求链接 |
这个作业的目标 | 按需求设计四则运算算法;学会任务分工。 |
Github 链接:博客正文首行 github 链接
项目成员:
姓名 | 学号 | Github链接 | 分工 |
---|---|---|---|
凌枫 | 3121005661 | https://github.com/Researcher-Feng/SoftwareEngineer_Calculator | 负责博客编写、程序的完善和代码 Bug 修复 |
陈卓恒 | 3122004905 | https://github.com/Jimmyczh231/SoftwareEngineer_Calculator | 负责程序的基本实现和测试、博客检查 |
一、基本介绍
开发环境
- 编程语言:python 3
- IDE:PyCharm 2024.2.1 (Professional Edition)
项目介绍
这是一个结对项目,我们共同实现了一个自动生成小学四则运算题目的程序。
该程序具有两个基本功能:
- 生成题目
- 答案校验
题目可以通过两种方式运行:
- 命令行方式
- 图形化界面
目录组织
.
├── .idea
├── 3121005661
├── 3122004905
├── old // 存放旧的代码版本
│ ├── main1.py
│ ├── main2.py
│ ├── main3.py
│ └── main4.py
├── test // 存放测试类
│ ├── graph.py // 代码调用关系图
│ ├── test.log // 空文件
│ ├── test_eval.py // 测试函数:字符串数学化
│ ├── test_evaluate.py // 测试函数:评估函数
│ ├── test_fraction_function.py // 测试函数:真分数字符串预处理
│ ├── test_generate_exercises.py // 测试函数:练习题生成
│ ├── test_generate_expression.py // 测试函数:表达式生成
│ ├── test_judge_function.py // 测试函数:答案校对
│ ├── test_logger.py // 测试函数:日志生成
│ └── test_valid.py // 测试函数:有效性验证
├── README.md // Github readme 文档
├── requirements.txt // 程序运行环境
└── main.py // 程序入口
二、效能分析
我们让程序生成 1000 道题目,数值上界为 6 ,在执行过程中分析程序的性能。
程序初始性能
如上表所示,如果按照Time
进行耗时的从大到小排序,可以发现,主程序main4
的总用时为 15738 ms,用时是比较久的。除了核心主程序main
和mainloop
,一个核心的自定义方法on_generate
和generate_exercises
是所有自定义方法中耗时最多的,用时大约为 6000 ms 左右,函数的作用是产生满足要求的练习题以及生成对应的答案。其余方法例如generate_expression
、evaluate_expression
和eval_expr
用时 2000 ms左右,这些方法都是需要优化的。
性能改进
基于上面的问题,我们对算法进行调整,对于输入的两个文本,我们直接使用轻量级的编辑距离计算相似度,如果相似度低于 50% ,为了确保查重的准确率,我们才调用自定义方法get_vocab
、使用BLEU
算法进行计算。性能优化后的情况如下:
程序优化后性能
我们对核心算法做了一定的改进,将性能提高了大约两倍。以前在生成题目的时候,我们的做法是:先随机生成一个表达式,然后遍历表达式中的符号,再逐个替换表达式中的符号(四种符号就能至少生成四个表达式),这样能够得到一组表达式,但是算法效率为 O(n2) ,效率很低;改进的做法是每次都随机生成,将算法效率降低为 O(n) ,因此上表所示各函数的执行时间均有所下降,其中main.py
函数的时间从 15738 ms 降低到 8156 ms。
模块性能
我们引用代码行级分析以实现对函数内部各行所用时间的统计,下图为主要函数的行级代码性能情况:
三、设计实现过程
整体设计
函数接口设计
函数 | 功能 |
---|---|
handle_cli_args() | 命令行模式的入口函数,分析用户输入的各种命令,执行相应程序 |
color_logger() | 彩色日志的初始化函数,用于命令行模式的输出 |
setup_gui() | 图形化界面的入口函数,根据用户输入,执行相应程序 |
on_grade() | 答案验证模块的入口函数,创建图形化界面,执行相应程序 |
on_generate() | 题目生成模块的入口函数,创建图形化界面,执行相应程序 |
open_file_dialog(msg_string) | 创建文件选择窗口的图形化界面,参数msg_string 为提示信息 |
generate_exercises(n, r) | 题目生成模块的功能处理函数,参数n 和r 分别为题目数量、数值上届 |
is_valid_expression(expr) | 表达式有效性验证函数,参数expr 为表达式字符串,返回布尔类型 |
eval_expr(expr) | 字符串数学化转换函数,参数expr 为表达式字符串,返回计算结果 |
evaluate_expression(expr) | 预处理字符串,提供eval_expr()函数的接口,返回布尔类型 |
generate_expression(r) | 表达式生成函数,参数r 为数值上届,返回表达式字符串 |
类与函数的调用关系
核心算法
本程序的一个核心函数是generate_exercises
函数,下面是它的流程图:
该函数用于生成n道练习题,每个练习包含一个算式和答案。下面是具体的过程:
函数首先初始化用于存储算式和答案的列表。接着,函数使用generate_expression()函数生成n个算式,直到生成的算式数量等于n。该函数会不断地生成新的算式,直到生成的算式不符合要求(例如答案为负数)。生成的每个算式都会与evaluate_expression()函数一起评估,以确定答案是否合法。如果答案合法,则将其添加到答案集中。如果答案集中已经包含了n个合法的答案,则继续生成新的算式。如果答案集中包含了80000个或更多的合法答案,则停止生成新的算式。生成的每个算式都会被转换为不含空格的字符串,以避免生成的算式与已有的算式重复。当函数生成了所有需要的练习后,它会将练习和答案组合在一起,并返回它们。
功能 1 - 8 的『 需求-实现 』对照
需求1
1.使用 -n 参数控制生成题目的个数,例如
Myapp.exe -n 10 将生成10个题目。
实现1
效果如下:
需求2
2.使用 -r 参数控制题目中数值(自然数、真分数和真分数分母)的范围,例如
Myapp.exe -r 10 将生成10以内(不包括10)的四则运算题目。该参数可以设置为1或其他自然数。该参数必须给定,否则程序报错并给出帮助信息。
实现2
实现原理:如下面展示的代码所示,我们通过分析用户输入的参数来判断是否需要给出错误提示,使用r_existed
变量来记录用户是否输入-r
参数。如果用户没有给定该参数,程序会执行logger.error
输出错误信息,然后通过help_msg()
给出帮助信息。
# 检测参数输入错误
if (r_existed == 1 and n_existed == 0) or (r_existed == 0 and n_existed == 1) \
or (e_existed == 1 and a_existed == 0) or (e_existed == 0 and a_existed == 1) or \
(r_existed == 0 and n_existed == 0 and e_existed == 0 and a_existed == 0):
logger.error("选项的参数输入有误,请重新输入。\n输入以下命令可以获得帮助:python main.py -h")
help_msg()
sys.exit()
效果如下:
需求3
3.生成的题目中计算过程不能产生负数,也就是说算术表达式中如果存在形如e1− e2的子表达式,那么e1≥ e2。
实现3
实现原理:如下面展示的代码所示,我们为了确保不能产生负数,实现了“双保险”:不仅在计算每个减法子表达式(负数只可能在减法中产生)的时候,确保不能产生负数,还对最终结果进行验证,如果最终结果是负数,那么删除这道习题。
# 计算每个子表达式的时候确保不能产生负数
for _ in range(random.randint(1, 3)):
op = random.choice(ops)
next_num = generate_number()
if random.choice([True, False]):
expression = f"({expression} {op} {next_num})"
added_parentheses = True # Set flag to True
if op == '-':
if evaluate_expression(expression) < 0:
return -1
else:
expression += f" {op} {next_num}"
added_parentheses = False
# 最终结果确保不能产生负数
while len(exercises) < n:
expr = generate_expression(r)
if expr == -1:
continue
answer = evaluate_expression(expr)
if answer < 0:
continue
效果如下:
需求4
4.生成的题目中如果存在形如e1÷ e2的子表达式,那么其结果应是真分数。
实现4
实现原理:如下面展示的代码所示,我们对于所有随机生成的分数和结果中的分数进行了下述处理方式,即使用Fraction
库,对分数进行精确表示,对结果进行分类处理,分为:真分数、纯数字、带分数 三种情况。确保不会出现小数、假分数等非法情况。
# 分数和除法的逆向格式化(转带分数)
def convert_fraction_to_mixed(fraction_str):
# 输入处理
if '/' not in fraction_str:
return fraction_str # If it's not a fraction, return as is
# 除法表达式拆分
try:
numerator, denominator = map(int, fraction_str.split('/'))
except ValueError:
return None # 处理非整数
if denominator == 0:
return None # 处理特殊情况
whole = numerator // denominator
remainder = abs(numerator) % denominator # 用绝对值处理负数情况
if whole == 0:
return f"{remainder}/{denominator}" # 真分数
elif remainder == 0:
return str(whole) # 纯数字
else:
return f"{whole}'{remainder}/{denominator}" # 带分数
效果如下:
需求5
5.每道题目中出现的运算符个数不超过3个。
实现5
实现原理:如下面展示的代码所示,我们通过random.randint(1, 3)
确保生成的运算符在3个以内。
for _ in range(random.randint(1, 3)):
op = random.choice(ops)
效果如下:
需求6
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
四则运算题目2
...
其中真分数在输入输出时采用如下格式,真分数五分之三表示为3/5,真分数二又八分之三表示为2’3/8。
实现6
实现原理:根据常识,如果题目能通过有限次交换+和×左右的算术表达式变换为另一道题目,那么他们结果一定是相同的;反之,如果两道题目的答案不同,他们不能通过有限次交换+和×左右的算术表达式变换为同一道题目。由此可见,他们互为充要条件。因此,只需要确保题目的答案不同,就能确保任何两道题目不能通过有限次交换+和×左右的算术表达式变换为同一道题目。为了方便起见,我们设置一个answer_set
,每次生成答案都检查答案是否在answer_set
中。虽然存在一定的性能问题,但是它能 100% 确保正确性。代码如下:
if answer in answer_set:
continue
answer_set.add(answer)
需求7
7.在生成题目的同时,计算出所有题目的答案,并存入执行程序的当前目录下的Answers.txt文件,格式如下:
- 答案1
- 答案2
特别的,真分数的运算如下例所示:1/6 + 1/8 = 7/24。
实现7
效果如下:
需求8
8.程序应能支持一万道题目的生成。
实现8
对于一万道题目的生成,虽然可以正确执行且每道题均符合要求,但是可能会比较耗时的,时间可能取决于每个人电脑的配置(型号、核心数、主频)。我们测量了在两个不同的电脑配置下,生成不同数量的题目所需的时间,仅供参考:
型号 | 核心数 | 主频 | 要求生成题目数 | 数值上界 | 用时 |
---|---|---|---|---|---|
Intel i7-8565U | 8核 | 1.8GHz | 10000 | 10 | 1分32秒 |
Intel i7-8565U | 8核 | 1.8GHz | 2000 | 7 | 1分02秒 |
apple m2 8g | 8核 | 8GHz | 10000 | 10 | 1分25秒 |
apple m2 8g | 8核 | 8GHz | 2000 | 7 | 17秒 |
效果如下:
需求9
9.程序支持对给定的题目文件和答案文件,判定答案中的对错并进行数量统计,输入参数如下:
Myapp.exe -e
.txt -a .txt 统计结果输出到文件Grade.txt,格式如下:
Correct: 5 (1, 3, 5, 7, 9)
Wrong: 5 (2, 4, 6, 8, 10)其中“:”后面数字5表示对/错的题目的数量,括号内的是对/错题目的编号。为简单起见,假设输入的题目都是按照顺序编号的符合规范的题目。
实现9
效果如下:
四、代码说明
函数 handle_cli_args()
# 命令行模式-处理逻辑
@profile
def handle_cli_args():
number = 0
ranging = 0
exercise_file = " "
answer_file = " "
try:
opts, args = getopt.getopt(sys.argv[1:], "hn:r:e:a:", ["help", "number=", "range=", "exercise_file=", "answer_file="])
except:
logger.error("选项的参数输入有误,请重新输入。\n输入以下命令可以获得帮助:python main.py -h")
sys.exit()
# 从左到右:是否包含生成题目数量、是否包含数值范围上届、是否包含题目路径、是否包含答案路径
n_existed, r_existed, e_existed, a_existed = 0, 0, 0, 0
for opt, arg in opts:
if opt in ("-h", "--help"): # 显示帮助
help_msg()
sys.exit()
if opt in ("-n", "--number"):
try:
number = int(arg)
if number <= 0:
logger.error("选项的参数输入有误,请重新输入。\n输入以下命令可以获得帮助:python main.py -h")
return
except:
logger.error("选项的参数输入有误,请重新输入。\n输入以下命令可以获得帮助:python main.py -h")
sys.exit()
if n_existed == 0:
n_existed = 1
elif opt in ("-r", "--ranging"):
try:
ranging = int(arg)
if ranging > 10 or ranging <= 1:
logger.error("选项的参数输入有误,请重新输入。\n输入以下命令可以获得帮助:python main.py -h")
return
except:
logger.error("选项的参数输入有误,请重新输入。\n输入以下命令可以获得帮助:python main.py -h")
sys.exit()
if r_existed == 0:
r_existed = 1
elif opt in ("-e", "--exercise_file"):
exercise_file = arg
if e_existed == 0:
e_existed = 1
elif opt in ("-a", "--answer_file"):
answer_file = arg
if a_existed == 0:
a_existed = 1
else:
help_msg()
sys.exit()
# 检测参数输入错误
if (r_existed == 1 and n_existed == 0) or (r_existed == 0 and n_existed == 1) \
or (e_existed == 1 and a_existed == 0) or (e_existed == 0 and a_existed == 1) or \
(r_existed == 0 and n_existed == 0 and e_existed == 0 and a_existed == 0):
logger.error("选项的参数输入有误,请重新输入。\n输入以下命令可以获得帮助:python main.py -h")
help_msg()
sys.exit()
# 执行生成四则运算题目功能
if n_existed and r_existed:
exercises, answers = generate_exercises(number, ranging)
with open("Exercises.txt", 'w', encoding='utf-8') as ef, open("Answers.txt", 'w', encoding='utf-8') as af:
ef.write("\n".join(exercises))
af.write("\n".join(answers))
logger.info("\n已随机生成四则运算题目与对应答案,分别存入当前目录下的 Exercises.txt 文件和 Answers.txt 文件")
# 执行判定答案对错功能
if e_existed and a_existed:
correct, wrong, invalid = judge_function(exercise_file, answer_file)
print(f"判题结果:\nCorrect: {len(correct)} ({', '.join(map(str, correct))})")
print(f"Wrong: {len(wrong)} ({', '.join(map(str, wrong))})")
if invalid:
print(f"Invalid: {len(invalid)} ({', '.join(map(str, invalid))})")
logger.info("\n判题完成,结果已保存到当前目录下的 Grade.txt")
sys.exit()
该函数用于处理命令行参数的,使用了Python的getopt模块来解析命令行参数,并使用了profile装饰器来生成代码运行时性能分析数据。handle_cli_args()函数的主要功能是解析命令行参数,并根据不同的参数执行不同的操作。
- 首先,使用try/except块来捕获getopt模块可能出现的异常。
- 然后,使用for循环遍历opts列表,并获取每个选项的名称和值。
如果选项是-h或--help,则调用help_msg()函数显示帮助信息并退出程序。
如果选项是-n或--number,则将arg转换为整数并检查其是否有效。如果有效,则将其存储在number变量中。
如果选项是-r或--ranging,则将arg转换为整数并检查其是否有效。如果有效,则将其存储在ranging变量中。
如果选项是-e或--exercise_file,则将arg存储在exercise_file变量中。
如果选项是-a或--answer_file,则将arg存储在answer_file变量中。 - 接下来,使用 if 语句检测参数输入是否有误。
如果r_existed为1且n_existed为0,或者r_existed为0且n_existed为1,或者e_existed为1且a_existed为0,或者e_existed为0且a_existed为1,或者r_existed为0且n_existed为0且e_existed为0且a_existed为0,则说明参数输入有误,调用help_msg()函数显示帮助信息并退出程序。
如果r_existed为1且n_existed为1,则执行generate_exercises()函数生成指定数量的四则运算题目和答案,并将题目和答案分别存储在exercises和answers变量中。接着,使用with语句打开两个文件,分别将题目和答案写入Exercises.txt和Answers.txt文件。最后,记录生成的题目和答案的数量并输出到控制台。
如果e_existed为1且a_existed为1,则执行judge_function()函数判断题目和答案的对错,并将正确、错误和无效答案分别存储在correct、wrong和invalid变量中。接着,使用print()函数输出判题结果,包括正确答案、错误答案和无效答案的数量和内容。最后,记录判题结果并输出到控制台。
函数 generate_exercises()
@profile
def generate_exercises(n, r, bar=False):
exercises = []
answers = []
seen_expressions = set()
answer_set = set()
progress = None
root = None
if bar:
# 创建 Tkinter 窗口
root = tk.Tk()
root.title("加载中...")
progress = ttk.Progressbar(root, orient="horizontal", length=300, mode="determinate")
progress.pack(pady=20)
progress['value'] = 0
progress['maximum'] = n
while len(exercises) < n:
expr = generate_expression(r)
if expr == -1:
continue
answer = evaluate_expression(expr)
if answer < 0:
continue
answer = convert_fraction_to_mixed(str(answer))
if answer in answer_set:
continue
answer_set.add(answer)
if answer is not None:
normalized_expr = expr.replace(" ", "")
if normalized_expr not in seen_expressions:
exercises.append(expr + " = ")
answers.append(str(answer))
seen_expressions.add(normalized_expr)
if bar:
progress['value'] += 1 # 更新进度条
root.update() # 更新窗口以反映进度条变化
if bar:
progress['value'] = n
time.sleep(0.002)
root.destroy() # 关闭窗口
# 行号
numbered_exercises = [f"{i + 1}. {exercise}" for i, exercise in enumerate(exercises)]
numbered_answers = [f"{i + 1}. {answer}" for i, answer in enumerate(answers)]
return numbered_exercises, numbered_answers
函数generate_exercises()用于生成n道数学题,每个题目包含一个等式和一个答案。
- 函数首先创建两个列表,用于存储生成的题目和答案。然后,它使用generate_expression()函数生成n个等式,直到生成的题目数量等于n。
- 然后,该函数使用evaluate_expression()函数评估每个等式,并检查答案是否有效。如果答案有效,则将其添加到答案列表中。
- 接着,该函数使用convert_fraction_to_mixed()函数将答案转换为真分数和带分数的形式,以便更容易理解。如果答案已经在答案列表中,则不添加该等式,避免生成重复的等式。
- 最后,如果bar为True,则使用tkinter创建了一个窗口,并显示一个进度条,程序结束后会销毁窗口和进度条。
函数 judge_function()
@profile
def judge_function(exercise_file, answer_file):
with open(exercise_file, 'r', encoding='utf-8') as ef, open(answer_file, 'r', encoding='utf-8') as af:
exercises = ef.readlines()
answers = af.readlines()
correct = []
wrong = []
invalid = []
for i, (e, a) in enumerate(zip(exercises, answers), 1):
# Remove the numbering from exercises and answers
expr = e.split('.', 1)[-1].split('=')[0].strip() # Get expression without the number
answer = a.split('.', 1)[-1].strip() # Get answer without the number
if not is_valid_expression(expr):
invalid.append(i)
continue
if str(evaluate_expression(expr)) == convert_mixed_to_fraction(answer):
correct.append(i)
else:
wrong.append(i)
with open('Grade.txt', 'w', encoding='utf-8') as gf:
gf.write(f"Correct: {len(correct)} ({', '.join(map(str, correct))})\n")
gf.write(f"Wrong: {len(wrong)} ({', '.join(map(str, wrong))})\n")
if invalid:
gf.write(f"Invalid: {len(invalid)} ({', '.join(map(str, invalid))})\n")
return correct, wrong, invalid
该函数为答案校对函数。
- 函数
judge_function
接受两个文件路径作为输入,分别表示练习文件和答案文件。它将读取这两个文件,并将它们分隔为一系列的练习和答案对。 - 然后,遍历这些练习和答案对,并使用
is_valid_expression
函数检查每个表达式是否有效。 - 接着,执行评估操作,使用
evaluate_expression
函数评估每个表达式。如果答案正确,则将其添加到correct
列表中;如果答案错误,则将其添加到wrong
列表中。如果表达式无效,则将其添加到invalid
列表中。 - 评估后,使用
convert_mixed_to_fraction
函数将答案转换为混合数。 - 最后,函数将打开
Grade.txt
文件,将正确、错误和无效的题目数量写入文件,并返回正确、错误和无效的题目列表。
五、测试运行
单元测试概览
测试功能 | 测试文件 | 测试内容 | 测试覆盖率 |
---|---|---|---|
计算算式 | test_eval.py | 测试算术表达式是否正确处理,验证基本的加、减、乘、除运算 | 94% |
算式处理 | test_evaluate.py | 测试基本算式的计算,检查对不同算式的处理 | 95% |
分数转换 | test_fraction_function.py | 测试混合数分数转换的正确性,包括不同输入格式的验证和无效输入的处理 | 95% |
算式生成 | test_generate_expression.py | 测试生成表达式的有效性,包括允许的运算符和括号的匹配 | 95% |
生成完整练习 | test_generate_exercise.py | 测试生成的习题数量和答案的唯一性,确保没有重复题目 | 96% |
批改算式和答案 | test_judge_function.py | 测试评估习题的功能,检查正确答案、错误答案和无效表达式的处理 | 98% |
日志创建处理 | test_logger.py | 测试表达式有效性的检查,包括运算符连续出现和括号配对的验证 | 94% |
检查算式 | test_valid.py | 测试表达式有效性的检查,包括运算符连续出现和括号配对的验证 | 94% |
测试函数的设计
由于测试比较多,下面仅以judge_function
函数的测试函数为例,介绍测试函数的设计,其余测试大同小异。
针对 judge_function
函数,我们设计了两个主要的测试函数,以确保该函数能够准确评估输入的算术题和答案。以下是这两个测试函数的基本特点:
特征 | 说明 |
---|---|
函数功能 | 评估给定的算术题及其对应的答案,返回正确、错误及无效答案的索引 |
输入 | 包含算术题的文本文件和包含答案的文本文件 |
输入的数据类型 | 均为文本文件,包含题目及答案的字符串 |
输出 | 三个列表,分别包含正确答案、错误答案和无效答案的索引 |
针对这一功能,我们设计了以下测试函数:
-
test_judge_function:
- 测试标准的算术题和答案,确保正确的答案被正确记录,错误的答案和无效的表达式得到合理处理。该函数涵盖了正常计算的情况,验证了正确、错误和无效索引的返回。
-
test_invalid_expression:
- 测试包含无效表达式的情况,确保无效表达式被准确识别并记录。该函数验证了
judge_function
对无效输入的处理能力。
- 测试包含无效表达式的情况,确保无效表达式被准确识别并记录。该函数验证了
设计小结
这两个测试函数通过涵盖不同类型的输入和预期输出,确保了 judge_function
在处理各种算术题及答案时的准确性和鲁棒性。这次项目中对代码模块化要求较高,针对不同模块功能的异常处理分散,测试函数和类比较不会有较多的测试。在实际开发中,应继续扩展测试以处理更多边界情况和异常输入。
测试代码
- test_evaluate.py
import unittest
from fractions import Fraction
from main import eval_expr
class TestEvalExpr(unittest.TestCase):
def test_valid_expressions(self):
"""Test if valid expressions evaluate correctly"""
valid_expressions = [
"3 + 2",
"4 - 1 + 1",
"5 * 2",
"(1/2 + 1/2)",
"10 / 2"
]
expected_results = [
5, # 3 + 2
4, # 4 - 1 + 1
10, # 5 × 2
1, # 1/2 + 1/2
5 # 10 ÷ 2
]
for expr, expected in zip(valid_expressions, expected_results):
with self.subTest(expr=expr):
self.assertEqual(eval_expr(expr), expected)
def test_fraction_expressions(self):
"""Test if fraction expressions evaluate correctly"""
fraction_expressions = [
"1/2 + 1/2",
"1/3 + 2/3",
"(1/4 + 3/4) * 2"
]
expected_results = [
1, # 1/2 + 1/2
1, # 1/3 + 2/3
2 # (1/4 + 3/4) × 2
]
for expr, expected in zip(fraction_expressions, expected_results):
with self.subTest(expr=expr):
self.assertEqual(eval_expr(expr), expected)
if __name__ == '__main__':
unittest.main()
- test_evaluate.py
import unittest
import re
from main import evaluate_expression
class TestEvaluateExpression(unittest.TestCase):
def test_valid_expressions(self):
"""Test if valid expressions evaluate correctly"""
valid_expressions = [
"3 + 5",
"10 - 2 × 3",
"8 ÷ 4 + 1",
"1/2 + 1/2",
"5 × (3 - 1)"
]
expected_results = [
8, # 3 + 5
4, # 10 - 2 × 3
3.0, # 8 ÷ 4 + 1
1.0, # 1/2 + 1/2
10 # 5 × (3 - 1)
]
for expr, expected in zip(valid_expressions, expected_results):
with self.subTest(expr=expr):
self.assertEqual(evaluate_expression(expr), expected)
def test_division_by_zero(self):
"""Test if division by zero returns None"""
expr = "1 ÷ 0"
self.assertIsNone(evaluate_expression(expr))
def test_invalid_expressions(self):
"""Test if invalid expressions return None"""
invalid_expressions = [
"2 + ",
"× 5",
"10 ÷ a",
"5 / (3 - 3)",
"1/0 + 1",
"" # Test for empty string
]
for expr in invalid_expressions:
with self.subTest(expr=expr):
self.assertIsNone(evaluate_expression(expr))
if __name__ == '__main__':
unittest.main()
- test_fraction_function.py
import unittest
from fractions import Fraction
from main import convert_mixed_to_fraction, convert_fraction_to_mixed
class TestConversionFunctions(unittest.TestCase):
def test_convert_mixed_to_fraction(self):
"""Test the conversion of mixed numbers to fractions"""
# Test conversion of mixed numbers to fractions
self.assertEqual(convert_mixed_to_fraction("3'1/2"), "7/2")
self.assertEqual(convert_mixed_to_fraction("2'3/4"), "11/4")
self.assertEqual(convert_mixed_to_fraction("0'1/3"), "1/3")
# Test conversion of multiple mixed numbers
self.assertEqual(convert_mixed_to_fraction("3'1/2 + 1'1/4"), "7/2 + 5/4")
# Test that expressions without mixed numbers remain unchanged
self.assertEqual(convert_mixed_to_fraction("5/2 + 1/4"), "5/2 + 1/4")
def test_convert_fraction_to_mixed(self):
"""Test the conversion of fractions to mixed numbers"""
# Test conversion of fractions to mixed numbers
self.assertEqual(convert_fraction_to_mixed("7/2"), "3'1/2")
self.assertEqual(convert_fraction_to_mixed("11/4"), "2'3/4")
self.assertEqual(convert_fraction_to_mixed("1/3"), "1/3")
# Test conversion of fractions to whole numbers
self.assertEqual(convert_fraction_to_mixed("6/3"), "2")
# Test conversion of fractions to whole numbers
self.assertEqual(convert_fraction_to_mixed("6"), "6")
# Test invalid input
self.assertIsNone(convert_fraction_to_mixed("5/a"))
self.assertIsNone(convert_fraction_to_mixed("1/0")) # Case where denominator is zero
if __name__ == '__main__':
unittest.main()
- test_generate_exercises.py
import unittest
from main import generate_exercises
class TestGenerateExercises(unittest.TestCase):
def test_generate_exercises(self):
"""Test if generate_exercises produces the correct number of exercises and unique answers."""
n = 10 # Number of exercises to generate
r = 10 # Range for numbers
exercises, answers = generate_exercises(n, r)
# Check that we have the correct number of exercises
self.assertEqual(len(exercises), n)
self.assertEqual(len(answers), n)
# Check for duplicates in exercises
self.assertEqual(len(exercises), len(set(exercises)))
# Check for duplicates in answers
self.assertEqual(len(answers), len(set(answers)))
# Check for negative answers
for answer in answers:
# 检查答案是否包含负号
self.assertFalse('-' in answer, f"Invalid answer: {answer} is negative")
def test_generate_exercises_with_progress_bar(self):
"""Test if generate_exercises with a progress bar works correctly."""
n = 5 # Number of exercises to generate
r = 5 # Range for numbers
exercises, answers = generate_exercises(n, r, bar=True)
# Check that we have the correct number of exercises
self.assertEqual(len(exercises), n)
self.assertEqual(len(answers), n)
# Check for duplicates in answers
self.assertEqual(len(answers), len(set(answers)))
if __name__ == '__main__':
unittest.main()
- test_generate_expression.py
import unittest
import random
from main import generate_expression, evaluate_expression
class TestGenerateExpression(unittest.TestCase):
def setUp(self):
self.r = 10 # Set the range
self.iterations = 100 # Set the number of test iterations
def test_generate_expression_basic(self):
for _ in range(self.iterations):
expression = generate_expression(self.r)
if expression == -1:
continue
# Check if the expression contains numbers or fractions
self.assertTrue(any(char.isdigit() for char in expression))
self.assertTrue(any(char in ['/', ' ', '(', ')'] for char in expression))
def test_generate_expression_format(self):
for _ in range(self.iterations):
expression = generate_expression(self.r)
if expression == -1:
continue
# Check the format of the expression
self.assertIsInstance(expression, str)
self.assertTrue(len(expression) > 0)
if __name__ == '__main__':
unittest.main()
- test_judge_function.py
import unittest
import os
from main import judge_function, is_valid_expression, evaluate_expression, convert_mixed_to_fraction
class TestJudgeFunction(unittest.TestCase):
def setUp(self):
# 创建临时的输入文件
self.exercise_file = 'test_exercises.txt'
self.answer_file = 'test_answers.txt'
self.grade_file = 'Grade.txt'
with open(self.exercise_file, 'w', encoding='utf-8') as ef:
ef.write("1. 3 + 5 =\n")
ef.write("2. 10 - 2 =\n")
ef.write("3. 6 ÷ 0 =\n")
ef.write("4. 8 × 2 =\n")
ef.write("5. 1/2 + 1/2 =\n")
with open(self.answer_file, 'w', encoding='utf-8') as af:
af.write("1. 8\n")
af.write("2. 8\n")
af.write("3. undefined\n") # Invalid case
af.write("4. 16\n")
af.write("5. 1\n")
def tearDown(self):
# 删除临时文件
os.remove(self.exercise_file)
os.remove(self.answer_file)
if os.path.exists(self.grade_file):
os.remove(self.grade_file)
def test_judge_function(self):
correct, wrong, invalid = judge_function(self.exercise_file, self.answer_file)
# 检查正确和错误的索引
self.assertEqual(correct, [1, 2, 4, 5]) # 期望的正确答案索引
self.assertEqual(wrong, []) # 无错误
self.assertEqual(invalid, [3]) # 包含无效答案的索引
def test_invalid_expression(self):
# 测试无效表达式
with open(self.exercise_file, 'w', encoding='utf-8') as ef:
ef.write("1. 3 + 5 =\n")
ef.write("2. invalid_expression\n") # 无效表达式
with open(self.answer_file, 'w', encoding='utf-8') as af:
af.write("1. 8\n")
af.write("2. 0\n") # 任意答案
correct, wrong, invalid = judge_function(self.exercise_file, self.answer_file)
self.assertEqual(invalid, [2]) # 确保无效表达式被记录
if __name__ == '__main__':
unittest.main()
- test_logger.py
import logging
import unittest
from main import color_logger
class TestColorLogger(unittest.TestCase):
def setUp(self):
"""Executed before each test, initializing the logger"""
self.logger = color_logger()
def test_logger_not_none(self):
"""Test if the logger is successfully created"""
self.assertIsNotNone(self.logger)
def test_logger_has_handlers(self):
"""Test if the logger has handlers"""
self.assertGreater(len(self.logger.handlers), 0)
def test_logger_level(self):
"""Test the logging level of the logger"""
self.assertEqual(self.logger.level, logging.DEBUG)
def test_console_handler_level(self):
"""Test the logging level of the console handler"""
console_handler = self.logger.handlers[0]
self.assertEqual(console_handler.level, logging.DEBUG)
def test_file_handler_level(self):
"""Test the logging level of the file handler"""
file_handler = self.logger.handlers[1]
self.assertEqual(file_handler.level, logging.INFO)
if __name__ == '__main__':
unittest.main()
- test_valid.py
import unittest
from main import is_valid_expression
class TestIsValidExpression(unittest.TestCase):
def test_valid_expressions(self):
"""Test if valid expressions return True"""
valid_expressions = [
"3 + 2",
"4 - 1 + 1",
"5 × 2",
"(1/2 + 1/2)",
"10 ÷ 2"
]
for expr in valid_expressions:
with self.subTest(expr=expr):
self.assertTrue(is_valid_expression(expr))
def test_invalid_expressions(self):
"""Test if invalid expressions return False"""
invalid_expressions = [
"2 + ",
"3 ÷ 0",
"(3 - 1",
"× 5",
"10 ÷ a",
"3 + 5 ×",
"3 ++ 5",
"3 + (5 * 2"
]
for expr in invalid_expressions:
with self.subTest(expr=expr):
self.assertFalse(is_valid_expression(expr))
def test_empty_string(self):
"""Test if empty string returns False"""
self.assertFalse(is_valid_expression(""))
if __name__ == '__main__':
unittest.main()
测试覆盖率截图
六、PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 20 |
· Estimate | · 估计这个任务需要多少时间 | 30 | 20 |
Development | 开发 | 1100 | 1800 |
· Analysis | · 需求分析 (包括学习新技术) | 20 | 24 |
· Design Spec | · 生成设计文档 | 20 | 16 |
· Design Review | · 设计复审 | 10 | 11 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 30 | 15 |
· Design | · 具体设计 | 120 | 90 |
· Coding | · 具体编码 | 600 | 1424 |
· Code Review | · 代码复审 | 60 | 40 |
· Test | · 测试(自我测试,修改代码,提交修改) | 240 | 180 |
Reporting | 报告 | 160 | 134 |
· Test Repor | · 测试报告 | 120 | 95 |
· Size Measurement | · 计算工作量 | 10 | 13 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 | 26 |
合计 | 1290 | 1954 |
七、结对感受
凌枫感受:在这次结对项目中,我主要负责代码 Bug 的修复、博客的编写,我认为最终的程序是完美的,因为它实现了全部的需求,而且既能通过命令行运行、又能通过图形化界面运行,这得益于开发过程中两个人的合作与交流。实际上,在代码开发中,我们都遇到了很多问题,而我的同伴发现了很多我没有发现的问题,这对项目帮助很大。
陈卓恒感受:在这次结对项目中,我主要负责程序的基本实现、博客的检查和基本的代码测试。最终的程序不仅达成了预期目标,成功实现了所有需求,还能够通过命令行和图形化界面运行。这一切得益于我与同伴之间的良好合作与沟通。在开发过程中,我们遇到了一些挑战,我的同伴提出了许多宝贵的建议,这对项目的进展起到了至关重要的作用。
commit 提交记录: