结对项目
结对项目
这个作业属于哪个课程 | 班级链接 |
---|---|
这个作业要求在哪里 | 结对项目 - 作业 - 计科22级12班 - 班级博客 - 博客园 (cnblogs.com) |
这个作业的目标 | 实现一个自动生成小学四则运算题目的命令行程序 |
姓名 | 学号 |
---|---|
韩其锟 | 3122004348 |
GitHub
https://github.com/chocohQL/3122004348-02
PSP
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 10 | 10 |
· Estimate | · 估计这个任务需要多少时间 | 10 | 10 |
Development | 开发 | 400 | 490 |
· Analysis | · 需求分析 (包括学习新技术) | 30 | 20 |
· Design Spec | · 生成设计文档 | 10 | 10 |
· Design Review | · 设计复审 | 10 | 5 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 10 | 5 |
· Design | · 具体设计 | 60 | 80 |
· Coding | · 具体编码 | 200 | 300 |
· Code Review | · 代码复审 | 20 | 10 |
· Test | · 测试(自我测试,修改代码,提交修改) | 60 | 60 |
Reporting | 报告 | 30 | 40 |
· Test Repor | · 测试报告 | 20 | 25 |
· Size Measurement | · 计算工作量 | 5 | 5 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 5 | 10 |
合计 | 440 | 540 |
运行结果
生成一万条题目:
生成10条题目,随机修改3个错误答案,进行判定统计:
系统设计
系统核心的设计思路: Application 类负责整体流程运行,下层抽象出一个 ProblemGenerator 负责具体题目和答案的生成,将题目表达式和答案封装到 Problem 实体类中。由于需要进行分数的计算,将每个数统一使用 Fraction 表示,实体类内部提供分数的加减乘除运算,使用构造函数创建分数自动进行化简,重写 toString 方法生成字符串。生成答案的思路是将表达式解析为操作数栈和数值栈,不断弹栈进行分数计算,并保证操作优先级。底层封装 FileUtil 工具类提供文件输入输出方法。
调用链:
-
Main 主函数:启动程序。
-
Application 程序:运行程序、解析参数、选择方法、调用 ProblemGenerator 生成题目、判定答案、调用 FileUtil 输入输出文件。
-
ProblemGenerator 问题生成器:生成具体问题和答案、判定答案等。
-
FileUtil 文件工具类:负责输入输出字符串行到指定文件中。
实体类:
- Problem 问题:记录表达式 exercises 和答案 answers 。
- Fraction 分数:进行分数的加减乘除计算,构造函数传入分子和分母自动进行简化。
具体实现
Application
public class Application {
/**
* 启动程序
*/
public static void run(String[] args) {
try {
Map<String, String> params = parseParams(args);
if (params.get("-r") != null && params.get("-n") != null) {
generateProblems(params.get("-n"), params.get("-r"));
} else if (params.get("-e") != null && params.get("-a") != null) {
judgeProblems(params.get("-e"), params.get("-a"));
} else {
throw new RuntimeException("请检查参数 [-r][-n] | [-e][-a]");
}
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
/**
* 解析参数
*/
public static Map<String, String> parseParams(String[] args) {
if (args.length != 4) {
throw new RuntimeException("无法解析参数");
}
Map<String, String> params = new HashMap<>();
for (int i = 0; i < args.length; i += 2) {
params.put(args[i], args[i + 1]);
}
return params;
}
/**
* 生成问题
*/
public static void generateProblems(String n, String r) {
int num = Integer.parseInt(n);
int range = Integer.parseInt(r);
if (num < 0 || range < 0) {
throw new RuntimeException("参数必须为自然数");
}
num = Math.min(10000, num);
List<Problem> problems = new ArrayList<>();
for (int i = 0; i < num; i++) {
// 生成题目
problems.add(ProblemGenerator.generateProblem(range));
}
// 输出结果
List<String> exercises = new ArrayList<>();
List<String> answers = new ArrayList<>();
for (int i = 0; i < num; i++) {
exercises.add(i + 1 + ". " + problems.get(i).exercises);
answers.add(i + 1 + ". " + problems.get(i).answers);
}
FileUtil.writeFile("Exercises.txt", exercises);
FileUtil.writeFile("Answers.txt", answers);
}
/**
* 判定答案
*/
public static void judgeProblems(String e, String a) {
List<String> exerciseFile = FileUtil.readFile(e);
List<String> answerFile = FileUtil.readFile(a);
int correct = 0;
int wrong = 0;
List<String> correctIndex = new ArrayList<>();
List<String> gradeIndex = new ArrayList<>();
for (int i = 0; i < exerciseFile.size(); i++) {
try {
String[] exercise = exerciseFile.get(i).split("\\. ");
String[] answer = answerFile.get(i).split("\\. ");
// 判定答案
if (ProblemGenerator.judgeProblem(exercise[1], answer[1])) {
correctIndex.add(exercise[0]);
correct++;
} else {
gradeIndex.add(exercise[0]);
wrong++;
}
} catch (Exception ex) {
throw new RuntimeException("无法解析表达式");
}
}
// 输出统计结果
List<String> grade = new ArrayList<>();
grade.add("Correct:" + correct + " (" + String.join(", ", correctIndex) + ")");
grade.add("Wrong:" + wrong + " (" + String.join(", ", gradeIndex) + ")");
FileUtil.writeFile("Grade.txt", grade);
}
}
Problem
public class Problem {
public String exercises;
public String answers;
public Problem(String exercises, String answers) {
this.exercises = exercises;
this.answers = answers;
}
}
FileUtil
public class FileUtil {
public static List<String> readFile(String filePath) {
List<String> lines = new ArrayList<>();
File file = new File(filePath);
if (!file.exists()) {
throw new RuntimeException("无法加载文件");
}
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
String line;
while ((line = reader.readLine()) != null) {
lines.add(line);
}
} catch (IOException e) {
throw new RuntimeException("文件读取失败");
}
return lines;
}
public static void writeFile(String filePath, List<String> lines) {
File file = new File(filePath);
try (FileWriter writer = new FileWriter(file)) {
for (String line : lines) {
writer.write(line + "\n");
}
} catch (IOException e) {
throw new RuntimeException("文件写入失败");
}
}
}
Fraction
public class Fraction {
private int numerator;
private int denominator;
public Fraction(int numerator, int denominator) {
this.numerator = numerator;
this.denominator = denominator;
simplify();
}
public int getNumerator() {
return numerator;
}
public int getDenominator() {
return denominator;
}
public Fraction add(Fraction other) {
return new Fraction(this.numerator * other.denominator + other.numerator * this.denominator,
this.denominator * other.denominator);
}
public Fraction subtract(Fraction other) {
return new Fraction(this.numerator * other.denominator - other.numerator * this.denominator,
this.denominator * other.denominator);
}
public Fraction multiply(Fraction other) {
return new Fraction(this.numerator * other.numerator,
this.denominator * other.denominator);
}
public Fraction divide(Fraction other) {
return new Fraction(this.numerator * other.denominator,
this.denominator * other.numerator);
}
private void simplify() {
int gcd = gcd(numerator, denominator);
numerator /= gcd;
denominator /= gcd;
}
private int gcd(int a, int b) {
return b == 0 ? a : gcd(b, a % b);
}
public int compareTo(Fraction other) {
return Integer.compare(this.numerator * other.denominator, other.numerator * this.denominator);
}
@Override
public String toString() {
if (numerator == 0) {
return "0";
}
if (Math.abs(numerator) < denominator) {
return numerator + "/" + denominator;
}
int intPart = numerator / denominator;
int numerator1 = Math.abs(numerator) % denominator;
return numerator1 == 0 ? String.valueOf(intPart) : intPart + "'" + numerator1 + "/" + denominator;
}
}
ProblemGenerator
public class ProblemGenerator {
private static final char[] OPERATORS = {'+', '-', '×', '÷'};
private static final Random RANDOM = new Random();
/**
* 生成题目
*/
public static Problem generateProblem(int r) {
String exercises = generateExercises(r);
String answer = calculateAnswer(exercises);
// 循环直到题目符合要求
while (answer == null) {
exercises = generateExercises(r);
answer = calculateAnswer(exercises);
}
return new Problem(exercises, answer);
}
/**
* 判定答案
*/
public static boolean judgeProblem(String exercises, String answer) {
return Objects.equals(calculateAnswer(exercises), answer);
}
/**
* 生成表达式
*/
public static String generateExercises(int r) {
StringBuilder sb = new StringBuilder();
sb.append(generateNumber(r));
for (int i = 0; i < RANDOM.nextInt(3) + 1; i++) {
sb.append(" ").append(generateOperator()).append(" ").append(generateNumber(r));
}
return sb.toString();
}
/**
* 生成运算符
*/
private static String generateOperator() {
return String.valueOf(OPERATORS[new Random().nextInt(OPERATORS.length)]);
}
/**
* 判断运算符
*/
private static boolean isOperator(char c) {
for (char operator : OPERATORS) {
if (operator == c) {
return true;
}
}
return false;
}
/**
* 生成数字
*/
private static String generateNumber(int r) {
return new Fraction(
RANDOM.nextInt(r - 1) + 1,
RANDOM.nextInt(r - 1) + 1
).toString();
}
/**
* 计算答案
*/
private static String calculateAnswer(String exercises) {
Stack<Fraction> numStack = new Stack<>();
Stack<Character> operator = new Stack<>();
// 加入操作栈
for (String str : exercises.split(" ")) {
if (str.length() == 1 && isOperator(str.charAt(0))) {
operator.push(str.charAt(0));
} else {
// 转为分数计算
numStack.push(getFraction(str));
}
}
while (!operator.isEmpty()) {
char op = operator.pop();
Fraction num2 = numStack.pop();
Fraction num1 = numStack.pop();
if (op == '+' || op == '-') {
// 优先计算乘除
if (!operator.isEmpty() && (operator.peek() == '×' || operator.peek() == '÷')) {
Fraction num3 = numStack.pop();
Character op1 = operator.pop();
if (op1 == '×') {
num1 = num1.multiply(num3);
} else if (op1 == '÷') {
if (num3.getNumerator() == 0) {
return null;
}
num1 = num1.divide(num3);
}
}
if (op == '+') {
numStack.push(num1.add(num2));
} else {
// 如果存在形如e1− e2的子表达式,那么e1≥e2
if (num1.compareTo(num2) >= 0) {
numStack.push(num1.subtract(num2));
} else {
return null;
}
}
} else if (op == '×') {
numStack.push(num1.multiply(num2));
} else if (op == '÷') {
if (num2.getNumerator() == 0) {
return null;
}
numStack.push(num1.divide(num2));
}
}
return numStack.size() == 1 ? numStack.pop().toString() : null;
}
/**
* 转换为分数
*/
private static Fraction getFraction(String str) {
if (str.contains("'")) {
String[] parts = str.split("[/']");
int intPart = Integer.parseInt(parts[0]);
int numerator = Integer.parseInt(parts[1]);
int denominator = Integer.parseInt(parts[2]);
numerator += intPart * denominator;
return new Fraction(numerator, denominator);
}
if (str.contains("/")) {
String[] parts = str.split("/");
return new Fraction(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]));
}
return new Fraction(Integer.parseInt(str), 1);
}
}
项目测试
测试用例
public class MainTest {
@Test
public void test1() {
Main.main(new String[]{});
}
@Test
public void test2() {
Main.main(new String[]{"-n", "10", "-r", "10"});
}
@Test
public void test3() {
Main.main(new String[]{"-n", "-1", "-r", "10"});
}
@Test
public void test4() {
Main.main(new String[]{"-n", "10005", "-r", "10"});
}
@Test
public void test5() {
Main.main(new String[]{"-e", "e.txt", "-a", "a.txt"});
}
@Test
public void test6() {
Main.main(new String[]{"-e", "e", "-a", "a"});
}
@Test
public void test7() {
Main.main(new String[]{"-e", "Exercises.txt", "-a", "Answers.txt"});
}
@Test
public void test8() {
Main.main(new String[]{"-e", "Exercises.txt", "-a", "ErrorAnswers.txt"});
}
@Test
public void test9() {
Main.main(new String[]{"-e", "Exercises.txt", "-b", "ErrorAnswers.txt"});
}
@Test
public void test10() {
Main.main(new String[]{"-n", "10", "-f", "10"});
}
}
覆盖率测试
除文件输入输出流异常外覆盖了项目所有分支。
性能测试
选择生成一万条题目进行性能测试,主要耗时在于文件写入、问题生成和答案生成。
项目小结
本次项目运用软件系统设计各方面知识,包括选择合理的算法、使用分治思想抽象系统模块、使用面向对象编程抽象题目和分数、将系统划分为多层进行解耦合、编写测试接口并进行性能分析等。此次任务能够增强我软件开发的能力并提高我系统设计的综合能力。