软件工程第二次作业-第一次个人编程作业
个人编程作业
项目 | 内容 |
---|---|
这个作业属于哪个课程 | [软件工程](首页 - 计科23级12班 - 广东工业大学 - 班级博客 - 博客园) |
这个作业要求在哪里 | [作业要求](个人项目 - 作业 - 计科23级12班 - 班级博客 - 博客园) |
这个作业的目标 | 训练个人项目软件开发能力,学会使用性能测试工具和实现单元测试优化程序 |
代码仓库: https://github.com/maple525866/3123004566-PaperplagiarismCheck/tree/master
PSP2表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 10 | 15 |
Estimate | 估计这个任务需要多少时间 | 60 | 90 |
Development | 开发 | 200 | 220 |
Analysis | 需求分析 (包括学习新技术) | 30 | 40 |
Design Spec | 生成技术文档 | 30 | 30 |
Design Review | 设计复审 | 20 | 20 |
Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 10 | 10 |
Design | 具体设计 | 30 | 40 |
Coding | 具体编码 | 150 | 250 |
Code Review | 代码复审 | 30 | 30 |
Test | 测试(自我测试,修改代码,提交修改) | 20 | 30 |
Reporting | 报告 | 60 | 100 |
Test Repor | 测试报告 | 20 | 10 |
Size Measurement | 计算工作量 | 60 | 45 |
Postmortem & Process Improvement Plan | 事后总结,并提出过程改进计划 | 40 | 30 |
计算模块接口的设计与实现过程
代码组织结构设计
1.1 类结构设计
目前系统采用单一主类设计模式,所有核心功能集中在PlagiarismChecker类中:
PlagiarismChecker (主类)
├── main() # 程序入口,处理命令行参数
├── readFile() # 文件读取模块
├── writeResult() # 结果输出模块
├── calculateSimilarity() # 相似度计算核心算法
└── longestCommonSubsequence() # LCS算法实现
1.2 函数关系层次
Level 1: main()
├── 参数验证
└── 异常处理框架
Level 2: readFile() + calculateSimilarity() + writeResult()
├── 文件I/O操作
├── 核心算法逻辑
└── 格式化输出
Level 3: longestCommonSubsequence()
└── 底层算法实现
1.3 模块职责分离
- 输入模块:readFile() - 负责UTF-8文件读取和内容处理
- 计算模块:calculateSimilarity() + longestCommonSubsequence() - 核心算法逻辑
- 输出模块:writeResult() - 结果格式化和文件写入
- 控制模块:main() - 流程控制和异常处理
关键函数流程图
2.1 主程序流程
2.2 相似度计算流程
算法设计
3.1.核心算法设计 - LCS(最长公共子序列)
选择理由:
- 顺序保持:LCS算法保持字符的相对顺序,更符合文本相似性判断
- 容错性强:对文本中的插入、删除、替换操作具有很好的容错能力
- 语义保持:即使有词汇替换,核心语义结构仍能被识别
3.2.独到之处
- 对称性:交换两个文本位置,结果不变
- 归一化:结果范围固定在[0, 1]
- 平衡性:考虑两个文本的平均长度,避免长度偏差
3.3 文本预处理策略
智能空格处理:
- 消除格式影响:不同的空格、换行、制表符不影响相似度
- 专注内容:只关注实际文字内容的相似性
- 中文友好:适合中文文本的特点(词汇间不需要空格分隔)
3.4 性能优化考量
动态规划优化:
- 时间复杂度:O(m×n),其中m、n为文本长度
- 空间复杂度:O(m×n),使用二维DP表
- 实际优化:可进一步优化为O(min(m,n))空间复杂度
内存友好: - 使用StringBuilder进行文件读取,避免字符串频繁拼接
- DP表按需创建,处理完成后自动回收
单测代码
单测覆盖率
package com.code;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.io.TempDir;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.*;
/**
* 论文查重系统单元测试类
* 覆盖各种边界情况和正常情况,确保程序稳定性
*/
public class PlagiarismCheckerTest {
@TempDir
Path tempDir;
@Test
@DisplayName("测试1: 完全相同的文本应该返回100%相似度")
void testIdenticalTexts() {
String text1 = "今天是星期天,天气晴,今天晚上我要去看电影。";
String text2 = "今天是星期天,天气晴,今天晚上我要去看电影。";
double similarity = PlagiarismChecker.calculateSimilarity(text1, text2);
assertEquals(1.0, similarity, 0.01, "完全相同的文本相似度应该为1.0");
}
@Test
@DisplayName("测试2: 完全不同的文本应该返回较低相似度")
void testCompletelyDifferentTexts() {
String text1 = "今天天气很好";
String text2 = "明年计划去旅行";
double similarity = PlagiarismChecker.calculateSimilarity(text1, text2);
assertTrue(similarity < 0.3, "完全不同的文本相似度应该很低");
}
@Test
@DisplayName("测试3: 部分相似文本的相似度计算")
void testPartiallySimilarTexts() {
String text1 = "今天是星期天,天气晴,今天晚上我要去看电影。";
String text2 = "今天是周天,天气晴朗,我晚上要去看电影。";
double similarity = PlagiarismChecker.calculateSimilarity(text1, text2);
assertTrue(similarity > 0.5 && similarity < 1.0,
"部分相似文本的相似度应该在0.5-1.0之间,实际值: " + similarity);
}
@Test
@DisplayName("测试4: 空字符串处理")
void testEmptyStrings() {
// 两个都为空
double similarity1 = PlagiarismChecker.calculateSimilarity("", "");
assertEquals(1.0, similarity1, 0.01, "两个空字符串应该相似度为1.0");
// 一个为空,一个不为空
double similarity2 = PlagiarismChecker.calculateSimilarity("", "测试文本");
assertEquals(0.0, similarity2, 0.01, "空字符串和非空字符串相似度应该为0.0");
double similarity3 = PlagiarismChecker.calculateSimilarity("测试文本", "");
assertEquals(0.0, similarity3, 0.01, "非空字符串和空字符串相似度应该为0.0");
}
@Test
@DisplayName("测试5: null值处理")
void testNullValues() {
double similarity1 = PlagiarismChecker.calculateSimilarity(null, null);
assertEquals(0.0, similarity1, 0.01, "两个null值应该返回0.0");
double similarity2 = PlagiarismChecker.calculateSimilarity(null, "测试文本");
assertEquals(0.0, similarity2, 0.01, "null和文本应该返回0.0");
double similarity3 = PlagiarismChecker.calculateSimilarity("测试文本", null);
assertEquals(0.0, similarity3, 0.01, "文本和null应该返回0.0");
}
@Test
@DisplayName("测试6: 只包含空格的字符串")
void testWhitespaceOnlyStrings() {
String text1 = " ";
String text2 = " \t ";
double similarity = PlagiarismChecker.calculateSimilarity(text1, text2);
assertEquals(1.0, similarity, 0.01, "只有空格的字符串应该被认为相同");
String text3 = " 测试 ";
String text4 = "测试";
double similarity2 = PlagiarismChecker.calculateSimilarity(text3, text4);
assertEquals(1.0, similarity2, 0.01, "去除空格后相同的文本应该相似度为1.0");
}
@Test
@DisplayName("测试7: 单字符文本")
void testSingleCharacterTexts() {
double similarity1 = PlagiarismChecker.calculateSimilarity("a", "a");
assertEquals(1.0, similarity1, 0.01, "相同单字符相似度应该为1.0");
double similarity2 = PlagiarismChecker.calculateSimilarity("a", "b");
assertEquals(0.0, similarity2, 0.01, "不同单字符相似度应该为0.0");
}
@Test
@DisplayName("测试8: 包含特殊字符的文本")
void testTextsWithSpecialCharacters() {
String text1 = "Hello, World! @#$%^&*()";
String text2 = "Hello, World! @#$%^&*()";
double similarity = PlagiarismChecker.calculateSimilarity(text1, text2);
assertEquals(1.0, similarity, 0.01, "包含特殊字符的相同文本相似度应该为1.0");
String text3 = "测试:文本!?";
String text4 = "测试:文档!?";
double similarity2 = PlagiarismChecker.calculateSimilarity(text3, text4);
assertTrue(similarity2 > 0.7, "包含特殊字符的部分相似文本相似度应该较高");
}
@Test
@DisplayName("测试9: 中英文混合文本")
void testMixedChineseEnglishTexts() {
String text1 = "今天我要学习Java编程language";
String text2 = "今天我要学习Python编程language";
double similarity = PlagiarismChecker.calculateSimilarity(text1, text2);
assertTrue(similarity > 0.7, "中英文混合的相似文本相似度应该较高,实际值: " + similarity);
}
@Test
@DisplayName("测试10: 数字和文字混合")
void testTextsWithNumbers() {
String text1 = "版本1.0发布于2023年";
String text2 = "版本2.0发布于2024年";
double similarity = PlagiarismChecker.calculateSimilarity(text1, text2);
assertTrue(similarity > 0.5, "包含数字的相似文本相似度应该合理,实际值: " + similarity);
}
@Test
@DisplayName("测试11: 长文本性能测试")
void testLongTextPerformance() {
StringBuilder longText1 = new StringBuilder();
StringBuilder longText2 = new StringBuilder();
String baseText = "这是一个测试文本段落。";
for (int i = 0; i < 100; i++) {
longText1.append(baseText).append("第").append(i).append("段。");
longText2.append(baseText).append("第").append(i).append("节。");
}
long startTime = System.currentTimeMillis();
double similarity = PlagiarismChecker.calculateSimilarity(longText1.toString(), longText2.toString());
long endTime = System.currentTimeMillis();
assertTrue(similarity > 0.8, "长文本相似度计算应该正确,实际值: " + similarity);
assertTrue(endTime - startTime < 5000, "长文本处理时间应该在合理范围内: " + (endTime - startTime) + "ms");
}
@Test
@DisplayName("测试12: LCS算法基本功能")
void testLCSAlgorithm() {
int lcs1 = PlagiarismChecker.longestCommonSubsequence("ABCDGH", "AEDFHR");
assertEquals(3, lcs1, "ABCDGH和AEDFHR的LCS长度应该为3(ADH)");
int lcs2 = PlagiarismChecker.longestCommonSubsequence("AGGTAB", "GXTXAYB");
assertEquals(4, lcs2, "AGGTAB和GXTXAYB的LCS长度应该为4(GTAB)");
int lcs3 = PlagiarismChecker.longestCommonSubsequence("", "ABC");
assertEquals(0, lcs3, "空字符串的LCS长度应该为0");
int lcs4 = PlagiarismChecker.longestCommonSubsequence("ABC", "ABC");
assertEquals(3, lcs4, "相同字符串的LCS长度应该等于字符串长度");
}
@Test
@DisplayName("测试13: 文件读写功能")
void testFileOperations() throws IOException {
// 测试文件写入和读取
Path testFile = tempDir.resolve("test_output.txt");
String filePath = testFile.toString();
// 测试写入结果
PlagiarismChecker.writeResult(filePath, 0.856);
// 验证写入的内容
String content = Files.readString(testFile, StandardCharsets.UTF_8);
assertEquals("85.60%", content, "写入的百分比格式应该正确");
// 测试读取功能
Path inputFile = tempDir.resolve("input.txt");
Files.writeString(inputFile, "测试文本内容", StandardCharsets.UTF_8);
String readContent = PlagiarismChecker.readFile(inputFile.toString());
assertEquals("测试文本内容", readContent, "读取的文件内容应该正确");
}
@Test
@DisplayName("测试14: 边界相似度值格式化")
void testSimilarityFormatting() throws IOException {
Path testFile = tempDir.resolve("format_test.txt");
// 测试0%
PlagiarismChecker.writeResult(testFile.toString(), 0.0);
String content1 = Files.readString(testFile, StandardCharsets.UTF_8);
assertEquals("0.00%", content1, "0%应该格式化为0.00%");
// 测试100%
PlagiarismChecker.writeResult(testFile.toString(), 1.0);
String content2 = Files.readString(testFile, StandardCharsets.UTF_8);
assertEquals("100.00%", content2, "100%应该格式化为100.00%");
// 测试小数
PlagiarismChecker.writeResult(testFile.toString(), 0.12345);
String content3 = Files.readString(testFile, StandardCharsets.UTF_8);
assertEquals("12.35%", content3, "小数应该正确舍入到两位");
}
@Test
@DisplayName("测试15: 文件不存在异常处理")
void testFileNotFoundException() {
String nonExistentFile = tempDir.resolve("non_existent.txt").toString();
assertThrows(IOException.class, () -> {
PlagiarismChecker.readFile(nonExistentFile);
}, "读取不存在的文件应该抛出IOException");
}
}
代码运行命令
java -jar target/main.jar orig.txt orig_0.8_add.txt result.txt