Git地址 | https://github.com/isliudong/WordCount |
---|---|
结对同学的作业地址 | <同伴博客> |
结对同学的学号 | 201731062214 |
个人博客地址 | https://www.cnblogs.com/liudong123/ |
作业要求 | <作业要求> |
一、结对过程(拍照)
二、PSP表格
三、基本解题思路
四、程序设计(详细)
分工:同伴负责:四个基础功能;我负责:文件写入、命令行参数定义、两个附加功能
基础框架设计
我们一共设计了3个类,包括1个接口类,1个基础功能类和1个附加功能类。6个功能函数(父类4个,子类两个)
逻辑流程设计
!img](https://img2018.cnblogs.com/blog/1664797/201910/1664797-20191012162038966-921828895.png)
我们对用户的操作进行了分类,假设用户没有输入任何命令行参数(只是执行了程序)那么我们调用BasisFunction(父类),用户只需输入文件路径即可得到相应的结果;假设用户使用了命令行参数此时我们再调用AddtionalFunction(子类)接口封装设计
四个基础功能的设计
两个附加功能和自定义命令行参数的设计
自定义命令行
在cmd命令行输入的参数会存入程序入口主函数main(string[] args)的字符串数组中。为了保证参数输入顺序不会影响程序执行顺序,使用分支语句判断并保存参数。然后再针对输入输出的路径进行判断是否为空,防止用户输入错误。按照题目要求 - i 和-o参数是必须要有的但是-m和-n的组合会出现三种情况,所以要再次使用分支语句进行判断。//对-l、-m、-n、-o参数识别并保存他们后面的输入值 for(int i=0;i<args.Length;i++) { if (args[i] == "-l")//路径参数 { path = args[i + 1]; i++; } else if (args[i] == "-m")//参数设定统计的词组长度 { wordlength = int.Parse(args[i+1]); i++; } else if (args[i] == "-n")//参数设定输出的单词数量 { outnum = int.Parse(args[i + 1]); i++; } else if (args[i] == "-o")//参数设定生成文件的存储路径 { outpath = args[i + 1]; i++; } }
按照相应参数生成词组(-m)
我使用嵌套循环实现了单词组合成数组,并且存入新的字符串数组中。同时对新生成的词组计数。只需要找到文档中单词个数、命令行输入参数-m、外层循环次数的数学关系,外层循环次数代表着生成多少个词组也意味着在那里停止不会越界;内层循环则代表着由单词组合成一个词组需要循环几次也要计算循环次数和-m参数的数学关系关系。//嵌套循环生成词组 words[0] = word[1]; for(int i=0;i<word.Length-wordlenth;i++) { for (int j = i; j <= i+wordlenth-1; j++) { words[i] = words[i] + " "+word[j]; } }
按照参数输出高频次数个数(-n)
在子类的Compare中我们设计了统计了单词出现频率平且使用双关键词排序的方法实现频率和字典排序,所以在设计-n这个附加功能时只需要将 -n的参数传入子类方法就可以实现。可以说之前的框架设计在这里起到了很大的作用。//单词比较算法 public int compare(String str1, String str2) { int length1 = str1.Length; int length2 = str2.Length; int limit = Math.Min(length1, length2); char[] a = str1.ToCharArray(); char[] b = str2.ToCharArray(); for (int i = 0; i < limit; i++) { char c1 = (char)(a[i] >= 'a' ? a[i] : (a[i] + 32)); char c2 = (char)(b[i] >= 'a' ? b[i] : (b[i] + 32)); if (c1 != c2) { return c1 - c2; } } return length1 - length2; }
五、代码规范
- 缩进问题:采用4个空格
- 每条语句单独占一行,一个‘{’占一行;同类变量可以在一行定义多个
- 命名方式:变量定义采用驼峰命名法(如stuNum)、类名方法名首字母也要大写(如DoSomething)、所有名字中不加下划线
- 注释:方法前用序言性注释(说明功能)、变量前用功能性注释
- 代码设计规范:方法满足单一功能性(只做一件事,并且要做好)、函数要有单一出口、体现面向对象原则
六、代码互审
互审过程中我们发现了很多的问下,下面我们就分享一下这些问题和解决方式:
- 在我审查同伴代码时出现的一些问题:
1、四个基础功能函数没有设计合理的返回值。后续改进:添加了相应返回值,方便了模块交互。
2、在countWord函数中一开始没有设计,单词长度应超过四个字符的需求。后续改进:增加了对单词长度的判断。
3、在countLine函数中最开始没有想到空白行不能计入行数的问题。后续改进:采用先将文本中的空白行剔除,再计算行数的方法。
4、在countFrequency函数中,最初没有针对在单词出现相同频率时按照字典顺序排序。后续改进:采用双关键词排序的方式优先对频率排序再对字典顺序排序。
- 同伴对我代码的复审:同伴博客
互审总结:我认为最结对编程中最重要的一部分就是代码互审,真正的做到1+1>2就是通过代码互审实现的。其实在结对编程的初期我们就都出现了一样的问题,因为这次我们使用github合作编程,每个人提交和下载代码时,经常会出现无法成功pull的问题。在我们一段时间的合作以后慢慢的才理顺了提交顺序。使用git来记录我们的合作过程也是非常重要的结对编程环节。
七、单元测试及异常处理
单元测试:
对单词字典比较算进行比较(Compare.compare()):
public class CompareTests { [TestMethod()] public void compareTest() { Compare compare = new Compare(); String[] word1 = { "as", "ae", "th", "cpig" }; String[] word2 = { "we", "are", "the", "pig" }; int t; bool k=false; for (int i = 0; i < 4; i++) { t = compare.compare(word1[i], word2[i]); if (t < 0) { k = true; } Assert.AreEqual(true,k ); } } }
测试结果:
复制字符混合测试
测试文本内容
测试成功
异常处理:
对文件读入进行测试
测试代码设计:
public void ReadTextTest() { Read read = new Read(); read.ReadText("D:\a.txt", 1); for(int i=0;i<read.word.Length;i++) { Assert.AreNotEqual("", read.word[i]); } //Assert.Fail(); }
运行出错:
发现是由于word = Regex.Split(sr.ReadToEnd(), @"\W+");代码出现问题,split函数通过正则表达式拆分出来单词数组存在首尾为空的情况。
通过将数组存入hashtable时进行非空判断,解决问题如图所示。
写文件文档路径是否存在异常处理:
代码设计:
FileStream fileStream1 = new FileStream(l, FileMode.OpenOrCreate, FileAccess.ReadWrite); fileStream1.Close();//如果文件不存在会自动在该路径下创建写入文件
测试心得:
对于异常处理的设计有几点想要分享:
1、2、有些函数是被调用函数,测试不成功但是在调用者函数里进行处理,也是可以的。
八、性能测试及改进
改进花费时间:25mins
性能测试:
函数性能分析:
主要是hashtable的创建,字母的比较和读入字符三个部分吃性能
改进方案:
九、代码展示及程序运行结果截图
Main:
public static void Main(string[] args) { int wordlength=1; int outnum=0; string outpath="/"; string path=null; for(int i=0;i<args.Length;i++) { if (args[i] == "-i")//路径参数 { path = args[i + 1]; i++; } else if (args[i] == "-m")//参数设定统计的词组长度 { wordlength = int.Parse(args[i+1]); i++; } else if (args[i] == "-n")//参数设定输出的单词数量 { outnum = int.Parse(args[i + 1]); i++; } else if (args[i] == "-o")//参数设定生成文件的存储路径 { outpath = args[i + 1]; i++; } } new Do().doing(path, wordlength, outnum, outpath); }
ReadText:
public String ReadText(String path,int wordlenth) { StreamReader sr = new StreamReader(path, Encoding.Default); while(sr.Read()!=-1) { sum++; } row= sr.ReadToEnd().Split('\n').Length; sr.BaseStream.Seek(0, SeekOrigin.Begin);//重置流指针 row = sr.ReadToEnd().Split('\n').Length;//行数统计 sr.BaseStream.Seek(0, SeekOrigin.Begin); word = Regex.Split(sr.ReadToEnd(), @"\W+");// words = new string[word.Length-wordlenth]; words[0] = word[1]; for(int i=0;i<word.Length-wordlenth;i++) { for (int j = i; j <= i+wordlenth-1; j++) { words[i] = words[i] + " "+word[j]; } } sr.BaseStream.Seek(0, SeekOrigin.Begin);//重置流指针 return sr.ReadToEnd(); }
写入文件:
using (StreamWriter sw = new StreamWriter(outpath)) { sw.WriteLine("单词数:" + count);//单词数 sw.WriteLine("字符数:" + zifushu); sw.WriteLine("行数:" + hangshu); sw.WriteLine("词汇量:" + cihui); sw.WriteLine("词组频统计(词频优先字典序):"); for (int i = 0; i < wd.Length; i++) { sw.WriteLine(wd[i] + ": " + hashtable[wd[i]]); } sw.Close(); Console.ReadLine(); }
将词组存入hashtable:
for (int i = 0; i < read.words.Length; i++) { if (hashtable.ContainsKey(read.words[i])) { geshu = (int)hashtable[read.words[i]]; geshu++; hashtable[read.words[i]] = geshu; } else { if (read.words[i] != "")//取出split产生的空字符 hashtable.Add(read.words[i], times[i]); } }
命令行运行结果: