寒假作业(2/2)——疫情统计

这个作业属于哪个课程 <2020春 W班>
这个作业要求在哪里 <作文正文>
这个作业的目标 开发疫情统计程序,学习github的使用
作业正文 寒假作业(2/2)
其他参考文献 《构建之法》、 引导和提示

1.Github仓库地址、代码规范链接

作业主仓库
我的Github仓库地址
我的代码规范

2.PSP表格

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 30 35
Estimate 估计这个任务需要多少时间 1000 1645
Development 开发 600 660
Analysis 需求分析 (包括学习新技术) 180 150
Design Spec 生成设计文档 60 55
Design Review 设计复审 30 15
Coding Standard 代码规范 (为目前的开发制定合适的规范) 30 20
Design 具体设计 30 60
Coding 具体编码 240 360
Code Review 代码复审 30 30
Test 测试(自我测试,修改代码,提交修改) 120 120
Reporting 报告 90 60
Test Report 测试报告 30 40
Size Measurement 计算工作量 20 10
Postmortem & Process Improvement Plan 事后总结, 并提出过程改进计划 60 30
合计 1550 1645

3.解题思路

开始的心酸历程:

一开始连 java 和javac都傻傻分不清(以前编译器一键运行,也很少用到args[]),可以说一开始的示例命令:$ java InfectStatistic list -date 2020-01-22 -log D:/log/ -out D:/output.txt 都没看懂,所以第一件事就是要去了解如何编译运行java文件,以及main中形式参数是怎样的,了解之后才知道java运行class文件,后面的参数便会依次传进args[]中。知道了我们的所有,那么就可以进行需求分析,得到我们的目的:所需。

初步构思:

处理args[]中的字符串String 便可以依照规则对-log内的日志文件处理,然后输出至-out中,而所谓的规则,就在-date ,-type,-province中,-date就是处理至这一天,-type和-province都是输出的格式,若不指定也有他们的默认输出格式 -date不指定 则应统计所有日志文件 -type不指定,则四种类型都要输出 -province不指定 则要依据日志中出现省份均需输出

从 我们所有 到 我们所需

所有:日志文本,以及一条命令行如:java InfectStatistic list -date 2020-01-22 -log D:/log/ -out D:/output.txt -type ip sp -province 福建 湖北
所需:根据命令行指示 输出文件
本质上这是一个文本的 读-处理统计-写 的问题,按格式读写不是问题,重点是在对命令行参数的解析之后根据其命令进行统计
即解析和统计
图片
查看原图

下面给出详细的设计思路和实现过程。

4.设计实现过程

总的思路不变,和上面一开始的想法差不多:解析命令->处理文件->输出文件,那接下来局势具体的实现想法。
依据需求分析 决定分出两个类,一个CmdAnalysis类 用于解析命令;一个HandleLog类 读取统计日志文件并输出。

解析命令:
分类型:
list:
-log (必选项)
-out (必选项)
-date (可选项) 格式yyyy-mm-dd如2020-02-10
-type (可选项)ip、sp、cure、dead
-province (可选项)列举的已排序的全国+31个省份
而后依类型读取、存储相关参数 如-log D:/log/ 则将D:/log/存下备用
图片
查看原图

处理文件:
读取log目录下所有文件名,与date比较,比其小的进行统计;
统计:进行省份 类型的统计 (可选项没有的话要用默认来统计,默认:日志中出现的省份)
图片
查看原图
然后根据格式输出即可

图片
查看原图

5.代码说明

利用equals区分读入的参数,并判断是否输入有误,有误return false,无误return true 并分别存储,具体可看注释


boolean mustLog = false;    //标记两个必选项-log -out 是否有输入
		boolean mustOut = false;
		if(!cmdString[0].equals("list")) {
			System.out.println("命令行参数缺少list");
			return false;
		}
		for(int i = 0;i < cmdString.length;i++) {
			if(cmdString[i].equals("-log")) {
				mustLog = true;
				logLocation = cmdString[++i];
			}
			else if(cmdString[i].equals("-out")) {
				mustOut = true;
				outLocation = cmdString[++i];
			}
			else if(cmdString[i].equals("-date")) {
				//检测日期合法性
				if(!isCorrectDate(++i)) {
					System.out.println("指定日期不合法");
					return false;
				}
			}
			else if(cmdString[i].equals("-type")) {
				//检测输入类型合法性(ip\sp\cure\dead)
				if(!isType(++i)) {
					System.out.println("指定类型不合法");
					return false;
				}
			}
			else if(cmdString[i].equals("-province")) {
				provinceShow[32] = 0;    //标识含-province参数,应依据输入输出,否则含默认方式(日志中涉及省份)展示
				if(!isProvince(++i)) {
					System.out.println("指定省份不合法");
					return false;
				}
			}
		}
		if(mustLog && mustOut)    //验证必选项是否输入
			return true;
		else {
			System.out.println("缺少输入-log 或 -out");
			return false;
		}

判断true时则进行存储(赋值) 否则返回false


	/*
	 *判断指定日期是否正确
	 *@param 命令行位置(下标)
	 *@return boolean 日期符合格式 true 否则 false
	 */
	private boolean isCorrectDate(int i) {
		if(i<cmdString.length) {
			if(isValidDate(cmdString[i])) {
				logDate = cmdString[i];
				return true;
			}else 
				return false;
		}else
			return false;
	}
	/*
	  *判断指定日期格式是否满足yyyy-MM-dd 字符串是否为数字
	 *@param String
	 *@return boole if String 满足格式 true 不满足false
	 */
	private boolean isValidDate(String strDate) {
		SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
		try {
            format.setLenient(false);
            Date date = format.parse(strDate);    //利用创建日期来判断格式,若抛出异常则日期格式有误
            String[] sArray = strDate.split("-");
            for (String s : sArray) {
                boolean isNum = s.matches("[0-9]+");
                if (!isNum) {
                    return false;
                }
            }
        } catch (Exception e) {
            //e.printStackTrace();
            return false;
        }
        return true;
	}
	/*
	 *判断指定类型是否正确,正确则记录输出类型的种类及顺序
	 *@param int 在命令行的位置
	 *@return boolean 满足类型true 不满足false
	 */
	private boolean isType(int i) {
		for(int a = 0;a < typeOrder.length;a++)
			typeOrder[a] = -1;    //-1不输出
		int currentIndex = i;
		if(i<cmdString.length) {
			int t = 0;
			for(;currentIndex < cmdString.length; currentIndex ++) {
				if(cmdString[currentIndex].equals(typeString[0])) {
					if(t < typeOrder.length)
						typeOrder[t] = 0;    
					t++;
				}	
				else if(cmdString[currentIndex].equals(typeString[1])) {
					if(t < typeOrder.length)
						typeOrder[t] = 1;
					t++;
				}
				else if(cmdString[currentIndex].equals(typeString[2])) {
					if(t < typeOrder.length)
						typeOrder[t] = 2;
					t++;
				}
				else if(cmdString[currentIndex].equals(typeString[3])) {
					if(t < typeOrder.length)
						typeOrder[t] = 3;
					t++;
				}
				else {
					break;
				}
			}
			if(t > 0 )
				return true;
			else
				return false;
		}else
			return false;
	}
	/*
	 * 判断指定省份是否正确
	 * @param int 在命令行的位置
	 * @return boolean 满足省份格式 true 不满足false
	 */
	private boolean isProvince(int i) {
		int currentIndex = i;
		if(i<cmdString.length) {
			for(;currentIndex < cmdString.length  && provinceShow[32] == 0; currentIndex ++) {
				for(int j = 0;j < province.length;j++) {
					if(cmdString[currentIndex].equals(province[j]))
						provinceShow[j] = 0;    //要求输出的省份
				}
			}
			return true;
		}else
			return false;
	}

读取日志文件,并将fileName与-date比较,筛选出符合条件的日志文件来详细统计


public void readLog() {
		File file = new File(logLocation); 
		File[] files = file.listFiles();
		for(int i = 0;i < files.length;i++) {
			if(file.isDirectory()) {
				String filePath = files[i].getPath();
				String fileName = files[i].getName();
				//System.out.println(fileName);
				if(fileName.compareTo(logDate + ".log.txt") <= 0) {
					//System.out.println(filePath);
					statistics(filePath);
				}
			}
		}
	}

利用正则表达式详细统计


