我也肝不动了

结对编程

211606348 周世元 211606338 许燕婷

一、预估与实际

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

二、需求分析

我通过百度的方式了解到,小学三年级数学有如下的几个特点:

  • 特点
    • 只有正整数运算
    • 没学负数,不会小数运算
    • 只会运算1000以内的数
    • 小学二年级除了要满足一年级的要求,还要算仅限于九九乘法表的乘法与除法
    • 小学三年级进行四则混合运算,运算符在2~4个之间,可以加括号,减法运算的结果不能有负数,除法运算除数不能为0,不能有余数。

输入格式:

  • 程序需从命令行中接收两个参数 年级 和 题目数量,分别用 -n 和 -grade 表示,这两个参数的顺序不限,当 n 为 2 或 1 时,分别代表给 2 年级,1 年级出题

输出格式:

  • 把题目和标准答案写入out.txt文件,输出的格式要求
  • 每道题目占一行
  • 每道题的格式:
    • (1)题号:数字 + 一对英文括号;
    • (2)题目:数字、符号之间空一格;
    • (3)题号与题目之间也需要加一空格,行末不需要加空格。
    • (4)题目和标准答案分开输出,先输出所有题目,后输出所有答案,答案的输出格式与题目一样。

经过分析,我认为,这个程序应当:

  • 可接收2个参数:题目数量、年级
  • 会检测出各种输入参数的异常
  • 会根据传入的年级生成对应需求的题目
  • 最后将题目答案输出到out.txt文件
  • 采用代码重构和分而治之的思想。
  • 各部分函数代码给注释,有明确的逻辑。
  • 程序运行中、文件输出后要给明确的提示

三、设计

1. 设计思路

  • 这个程序有2个类,7个函数,功能如下:

  • 主类和内部类

    • 内部类:Node (用来存储后缀表达式)
  • 主函数

    • 主函数里调用check()函数排除输入参数的各种问题
    • 主函数里判断当前输入参数的年级,根据年级执行不同的题目生成器
    • 主函数里根据对应的年级调用init3()函数初始化三年级题目和答案
    • 主函数里根据对应的年级调用init1_2()函数初始化1、2年级题目和答案
    • 主函数里调用OutPutFile()函数将题目和答案输出到out.txt文件中
  • check()函数用来排除输入参数的各种问题,函数流程如下:

    • 判断输入的参数的长度(只能为4)
    • 判断是否用-n 和-grade作为标识符
    • 取出参数的前导0(类似0000001的情况)
    • 检查参数题目数量必须都是数字
    • 检测年级参数是否是1-3
    • 判断题目的数量是否为正整数
  • init3()函数用来生成题目和答案,函数流程如下:

    • 生成3个可变字符串
    • 随机生成2-4个运算符(必须有2个不同)
    • for循环生成题目
    • 随机在+、-运算符左右边的数字分别生成2个括号
    • 将随机生成的题目丢用逆波兰表达式算出答案并保存
    • 将题目和答案拼接成字符串
  • init1_2()函数用来生成题目和答案,函数流程如下:
    -直接引用1.2年级的题目生成器

  • CalPoland()函数

    • 实现逆波兰表达式
  • ShuntYardAlgo()函数

    • 实现调度场算法
  • OutPutFile()函数

    • 将题目和答案输出到out.txt文件
  • 函数流程图:

  • 算法的关键:

    • 判断输入的参数是否合法,对于不合法的各种情况需给予提示
    • 生成带括号的四则运算的题目是否满足教学大纲的要求

2. 实现方案

写出具体实现的步骤

  • 准备工作:
    • 先在Github上Fork厂库,并克隆到本地
    • 编写MathExam.java
    • 在三年级题目的基础上改进逻辑并增加1.2年级出题功能
    • 重构代码,并增加注释
    • 代码测试
  • 技术关键点:
    • 生成带括号的四则运算
    • 拼接可变字符串
    • 按需求产生随机数
    • 按需求产生随机运算符和括号
    • 输入参数的各种判断
    • 逆波兰表达式
    • 调度场算法
    • 文件io流

四、编码

编码过程:

  • main函数:

    • 调用check()函数判断输入参数是否有异常
    • 没有异常的情况下判断年级
    • 如果是1.2年级就将4个当前输入的参数,删除2个标识符其余封装在一个数组中传给init1_2()函数中进行1.2年级题目初始化.并调用outPutFile()函数输出out.txt文件。
    • 如果是3年级就直接调用init3()函数中进行3年级题目初始化.并调用outPutFile()函数输出out.txt文件。
  • init3()函数:

    • 创建1个可变字符串str1用于拼接答案
    • 创建1个for循环
      • 循环内生成2个随机数
      • 用str拼接题目
      • 用条件判断语句判断当前的运算符并算出答案
      • str1拼接答案
      • str1拼接到str尾部
  • init1_2()函数:

    • 和作业1的init()函数功能相同

