(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