private void statistics(String filePath) {
		//[\\u4E00-\\u9FA5]+ 匹配多个中文字符
		String handleType[] = {"[\\u4E00-\\u9FA5]+ 新增 感染患者 \\d+人","[\\u4E00-\\u9FA5]+ 新增 疑似患者 \\d+人",
				"[\\u4E00-\\u9FA5]+ 感染患者 流入 [\\u4E00-\\u9FA5]+ \\d+人","[\\u4E00-\\u9FA5]+ 疑似患者 流入 [\\u4E00-\\u9FA5]+ \\d+人",
				"[\\u4E00-\\u9FA5]+ 死亡 \\d+人","[\\u4E00-\\u9FA5]+ 治愈 \\d+人","[\\u4E00-\\u9FA5]+ 疑似患者 确诊感染 \\d+人",
				"[\\u4E00-\\u9FA5]+ 排除 疑似患者 \\d+人"};
		BufferedReader reader = null;
		try {
			// 指定读取文件的编码格式,要和写入的格式一致,以免出现中文乱码
			reader = new BufferedReader(new InputStreamReader(new FileInputStream(filePath), "UTF-8")); 
			String str = null;
			while ((str = reader.readLine()) != null) {
				// 注释部分 "//"不处理
				if(!str.startsWith("//")) {
					if(str.matches(handleType[0])) {
						increaseIp(str);
					}
					else if(str.matches(handleType[1])){
						increaseSp(str);
					}
					else if(str.matches(handleType[2])){
						ipTransfer(str);
					}
					else if(str.matches(handleType[3])){
						spTransfer(str);
					}
					else if(str.matches(handleType[4])){
						dead(str);
					}
					else if(str.matches(handleType[5])){
						cure(str);
					}
					else if(str.matches(handleType[6])){
						spDiagnose(str);
					}
					else if(str.matches(handleType[7])){
						spExclude(str);
					}
				}
				//System.out.println(str);
			}
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			try {
				reader.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
	/*
	 * 对日志文件逐行处理,新增感染,全国、感染省份 感染人数增加num人
	 * @param 日志中的每一行
	 */
	private void increaseIp(String str) {
		String[] strArray = str.split(" ");	//以空格将一行输入分为一组字符串,便于统计
		String numIp = strArray[3].substring(0, strArray[3].length()-1);    //感染人数
		int num = Integer.parseInt(numIp);    //转换为整数便于四则运算
		sum[0][0] += num;    //全国感染人数增加
		for(int i = 0;i < CmdAnalysis.province.length;i++) {
			if(strArray[0].equals(CmdAnalysis.province[i])) {   //具体到某个省感染人数增加
				sum[i][0] += num;
				if(provinceShow[32] == -1) {    //按默认方式,标记日志中出现的省份 以默认显示,否则则按指定省份显示
					provinceShow[i] = 0;
				}
			}
		}
	}
......其他函数类似

按格式输出至-out 目录


        /*
	 * 处理符合条件的日志文件并输出
	 */
	public void writeLog() {
		BufferedWriter fw = null;
		try {
			File file = new File(outLocation);	
			fw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file, false), "UTF-8")); // 指定编码格式,以免写时中文字符异常
			for(int i = 0;i < provinceShow.length-1;i++) {
				if(provinceShow[i] != -1) {
					fw.append(CmdAnalysis.province[i]+" ");
					for(int j = 0;j<typeOrder.length;j++) {
						if(typeOrder[j] != -1) {
							fw.append(typeString[typeOrder[j]]+""+sum[i][typeOrder[j]]+"人 ");
						}
					}
					fw.append("\n");
				}
			}
			fw.append("// 该文档并非真实数据,仅供测试使用");
			fw.flush(); // 全部写入缓存中的内容
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			if (fw != null) {
				try {
					fw.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}

6.单元测试截图和描述

单元测试总时间(13个测试)日志文件采用example内的3个日志文件进行验证

图片
查看原图

其中10、12、13为错误输入,则在控制台提示错误,并不输出文件

图片
查看原图

剩余为正常输入 验证结果无误

图片
查看原图
图片
查看原图
图片
查看原图

结果
图片
查看原图

7.单元测试覆盖率优化和性能测试

单元测试覆盖率如下:

图片
查看原图

部分line未达100%,原因是代码中含异常处理情况如文件读写失败等,以及处理输入方面也有一些个别输入错误情况未涉及到,因此并未达到100%

图片
查看原图

性能优化 尽量使用正则表达式,而避免直接使用equals

8.项目路上的心路历程

由PSP表格实际耗费时间与计划时间出入甚大,虽然最终完成时间相差不大,但仔细观察,其中项计划与实际耗费时间有的多了有的少了,导致最终方差较小,实际这是对自己开发能力的不够了解或者说计划不甚合理,但谅在初次编程作业还可以理解,主要是自我感觉 编码能力有待加强,编写过程中,想法有了,编码有些许困难,比如一些字符串的正则表达式以及相关处理,以至于在开发过程中吃了不少苦头。

但这还只是编程问题,毕竟这些函数都接触过,只是时间久了没用遗忘了。最难的应该是对新事物的接受吧。这次的gitghub,单元测试等都是以前我从未接触过的,现在啃起来可以说是晦涩难懂,摸不着头脑,得一步一步地自主学习,想想自主学习正是我所缺少的,所以此次的作业也能锻炼自身的这方面的能力,收获不小,其次看自己的编码时间,说明自己的编码能力有待加强啊(但是累是真的累,快乐也是真的快乐-痛并快乐着?)

9.5个仓库

1.awesome-github-vue
简介:Vue相关开源项目库的汇总,包含UI组件、开发框架、实用库、服务端、辅助工具、应用实例、Demo示例,加强对vue的学习
2.WebBasicCommonDemos
简介:html、css、javascript、ajax、weex、vue.js、less、webpack等前端学习基础知识,巩固web基础
3.jQuery
简介:jQuery相关学习资源并包含部分案例
4.JavaScriptStudy
简介:JavaScript的学习代码总结,高级特性、数据结构、设计模式、typescript、vue、angular、react、node、webpack、weex、小程序、tensorflow…,JavaScript是世界上最好的语言!
5advanced-java
简介:互联网 Java 工程师进阶知识完全扫盲:涵盖高并发、分布式、高可用、微服务、海量数据处理等领域知识,后端同学必看,前端同学也可学习(利于了解后端)

posted @ 2020-02-18 09:51  _Spike  阅读(225)  评论(4编辑  收藏  举报