遇到的问题:

  • 带括号的四则运算如何生成

查阅别人的博客并没有找到带括号的四则运算。然后我就先试着生成随机的没有括号的四则运算,完成任务后我再考虑如何加上括号,设计了好久终于加了一些if判断并且生成了带括号的四则运算了。

  • 如何算出题目的答案

原先我是打算在for循环的生成随机数当中加一些判断然后直接在循环中生成答案并且保存下来。四则运算生出来后我发现我在for循环当中在生成了随机数后并没有保存下来,而且实现起来也很难。我就打算用老师推荐的调度场算法和逆波兰表达式。一开始我查阅各种资料,终于了解这个算法是如何算出答案的,也会用了,但是自己独立写出来太吃力了,就将博客上面用c语言写的调度场算法和逆波兰表达式改成了java的表达方式,解决一堆报错,然后我就可以直接拿来用了。

  • 由于标识符-n -grade 的位置可以任意交换,在检测参数错误的时候会有非常多的冗余代码
    • 解决方案:args[args[0].equals("-n") ? 1 : 3]

这个问题我想了很久,发现在每次涉及到4个参数判断的时候都要考虑-n -grade 谁在前?谁在后?的问题,生成很多的冗余代码,后面是大佬李佳将他想了很久的方法分享给我,我也采用了他的问号表达式的方法来解决的,代码看起来清新多了。

  • 将c语言的调度场算法和逆波兰表达式改写成java的代码,老是会报数组跃界异常
    这个问题可谓是当初让我冥思苦想,一直苦苦解决不了,查了很多资料才突然想起来,c语言是数组以'\0'为结尾,当时我是建立了一个char[100]的数组,我每次传字符串进去就会报错,如果将字符串拆分成字符传入就不会报错。原因是,我创建的char有100个空间,我当时采用的是str.toString 的方法将string转成char,char数组剩下的一些空间自动赋初值为0,我当时的循环判断的是while(str[p]!=0),当p++,str[p]为0 所以不会跃界,程序也能正常运行。而java里面字符串不是以'\0'为结尾的,一跃界就会报异常了。

1. 调试日志

  • ShuntYardAlgo()函数,字符串传入报数组跃界,字符串转字符传入不会报错。

解决:这是我本次开发浪费最多时间调试的bug。原因知道后其实很简单,字符串转字符我没用str.toCharrArrays()这个方法,而是自己开辟一个char[100],默认舒适化是0。当遍历字符串末尾的时候自然就跃界报错了。

  • 如何生成带括号的四则运算

解决:当时在这个地方也困扰了我很久,一步步的增加判断,随机安排括号,由于不好控制括号出现的位置我调试了很久才生成带括号的四则运算

