这个作业属于哪个课程 | 软件工程 |
---|---|
这个作业要求在哪里 | 结对项目 |
一、合作者
姓名 | 学号 |
---|---|
张楠 | 3222004599 |
周广 | 3122004418 |
github | 链接 |
二、PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 40 | 30 |
· Estimate | · 估计这个任务需要多少时间 | 30 | 30 |
Development | 开发 | 110 | 150 |
· Analysis | · 需求分析 (包括学习新技术) | 30 | 35 |
· Design Spec | · 生成设计文档 | 30 | 35 |
· Design Review | · 设计复审 (和同事审核设计文档) | 30 | 45 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 15 | 25 |
· Design | · 具体设计 | 40 | 50 |
· Coding | · 具体编码 | 350 | 420 |
· Code Review | · 代码复审 | 20 | 15 |
· Test | · 测试(自我测试,修改代码,提交修改) | 60 | 70 |
Reporting | 报告 | 80 | 75 |
· Test Report | · 测试报告 | 30 | 30 |
· Size Measurement | · 计算工作量 | 30 | 20 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 | 35 |
合计 | 885 | 1000 |
三、设计实现过程
1.代码组织
-
引入依赖库
-
main.rs
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>>{
let matches = miniopgenerator::get_args();
miniopgenerator::run_args(matches)
}
- get_args
// 获取命令行指令
pub fn get_args() -> ArgMatches {
let matches = Command::new("MyApp")
.version("1.0.0")
.author("zg")
.about("A mini quadratic operator generator")
.arg(
Arg::new("num")
.short('n')
.long("num")
.value_parser(value_parser!(usize))
.help("Use this parameter to control the number of generated topics"),
)
.arg(
Arg::new("realm")
.short('r')
.long("realm")
.value_parser(value_parser!(i32))
.help("Use this parameter to control the range of values in the title"),
)
.arg(
Arg::new("exercisefile")
.short('e')
.long("exercisefile")
.value_parser(value_parser!(String))
.help("Exercisefile name")
.group("input")
.requires("answerfile"),
)
.arg(
Arg::new("answerfile")
.short('a')
.long("answerfile")
.value_parser(value_parser!(String))
.help("Answerfile name")
.requires("input"),
)
.get_matches();
matches
}
- run_arg
// 执行命令
pub fn run_args(matches: ArgMatches) -> Result<(), Box<dyn Error>> {
if let (Some(num), Some(realm)) = (
matches.get_one::<usize>("num"),
matches.get_one::<i32>("realm"),
) {
generate_expression(num, realm);
}
if let (Some(exercisefile), Some(answerfile)) = (
matches.get_one::<String>("exercisefile"),
matches.get_one::<String>("answerfile"),
) {
check_answer(exercisefile, answerfile)?;
}
Ok(())
}
- write_to_file
// 写入文件
fn write_to_file(filename: &str, content: Vec<String>) -> std::io::Result<()> {
let mut file = File::create(filename).expect("创建文件失败!");
let mut ansfile = File::create("Answers.txt").expect("创建文件失败!");
for (index, exp) in content.iter().enumerate() {
write!(file, "{}. {} = \n", index + 1, exp)?;
write!(
ansfile,
"{}. {} = {}\n",
index + 1,
exp,
calculate_fraction(exp)
)?;
}
Ok(())
}
- check_answer
// 检查答案
fn check_answer(exercisefile: &String, answerfile: &String) -> io::Result<()> {
let exercises = fs::read_to_string(exercisefile)?;
let answers = fs::read_to_string(answerfile)?;
// 使用正则表达式来消去题目前面的序号
let re = Regex::new(r"\d+\. ").unwrap();
let mut correct_count = 0;
let mut incorrect_count = 0;
let mut correct_indices = Vec::new();
let mut incorrect_indices = Vec::new();
for (index, (question, answer)) in exercises.lines().zip(answers.lines()).enumerate() {
let tokens: Vec<_> = answer.split(' ').collect();
let question = &question[0..question.trim().len() - 1];
if let Some(captures) = re.captures(question) {
let calculate_ans = calculate_fraction(
&question[captures.get(0).unwrap().end()..]
.trim()
.to_string(),
);
if let Some(ans) = tokens.last() {
if ans.to_string() == calculate_ans {
correct_count += 1;
correct_indices.push(index + 1);
} else {
incorrect_count += 1;
incorrect_indices.push(index + 1);
}
}
}
}
let grade_result = format!(
"Correct: {} ({:?})\nWrong: {} ({:?})",
correct_count, correct_indices, incorrect_count, incorrect_indices
);
fs::write("Grade.txt", grade_result)?;
Ok(())
}
- Operator
// 定义一个枚举来表示四则运算符
enum Operator {
Add,
Subtract,
Multiply,
Divide,
}
// 为Operator实现一个方法,用于将其转换为字符串表示
impl Operator {
fn to_string(&self) -> &str {
match self {
Operator::Add => "+",
Operator::Subtract => "-",
Operator::Multiply => "*",
Operator::Divide => "/",
}
}
}
// 实现Distribution trait,以便我们可以使用rand::random()来生成Operator
impl Distribution<Operator> for Standard {
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> Operator {
match rng.gen_range(0..4) {
0 => Operator::Add,
1 => Operator::Subtract,
2 => Operator::Multiply,
_ => Operator::Divide,
}
}
}
- ExpressionNode
enum ExpressionNode {
Number(i32),
Operation(Box<ExpressionNode>, Operator, Box<ExpressionNode>),
}
// 实现一个方法来将表达式节点转换为字符串
impl ExpressionNode {
fn to_string(&self) -> String {
match self {
ExpressionNode::Number(num) => num.to_string(),
ExpressionNode::Operation(left, op, right) => {
let left_str = left.to_string();
let right_str = right.to_string();
match right.as_ref() {
ExpressionNode::Number(_) => {
return format!("{} {} {}", left_str, op.to_string(), right_str)
}
_ => (),
}
if rand::thread_rng().gen_bool(0.5) {
format!("{} {} ({})", left_str, op.to_string(), right_str)
} else {
format!("{} {} {}", left_str, op.to_string(), right_str)
}
}
}
}
}
- build_expression_tree
// 递归函数来构建表达式树
fn build_expression_tree(rng: &mut impl Rng, depth: usize, realm: i32) -> ExpressionNode {
let node = ExpressionNode::Number(rng.gen_range(1..realm));
let operator = rng.sample(Standard);
if depth > 1 {
// 随机决定是否继续添加运算符
if rng.gen_bool(0.5) {
let right = match operator {
Operator::Divide => {
// 确保除法结果为真分数
let numerator = match node {
ExpressionNode::Number(value) => value,
_ => panic!("获取随机值失败!"),
};
let denominator = rng.gen_range(1..realm) + numerator;
Box::new(ExpressionNode::Number(denominator))
}
Operator::Subtract => {
// 确保减法结果为正数
let numerator = match node {
ExpressionNode::Number(value) => value,
_ => panic!("获取随机值失败!"),
};
if numerator == 1 {
Box::new(ExpressionNode::Number(numerator))
} else {
Box::new(ExpressionNode::Number(
numerator - (rng.gen_range(1..numerator)),
))
}
}
_ => Box::new(build_expression_tree(rng, depth - 1, realm)),
};
ExpressionNode::Operation(Box::new(node), operator, right)
} else {
// 如果不添加运算符,直接返回
let right = generate_right(rng, &node, &operator, realm);
ExpressionNode::Operation(Box::new(node), operator, right)
}
} else {
// 如果达到最大深度,直接返回
let right = generate_right(rng, &node, &operator, realm);
ExpressionNode::Operation(Box::new(node), operator, right)
}
}
- generate_right
fn generate_right(
rng: &mut impl Rng,
node: &ExpressionNode,
operator: &Operator,
realm: i32,
) -> Box<ExpressionNode> {
match operator {
Operator::Divide => {
// 确保除法结果为真分数
let numerator = match *node {
ExpressionNode::Number(value) => value,
_ => panic!("获取随机值失败!"),
};
let denominator = rng.gen_range(1..realm) + numerator;
Box::new(ExpressionNode::Number(denominator))
}
Operator::Subtract => {
// 确保减法结果为正数
let numerator = match *node {
ExpressionNode::Number(value) => value,
_ => panic!("获取随机值失败!"),
};
if numerator == 1 {
Box::new(ExpressionNode::Number(numerator))
} else {
Box::new(ExpressionNode::Number(
numerator - (rng.gen_range(1..numerator)),
))
}
}
_ => Box::new(ExpressionNode::Number(rng.gen_range(1..realm))),
}
}
- add_brace
// 添加左括号
fn add_braces(exp: &str) -> String {
let operators = vec!['+', '-', '*', '/'];
let pos1 = exp.chars().position(|c| operators.contains(&c));
let pos2 = exp[pos1.unwrap() + 1..]
.chars()
.position(|c| operators.contains(&c));
match (pos1, pos2) {
(Some(index1), Some(index2)) => format!(
"({}){}",
&exp[0..(index1 + index2)],
&exp[(index1 + index2)..]
),
_ => exp.to_string(),
}
}
- generate_expression
fn generate_expression(num: &usize, realm: &i32) {
let mut rng = rand::thread_rng();
let mut vecexp: Vec<_> = Vec::new();
for _ in 0..*num {
// 构建一个包含最多三个运算符的表达式树
let expression_tree = build_expression_tree(&mut rng, 2, *realm);
// 将表达式树转换为字符串
let mut expression_str = expression_tree.to_string();
if expression_str.chars().last().unwrap() != ')' {
if rand::thread_rng().gen_bool(0.5) {
expression_str = add_braces(&expression_str);
}
}
vecexp.push(expression_str);
}
write_to_file("Exercises.txt", vecexp).expect("写入文件失败!");
}
- calculate_fraction
// 四则计算
fn calculate_fraction(exp: &String) -> String {
let tokens: Vec<&str> = exp.split(&[' ', '(', ')']).collect();
let (opening_index, closing_index) = find_parentheses_indices(&tokens);
match (opening_index, closing_index) {
(Some(op_index), Some(close_index)) => {
let num1 = tokens[op_index + 1].parse::<i32>().unwrap();
let num2 = tokens[close_index - 1].parse::<i32>().unwrap();
let op1 = tokens[op_index + 2];
let (op2, num3) = if op_index == 0 {
(
tokens[close_index + 1],
tokens[close_index + 2].parse::<i32>().unwrap(),
)
} else {
(tokens[op_index - 1], tokens[0].parse::<i32>().unwrap())
};
let ans = calculate(num1, num2, op1);
if op1 == "/" {
if op2 == "+" {
return format!("{}'{}", num3, ans);
} else if op2 == "*" {
return calculate(num1 * num3, num2, op1);
} else if op2 == "/" {
return calculate(num1, num2 * num3, op1);
} else {
return calculate(num1 - (num2 * num3), num2, op1);
}
} else {
let ans = ans.parse::<i32>().unwrap();
return calculate(ans, num3, op2);
}
}
_ => {
let num1 = tokens[0].parse::<i32>().unwrap();
let num2 = tokens[2].parse::<i32>().unwrap();
let op1 = tokens[1];
if tokens.len() < 5 {
calculate(num1, num2, op1)
} else {
let num3 = tokens[4].parse::<i32>().unwrap();
let op2 = tokens[3];
if op1 == "/" {
let ans = calculate(num1, num2, op1);
if op2 == "+" {
return format!("{}'{}", num3, ans);
} else if op2 == "*" {
return calculate(num1 * num3, num2, op1);
} else if op2 == "/" {
return calculate(num1, num2 * num3, op1);
} else {
return calculate(num1 - (num2 * num3), num2, op1);
}
} else if op1 == "*" {
return calculate(num1 * num2, num3, op2);
} else if op2 == "/" {
if op1 == "-" {
return calculate((num1 * num3) - num2, num3, op2);
} else {
return format!("{}'{}", num1, calculate(num2, num3, op2));
}
} else if op2 == "*" {
return calculate(num1, num2 * num3, op1);
} else {
let ans = calculate(num1, num2, op1).parse::<i32>().unwrap();
calculate(ans, num3, op2)
}
}
}
}
}
- calculate
fn calculate(num1: i32, num2: i32, op: &str) -> String {
if op == "+" {
(num1 + num2).to_string()
} else if op == "-" {
(num1 - num2).to_string()
} else if op == "*" {
(num1 * num2).to_string()
} else {
if num2 == 0 {
panic!("除数不能为0");
}
let common_divisor = gcd(num1, num2);
let num1 = num1 / common_divisor;
let num2 = num2 / common_divisor;
if num1 > num2 {
format!("{}'{}/{}", num1 / num2, num1 % num2, num2)
} else {
format!("{}/{}", num1, num2)
}
}
}
- gcd
fn gcd(a: i32, b: i32) -> i32 {
let mut a = a;
let mut b = b;
while b != 0 {
let temp = b;
b = a % b;
a = temp;
}
a
}
- find_parentheses_indice
fn find_parentheses_indices(vec: &Vec<&str>) -> (Option<usize>, Option<usize>) {
let opening_parenthesis_index = vec.iter().position(|&item| item == "");
let closing_parenthesis_index = vec.iter().rposition(|&item| item == "");
(opening_parenthesis_index, closing_parenthesis_index)
}
四、代码说明
1、关键代码
(1)get_args
使用 Rust 中的 clap 库来定义命令行参数,并解析用户提供的命令行参数
(2)run_args
根据命令行参数执行一些操作,包括生成四则运算表达式并写入文件,以及输出指定的文件名。
(3)build_expression_tree
根据给定的深度和数值范围生成一个四则运算表达式的表达式树。
(4)generate_right
根据当前节点的运算符生成一个合适的右子树节点,用于构建四则运算表达式的表达式树。
(5)calculate_fraction
计算这个数学表达式的结果,并返回结果的字符串形式。
(6)check_answer
计算出给的exercisefile的答案,与answerfile进行对比,将所得结果输出到Grade.txt中。
2、思路说明
(1)get_args
这个函数定义了一个名为 get_args 的公共函数(即 pub 关键字修饰的),其返回类型为 ArgMatches。ArgMatches 是 clap 库中的一个结构体,用于存储解析后的命令行参数信息。最后,通过调用 get_matches() 方法,对用户提供的命令行参数进行解析,并将解析结果存储在 matches 变量中,然后将 matches 变量作为函数的返回值返回。
(2)run_args
run_args函数根据命令行参数执行操作。首先,根据提供的参数生成四则运算表达式,确保数值范围符合要求,并将表达式写入文件。然后,检查是否指定了文件名参数,如果有,则输出相应的文件名。最后,返回执行成功的结果。
(3)build_expression_tree
build_expression_tree 函数用于构建四则运算表达式的表达式树。通过递归地生成表达式节点,首先生成根节点,然后根据给定的深度逐层生成子节点,直到达到最大深度。在生成节点过程中,根据随机选择的运算符,确保除法结果为真分数,减法结果为正数,保证生成的表达式符合规则。最后返回构建好的表达式树的根节点。
(4)enerate_right
generate_right 函数用于生成四则运算表达式的右子树节点。根据当前节点的运算符,确保生成的右子树节点满足特定条件,如除法结果为真分数,减法结果为正数。通过模式匹配和随机数生成来实现不同运算符下的条件判断和节点生成。最后返回生成的右子树节点。
(5)calculate_fraction
calculate_fraction接受一个数学表达式的字符串,计算其结果并返回。它首先将表达式拆分成片段,然后根据运算符的顺序和括号的位置进行递归计算。根据运算符的不同,采取不同的计算策略,最终得出表达式的结果。
(6)test_get_args_exercis
模拟了用户在命令行中输入参数的情况,然后创建了一个命令行应用程序,并定义了两个参数:exercisefile 和 answerfile,分别表示练习文件和答案文件的名称。接着,通过解析命令行参数,并使用断言来验证解析结果是否符合预期。如果测试通过,表示命令行参数解析功能正常运作。
五、测试运行
(1)生成100个算式
(2)下载算式
(3)统计结果输入到grade.txt
(4)下载答案与题目
六、项目小结
小结1:
周广:
虽然最初对项目的整体思路有所了解,但具体实现过程花费了更多时间。发现问题和解决方案之间的鸿沟可能比预期更大,特别是在处理重复表达式和添加多个小括号时遇到困难。即使有结对编程的支持,合作伙伴提供了一些帮助和思路,但整体而言,还是花费了相当多的时间在讨论和熟悉工具上。这表明了即使有初步的想法,实际的实现仍然可能面临各种挑战和耗时的任务。
小结2:
张楠:
刚刚开始接到项目时,我感到有些迷茫,不知道从哪里下手。但随着查阅资料和与搭档的交流,我逐渐理解了项目的大致思路。完成这个作业,很大程度上得益于有一个优秀的搭档。他在编程方面的能力远超过我,能够快速构思整个项目的框架,确定所需的主要函数,甚至提供了这些函数的实现思路。在短短的一个下午,他向我解释了他的构想,解答了我的疑问,并帮助我建立了项目的框架。有了他的帮助,我才得以顺利完成这个任务。