(1)Github项目地址:https://github.com/Kyoy/WordCount
(2)PSP表格
PSP2.1 | PSP阶段 | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
· Planning | · 计划 | 60 | 60 |
· Estimate | · 估计这个任务需要多少时间 | 60 | 30 |
· Development | · 开发 | 300 | 350 |
· Analysis | · 需求分析 (包括学习新技术) | 60 | 30 |
· Design Spec | · 生成设计文档 | 100 | 30 |
· Design Review | · 设计复审 (和同事审核设计文档) | 30 | 30 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 60 | 30 |
· Design | · 具体设计 | 60 | 150 |
· Coding | · 具体编码 | 300 | 450 |
· Code Review | · 代码复审 | 150 | 200 |
· Test | · 测试(自我测试,修改代码,提交修改 | 150 | 200 |
· Reporting | · 报告 | 300 | 300 |
· Test Report | · 测试报告 | 150 | 200 |
· Size Measurement | · 计算工作量 | 30 | 10 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 60 | 30 |
· 合计 | 1870 | 2100 |
(3)解题思路
拿到题目的要求,我们不难发现本次作业的两大部分:编码和测试,编码部分的要求包括基础功能中的统计字符数、统计单词数、统计行数、输出至指定文件和扩展功能中的递归处理、获得复杂数据、使用停用词表,最后还要求生成exe文件。题目关于单词和代码行数、注释行数的定义与我们的普遍认知有所不同,加上老师给出的示例答案总在变化而且争议较大,因此我没有使用类似词法分析器的方法实现统计功能,而采用了比较简单但可能不太严谨的方法。题目对于输入输出有严格且没有争议的要求,因此我去了解了java文件读取的相关方法。
参数本想用args[]接受,但是args[]会自动将*进行匹配,且exe文件不知道如何输入args[]参数,所以后来改用Scanner获取用户输入作为参数。
文件处理参考网站[1][2],正则式参考网站[4],打包exe参考网站[3]
(4)程序实现
我设计了两个类,WordCount
类和Run
类。WordCount
负责实现相关的统计功能,包括统计单词数、字符数、总行数、代码行数、注释行数、空行数,后三项具体的行数统计稍难,故设计一个方法public static int kindOfLine(String line) 传入当前行内容,返回当前行类型,其余统计较为简单,均在构造函数中进行,该类有line
wordNum
charNum
noteLine
emptyLine
codeLine
等属性,对象调用相应get方法获得对应属性。Run
类负责接收参数并控制相应流程,为实现路径的解析,定义了三个方法public static String removeFilePath(String fileName)和public static String getPath(String fileName)和public static ArrayList<File> getLegalFile(String directory,String resourceFileName) 为实现停用词功能,定义了方法public static HashSet<String> stopList(File stopFile) ,main()方法中获取参数进行判断并进行相应输出。上述方法会有针对性的在代码说明中解释。
(5)代码说明
空行和不包含注释符号的代码行容易判断,包含注释符号的代码行稍稍复杂,通过split方法获得注释符号之前或之后的子串,再通过正则匹配判断其中是否含有代码,若有则该行仍为代码行,最后剩下的就是注释行。
public static int kindOfLine(String line){ //-1 empty 1 code 0 note
String temp ="";
if(line.trim().length() == 0)
return -1;
if(!line.contains("//") && !line.contains("/*") && !line.contains("*/")) //不含这些符号必是code
return 1;
if(line.contains("//")) // //之前的code
temp = line.split("//")[0];
if(line.contains("/*")) // /*之前的code
temp = line.split("/\\*")[0];
if(line.contains("*/")) // */之后的code
temp = line.split("\\*/")[line.split("\\*/").length - 1];
if(temp.matches(".*\\w+.*")) // 含有字母或数字则该行是code
return 1;
return 0;
}
构造函数接受File作为参数,通过BufferedReader每次读取一行,最终拼接成一个字符串,将其转换为字符数组,统计其长度可得字符数;按照题目对于单词的定义,将字符按照空格或逗号进行分割,统计分割后的非空字符串数即为单词数;调用kindOfLine获得行数信息。
构造函数重载接收file和stopList作为参数,统计单词数时剔除stopList中含有的单词。
public WordCount(File file) {
BufferedReader bf;
try {
bf = new BufferedReader(new FileReader(file));
String temp1, temp2 = "";
while((temp1 = bf.readLine()) != null) {
temp2 += temp1 + String.valueOf('\n');
line++; // 获取行数
int i = kindOfLine(temp1); // 获取行数详细信息
if(i == -1)
emptyLine++;
else if(i == 1)
codeLine++;
else
noteLine++;
for (String val: temp1.split(" |,")){
wordNum += val.equals("") ? 0 : 1; // 非空单词数
}
}
buffer = temp2.toCharArray();
bf.close();
charNum = buffer.length - 1; // 字符数
} catch (Exception e) {
e.printStackTrace();
}
}
public WordCount(File file, HashSet<String> stopList)
removeFilePath方法接收一个字符串,经过正则匹配,若该字符串是一个绝对路径,则将其分割,返回文件名;
getPath方法接收一个字符串,调用removeFilePath方法,返回绝对路径中的路径部分。
public static String removeFilePath(String fileName){
if(fileName.matches("^[A-z]:\\\\\\S+$")) // 正则匹配绝对路径
fileName = fileName.substring(fileName.lastIndexOf("\\")+1, fileName.length()); // 获取文件名
return fileName;
}
public static String getPath(String fileName){
String name = removeFilePath(fileName);
return fileName.replace("\\"+name,""); // 获取路径
}
stoplist方法接受一个文件,获取文件内的单词并存入HashSet中并返回,在统计单词数时,将单词和Set中的单词进行比对,若相同则将其忽略。
public static HashSet<String> stopList(File stopFile){
HashSet<String> stopList = new HashSet<>();
try {
BufferedReader reader = new BufferedReader(new FileReader(stopFile));
String temp;
String Str = "";
while((temp = reader.readLine())!=null) { // 读每一行
Str += temp;
}
String[] words = Str.split(" "); // 按空格分割单词
for(String val:words){
stopList.add(val);
}
}
catch (Exception e)
{
e.printStackTrace();
}
return stopList;
}
getLegalFile方法接受两个参数,其中directory是文件路径,resourceFileName是文件名,支持*通配符,返回值是directory路径下满足resourceFileName文件名要求的文件组成的List,其中判断文件名是否符合要求利用了 通配符匹配。
public static ArrayList<File> getLegalFile(String directory,String resourceFileName) //directory是文件路径,resourceFileName是文件名,支持*通配符
{
String regex = resourceFileName.replace("*", "[0-9A-z]*"); // 通配符替换
ArrayList<File> fileList = new ArrayList<>();
File file = new File(directory);
try{
if(file.isDirectory()){
File[] files = file.listFiles(); // 获取directory路径下的所有文件组成File[]
for(File val : files){
if(val.getName().matches(regex) && val.isFile()){ // File[]中的文件名等于或满足通配符要求,添加到ArrayList中
fileList.add(val);
}
}
}
}
catch(NullPointerException e){
e.printStackTrace();
}
return fileList;
}
main()方法中定义了7个布尔型变量,根据输入信息决定输出相应信息。相对路径为简化处理仅支持文件名,对于ouputFile和stopFile组装出绝对路径,对于resourceFile分离出文件路径directory和文件名resourceFileName,调用getLegalFile方法获得对应的文件信息。
boolean isC = false, isW = false, isL = false, isO = false, isS = false, isA = false, isE = false; // 是否进行相应操作,默认否
else if(strList[i].equals("-o")){
if(i < strList.length - 1){
isO = true;
outputFileName = strList[++i];
if(outputFileName.contains(":\\")){ // 若是绝对路径,则作为文件路径
outputFile = new File(outputFileName);
}
else{
outputFile = new File(curPath + "\\" + outputFileName); // 若是相对路径,将当前路径和相对路径拼接获得文件路径
}
}
}
else if(strList[i].equals("-s"))
isS = true;
else if(strList[i].equals("-a"))
isA = true;
else if(strList[i].equals("-e")){
if(i < strList.length - 1){
isE = true;
stopFileName = strList[++i];
if(stopFileName.contains(":\\")){ // 若是绝对路径,则作为文件路径
stopFile = new File(stopFileName);
}
else{
stopFile = new File(curPath + "\\" + stopFileName); // 若是相对路径,将当前路径和相对路径拼接获得文件路径
}
stopList = stopList(stopFile);
}
}
else{
resourceFileName = strList[i];
if(resourceFileName.contains(":\\")){
resourceFileName = removeFilePath(resourceFileName); // 获取文件名
directory = getPath(strList[i]); // 获取文件路径
}
else{
directory = curPath;
}
}
}
根据输入进行判断并进行输出
for(File val : fileList){
String name = val.getName();
if(val.equals(stopFile) || val.equals(outputFile)){
continue;
}
if(isE){
wc = new WordCount(val, stopList);
}
else{
wc = new WordCount(val);
}
if(isC){
output = output + name + ",字符数: " + wc.getCharNum() + "\r\n";
}
if(isW){
output = output + name + ",单词数: " + wc.getWordNum() + "\r\n";
}
if(isL){
output = output + name + ",行数: " + wc.getLine() + "\r\n";
}
if(isA){
output = output + name + ",代码行/空行/注释行: " + wc.getCodeLine() + "/" + wc.getEmptyLine() + "/" + wc.getNoteLine() + "\r\n";
}
}
BufferedWriter out = new BufferedWriter(new FileWriter(new File(curPath + "\\result.txt")));
out.write(output);
out.flush();
out.close();
if(isO){
outputFile.createNewFile(); // 创建新文件
out = new BufferedWriter(new FileWriter(outputFile));
out.write(output); // \r\n即为换行
out.flush(); // 把缓存区内容压入文件
out.close(); // 关闭文件
}
(6)测试设计过程
白盒的测试用例需要做到:
·保证一个模块中的所有独立路径至少 被使用一次
·对所有逻辑值均需测试 true 和 false
·在上下边界及可操作范围内运行所有循环
·检查内部数据结构以确保其有效性
--引用自博客[5]
设计以下10个测试用例(第一行为文件名,第二行为操作命令,之后为文件的内容):
//test1.c
//wc.exe -a test1.c
int
//test2.txt
//wc.exe -a -l -w -c test2.txt -o output.c
int
//test3.c
//wc.exe -a D:\test3.c
int
//test4.c
//wc.exe -a test4.java
int
//test5.c
//wc.exe -a test5.*
int
//test6.c
//wc.exe -a test6.c
int
}//
/*
//test7.c
//wc.exe -a -l -w -c test7.c -e stop.txt
int,void
//test8.c
//wc.exe -a -l -w -c -s test8.c -e stop.txt
int,void
{//
//test9.c
//wc.exe -a -l -w -c -s *.c -e stop.txt
int,void
/*
//test10.c
//wc.exe -a -l -w -c -s *.c -e stop.txt -o D:\output.txt
int,void
*/ main
结果:正确的测试用例都有正确的输出,错误的测试用例wc.exe会提示相应错误信息。
(7)参考文献链接
[1]https://www.google.com
[2]https://docs.oracle.com/javase/8/docs/api/
[3]http://blog.csdn.net/xiongcancan/article/details/46997799
[4]http://www.runoob.com/java/java-regular-expressions.html
[5]http://www.cnblogs.com/ITGirl00/p/3858357.html