2. 关键代码

	public static StringBuffer init3(String[] args) throws Exception {
		int m = 1;
		int l = 1;
		String str = null;
		// 生成2个字符串用于拼接题目和答案
		StringBuffer strbuf = new StringBuffer();
		StringBuffer strbuf1 = new StringBuffer();
		StringBuffer strbuf2 = new StringBuffer();
		// random用于随机生成题目中的操作数
		int random = 0;
		// int flag=5;
		// 将四种运算符放入数组
		String[] strop = { "+", "-", "x", "÷" };
		// 将随机生成的2-4个运算符存入ostr数组
		String[] ostr = new String[4];
		int w = Integer.valueOf(args[args[0].equals("-n") ? 1 : 3]);
		for (int i = 0; i < w; i++) {
			// 随机生成的2-3个运算符
			int operator = (int) (Math.random() * 3) + 2;
			int k = 0;
			// 随机生成运算符号(至少有2种不同运算符号)
			for (int j = 0; j < operator; j++) {
				k = (int) (Math.random() * 4);
				ostr[j] = strop[k];
				if (operator == 2 && j == 1) {
					// 控制在只有2个运算符的情况下2个运算符号一定不同
					while (ostr[0] == ostr[1]) {
						k = (int) (Math.random() * 4);
						ostr[0] = strop[k];
					}
				}
			}
			int flag1 = 1;
			int flag = 1;
			for (int j = 0; j < operator + 1; j++) {
				if (operator == 4)
					// 如果运算符有+-就自动生成括号运算符
					if (j < operator && (ostr[j].equals("+") || ostr[j].equals("-")) && flag == 1) {
						strbuf.append("( ");
						flag = 0;// 判断是否加了左括号0代表已近加了
						flag1 = 0;// 判断是否可以加右括号了
					}
				random = (int) (Math.random() * 101);
				if (j == operator) {
					// 最后一个随机数的末尾不加空格
					if (flag == 0 && flag1 == 1) {
						strbuf.append(random + " )");
						flag1 = 0;
						flag = 100;
					} else {
						strbuf.append(random);
						if (flag == 0) {
							// 在最后一个数后面加上右括号
							flag1 = 1;
						}
					}
 				} else {
					if (flag == 0 && flag1 == 1) {
						strbuf.append(random + " )" + " ");
						flag1 = 0;
						flag = 100;
					} else {
						strbuf.append(random + " ");
						if (flag == 0) {
							// 在下一次循环当中可以加入右括号
							flag1 = 1;
						}
					}
				}
				if (j < operator)
					strbuf.append(ostr[j] + " ");
			}
			l = 1;
			// 重新初始化数组
			ostr = new String[4];
			//
			MathExam mathExam = new MathExam();
			mathExam.Init();
			str = strbuf.toString();

			char[] charArray = str.toCharArray();
			double shuntYardAlgo = mathExam.ShuntYardAlgo(charArray);
			String vf = String.valueOf(shuntYardAlgo);
			boolean matches = vf.matches("\\d+.[0]?");
			if (matches) {

				// strbuf2用于拼接题目
				strbuf2.append("(" + m + ")" + " " + strbuf + "\r\n");
				// strbuf1用于拼接答案
				strbuf1.append("(" + m + ")" + " " + strbuf + " " + "=" + " " + (int) shuntYardAlgo + "\r\n");
				m++;
				l = 0;
			}
			strbuf = new StringBuffer();
			if (l != 0)
				i--;
		}
		strbuf2.append("\r\n");
		strbuf2.append(strbuf1);
		return strbuf2;
	}

3. 代码规范

请给出本次实验使用的代码规范:

  • 第一条代码中的命名均不以下划线或美元符号开始,也不一下划线或美元符号结束。
  • 第二条代码中的命名严禁使用拼音与英文混合的方式,更不允许直接使用中文的方式。
  • 第三条类名使用UpperCamelCase风格,但是以下情形例外:DO / DTO / VO / AO / PO 等。
  • 第四条方法名、参数名、成员变量、局部变量都统一使用 lowerCamelCase 风格,必须遵循驼峰形式。
  • 第五条变量和常量的命名方式:非公有(private/protected/default)变量前面要加上小写m;
  • 第六条类型与中括号紧挨相连来定义数组。正例:定义整形数组 int[] arrayDemo;并人工检查代码是否符合规范
  • 第七条杜绝完全不规范的缩写,避免忘文不知义。反例:AbstractClass“缩写”命名成AbsClass;condition“缩写”命名成 condi,此类随意缩写严重降低了代码的可阅读性。
  • 第八条为了达到代码自己自解释的目标,任何自定义编程元素在命名时,使用精良完整的单词组合来表达其意。但是有些名称实在过于长,这个可以适当的缩写,不要忘文不知义就可以。这个不是客观的规定。
  • 第九条不允许任何魔法值(即未经预先定义的常量)直接出现在代码中。
  • 第十条大括号的使用约定。如果是大括号内为空,则简介地写成{}即可,不需要换行;如果是非空代码块则:
    • 左大括号前不换行。
    • 左大括号后换行。
    • 右大括号前换行。
    • 右大括号后还有 else 等代码则不换行;表示终止的右大括号后必须换行。


五、测试

| 测试 | 预期结果 | 实际结果 |
| --------------------------------------- | -------------------------------- | -------- | -------- |
| 不输入参数 | -grade和-n标识符错误!! | -grade和-n标识符错误!!|
|只输入一个参数:100 | -grade和-n标识符错误!! | -grade和-n标识符错误!!|
| -n 10 -grade 1 | 题目已经生成,详情请见out.txt | 题目已经生成,详情请见out.txt |
| -n 10.5 -grade 1 | 请输入正整数 | 请输入正整数 |
| -n ascc -grade 2 | 请输入正整数 | 请输入正整数 |
| -n 10 -grade vsdv | 年级参数错误,只能在[1~3]以内 | 年级参数错误,只能在[1~3]以内 |
| -n 00001 -grade 3 | 题目已经生成,详情请见out.txt | 题目已经生成,详情请见out.txt |
| -n 1000 -grade 2.3 | 年级参数错误,只能在[1~3]以内 | 年级参数错误,只能在[1~3]以内 |
| -n 10 -grade 002 | 题目已经生成,详情请见out.txt | 题目已经生成,详情请见out.txt |
| -n 10000 -grade 3 | 题目已经生成,详情请见out.txt | 题目已经生成,详情请见out.txt |
| -n -1 -grade 3 | 请输入正整数 | 请输入正整数 |
| 1000 -n -grade 2 | -grade和-n标识符错误!! | -grade和-n标识符错误!! |
| -n 10 -grade -3 | 年级参数错误,只能在[1~3]以内 | 年级参数错误,只能在[1~3]以内 |
| -n 1000 2 -grade | -grade和-n标识符错误! | -grade和-n标识符错误! |
| -grade 2 -n 1000 | 题目已经生成,详情请见out.txt | 题目已经生成,详情请见out.txt |
| -grade 0.1 -n 800 | 年级参数错误,只能在[1~3]以内 | 年级参数错误,只能在[1~3]以内 |
| -grade a1 -n 10 | 年级参数错误,只能在[1~3]以内 | 年级参数错误,只能在[1~3]以内 |
| -grade 001 -n 20 | 题目已经生成,详情请见out.txt | 题目已经生成,详情请见out.txt |
| -grade 1 -n 0000000002 | 题目已经生成,详情请见out.txt | 题目已经生成,详情请见out.txt |
| -grade 0.1 -n 0.1 | 请输入正整数 | 请输入正整数 |
| -n a1 -grade a1 | 请输入正整数 | 请输入正整数 |

六、总结

  • 周四下午喉咙痛,周五、周六感冒发烧喉咙痛,周日下午好一些了,周日晚上就开始起来补代码,周日、周一晚上都是写到2点才睡觉,周二写到3点半才睡着,着实是有些累人了,感冒刚好,但是感觉舍友都在那么努力打代码,自己也不能落下吧。
  • 先说我很后悔的就是没有履行好上篇博客的总结中对下次任务的规划。我草草的做了需求分析和设计,唯一进步的可能就是不像第一次那样直接上代码。
  • 由于设计没做好我感觉我选择了一个错误的方向,所以终日在出题、出带括号的四则运算当中徘徊,虽然最后是干出来了,当时浪费的时间可想而知,舍友跟我说你应该先研究透调度算法和逆波兰表达式,等你看懂了,知道他是如何检测一个题目并且算出答案的,你就可以按照它的需求来出题,而不是自己瞎出一通。
  • 我觉得他说的话有一定的道理。之所以这么说是因为我目前也还没看懂老师提供c语言编写的调度算法和逆波兰表达式,我只是知道了调度算法和逆波兰表达式的原理。我可以金准的说看懂了一些原理吧,至少知道如何算出一个后缀表达式,如何用逆波兰表达式计算后缀表达式了。
  • 也许也是因为时间不太够,或许是生病脑子不太清醒。我还是说说我是如何实现作业的吧。
  • 在我卡在了生成带括号四则运算的不归路上,我决定换条路,就去研究了一下下调度场算法和逆波兰表达式,毫无疑问我看懵了,查各种博客资料我才了解了原理,但是很为难的是我发现我怎么都写不出来,放弃吗?当然我就是选择了放弃,我花了1个小半时把c语言版的调度场算法和逆波兰表达式改成java的,直接拿来用,下面的路就很清晰了,只需要生成带括号的四则运算再丢给逆波兰表达式处理的到答案就ok了。
  • 接下来就是死肝之路,带括号的四则运算也许是我的感冒好了一点头脑清醒了一些,花了些时间居然写出来了,尽管bug漫天纷飞,但是至少是出来了可以看到的东西,死磕到底。
  • 再总结一下,这次比上次还是有一丁丁丁点进步的,开头做了需求分析和设计。
  • 最后说说我在结对编程的感受吧,两人之间互相鼓励,不容易沮丧也能增加我的工作积极性。因为在面对问题的时候,会有人一起分担,共同尝试新的策略。
  • 她还能帮我重构代码,帮我写代码,两个人都很轻松,代码看起来再也不乱糟糟的了,编程效率也高了,我发现不了的bug她也能帮我找出来,及时找不出来也能帮我换个方向去查bug。
  • 最好的就是,有个一起打代码的伴多好啊,不是吗???

posted @ 2018-09-18 20:06  周世元ISO8859-1  Views(234)  Comments(1Edit  收藏  举报