2016012078 小学四则运算练习软件项目报告

Coding.net原码仓库地址https://git.coding.net/Meloody/Code.git

一、需求分析(前提

  话说需求分析是一份体力活儿,也是一份技术活儿,它既是人际交往的艺术,又是逻辑分析与严密思考的产物。

  具有理解能力、设计能力是分析需求必备的技能。一个UML图可以清晰地展示出需求分析。(参考博客1)但考虑此次作业涉及领域相对较小,客户需要一个四则运算的命令行软件,所以不采用UML图。

  

  • 输入 n 人性化地决定生成题目的个数

  • 运算符为:+-*÷

  • 每个数字在0-100之间,运算符3-5个

  • 每个练习题至少要包含2种运算符

  • 运算过程中不出现负数与非整数

  • 设置参数控制生成题目数量、运算符个数、计算数产生范围

  • 使用jdk8u161版本
  • 使用jre8u161版本
  • 学号与题及其正确答案输出到文件“result.txt”

  

二、功能设计(价值)

基本功能:

  • 实现四则运算出题,随机生成运算式。

扩展功能:

  • 计算分数的加减法  例如:16/5-7/3/7*3/5 = 3

  • 运算式去重

 

三、设计实现(保障

设计包括你会有哪些类,这些类分别负责什么功能,他们之间的关系怎样?你会设计哪些重要的函数,关键的函数是否需要画出流程图?函数之间的逻辑关系如何?

  

设计包括的类及负责的功能,他们之间的关系:

(一) 整体上,project项目/src下建立Main.java和WriteToFile.java文件。

(二) 共包含两个类文件,Main为主函数,主要负责实现产生随机四则运算,WriteToFile为辅助类,解决文件读写问题。

 

涉及重要函数和函数流程图及逻辑关系:

 (一) 导入类和函数:

int random(int x) 随机数类,这个类的实例用于生成伪随机数流

 

import java.util.Random;

  

ArrayList(),有序可重复的集合,其底层就是数组

 

import java.util.ArrayList;

 

Scanner,属于SDK1.5新增的一个类,可是使用该类创建一个对象

import java.util.Scanner;

BufferedWriter,缓冲字符输出流,继承于Writer,为其他字符输出流添加一些缓冲功能

import java.io.BufferedWriter;

FileWriter,字符流,用法和FileInputStream和FileOutputStream一样,只是应用的范围不一样(文件字符流用于纯文本文件的读写比较快速)

import java.io.FileWriter;

printStackTrace(),是打印出异常,还显示调用信息

e1.printStackTrace();

eval()函数,计算字符串内的运算式

 ax=ax+"="+jse.eval(ax);

(二) 流程图如下:

 

 

四、算法详解(灵魂

(一) 当被除数不能整除除数时,随机生成能够整除的除数

private static int dec(int x,int y){
		Random random=new Random();
		if(x%y!=0)//
{ y=random.nextInt(100)+1; return dec(x,y); } else{ return y; } }

(二) 通过javascript的eval函数计算字符串内的运算式

   /**
	 * 计算等式结果,返回运算式
	 */
	static ScriptEngine jse = new ScriptEngineManager().getEngineByName("JavaScript");  
	private static ArrayList<String> calculate(ArrayList<String> arrayList){
		ArrayList<String> ArithExpress=new ArrayList<String>();
		for(String ax:arrayList){
			try {
				ax=ax+"="+jse.eval(ax);
				System.out.println(ax);
				ArithExpress.add(ax);
			} catch (ScriptException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
		return ArithExpress;
	}

 

(三)逆波兰式(相似调度场)的求值:

逆波兰式式也称后缀表达式。一般求值表达式都是中缀表达式,而后缀表达式求值更容易,所以将中缀表达式转换为后缀表达式。

   

  步骤:

  第一步:将中缀表达式转换为后缀表达式。 

          1,将+,-,*,/,(等要用到的运算进行优先级处理)

          2,需要用到一个符号栈处理:

              a,数字字符,小数点直接存入另一个字符串S。

              b, "( "直接入栈,当表达式str字符中")"出现,将所有栈顶运算符转移至S直至遇到"(","( "出栈。

              c,当表达式str字符中的运算符优先级大于等于(注意一定要大于等于)栈顶运算符优先级时,将字符依次存入S,直至小于为止。当前运算符入栈。

              d,最后当str访问完,将栈中剩余运算符存到S。

 

  第二步:将后缀表达式求值。

          1,需要一个浮点型栈(具体根据题目要求)存放数值。

          2,遍历S遇见操作数,小数点时处理后入栈,直至遇见运算符,出栈两个操作数,处理后将结果再入栈。

          3,栈顶元素即为表达式解。

 

   eg. 根据后缀表达式:12 34 11 99 * / - 计算结果。
    从左到右遍历后缀表达式,
    遇到数字就进栈,
    遇到符号,就将栈顶的两个数字出栈运算,运算结果进栈,直到获得最终结果。

  1 import java.util.ArrayList;
  2 import java.util.Iterator;
  3 import java.util.List;
  4 import java.util.Stack;
  5 
  6 /*
  7  *测试 -5*(10/(2*4.5-4)+(-3/1.5+4)*(-2))/(-2/1-(-1))+12=2.0
  8  */
  9 public class Calc {
 10     // 求四则运算表达式运算结果
 11     public static double excute(String value) throws Exception {
 12         List<String> list = toList(value);// 按顺序转成数字符号list 即中序表达式
 13         list = toSuffixExpressionList(list);// 转成逆波兰式数字符号list 即后序表达式
 14         double result = suffix_excute(list);// 求逆波兰式结果
 15         System.out.println(value + "=" + result);
 16         return result;
 17     }
 18 
 19     // 表达式划分成中序list 即从左到右数字符号分开
 20     private static List<String> toList(String value) {
 21         // 开始为-时加上0
 22         if ("-".equals(value.substring(0, 1))) {
 23             value = "0" + value;
 24         }
 25         int begin = 0;
 26         int end = 0;
 27         String item;
 28         List<String> resultList = new ArrayList<String>();
 29         for (int i = 0, len = value.length(); i < len; i++) {
 30             item = value.substring(i, i + 1);
 31             if (isOperator(item)) {
 32                 // 负数跳过
 33                 if ("-".equals(item) && "(".equals(value.substring(i - 1, i))) {
 34                     continue;
 35                 }
 36                 end = i;
 37                 // 前一个非符号时加上数字
 38                 if (begin != end) {
 39                     resultList.add(value.substring(begin, end));
 40                 }
 41                 // 加上符号
 42                 resultList.add(value.substring(end, end + 1));
 43                 begin = end + 1;
 44             }
 45         }
 46         // 加上最后一个数字
 47         if (begin != value.length()) {
 48             resultList.add(value.substring(begin));
 49         }
 50         // System.out.println(value + "=" + list);
 51         return resultList;
 52     }
 53 
 54     // 中序list转换成逆波兰式list 左右根
 55     private static List<String> toSuffixExpressionList(List<String> list) throws Exception {
 56         Stack<String> operatorStack = new Stack<String>();// 符号栈
 57         Stack<String> resultStack = new Stack<String>();// 结果栈
 58         Iterator<String> iter = list.iterator();
 59         while (iter.hasNext()) {
 60             String item = iter.next();
 61             if (isOperator(item)) {
 62                 if (")".equals(item)) {
 63                     // 遇到)时符号栈一直弹出并压入结果栈直到遇到(,弹出(废弃,结束。
 64                     while (!(operatorStack.isEmpty() || "(".equals(operatorStack.peek()))) {
 65                         resultStack.push(operatorStack.pop());
 66                     }
 67                     // 弹出(
 68                     if (!operatorStack.isEmpty() && "(".equals(operatorStack.peek())) {
 69                         operatorStack.pop();
 70                     } else {
 71                         throw new Exception("(少了");
 72                     }
 73                 } else if ("(".equals(item)) {
 74                     // 遇到(时直接入符号栈,结束
 75                     operatorStack.push(item);
 76                 } else {
 77                     // 遇到其他运算符时与符号栈顶(若符号栈顶为空或(时直接入符号栈,结束)运算比较 若比栈顶高直接入符号栈,结束
 78                     // 否则符号栈弹出并压入结果栈 并再执行与符号栈顶比较直到弹入符号栈,结束
 79                     while (!(operatorStack.isEmpty() || "(".equals(operatorStack.peek()))) {
 80                         if (compareOperator(item, operatorStack.peek()) < 1) {
 81                             resultStack.push(operatorStack.pop());
 82                         } else {
 83                             break;
 84                         }
 85                     }
 86                     operatorStack.push(item);
 87                 }
 88 
 89             } else {
 90                 // 数字时直接入结果栈
 91                 resultStack.push(item);
 92             }
 93         }
 94         // 符号栈全部弹出并压入结果栈
 95         while (!operatorStack.isEmpty()) {
 96             if ("(".equals(operatorStack.peek())) {
 97                 throw new Exception("(多了");
 98             }
 99             resultStack.push(operatorStack.pop());
100         }
101         // 结果栈弹出并反序得出最终结果
102         iter = resultStack.iterator();
103         List<String> resultList = new ArrayList<String>();
104         while (iter.hasNext()) {
105             resultList.add(iter.next());
106         }
107         // System.out.println(list + "=" + rtList);
108         return resultList;
109     }
110 
111     // 逆波兰式list 求值
112     // 从左至右扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(次顶元素 op
113     // 栈顶元素),并将结果入栈;重复上述过程直到表达式最右端,最后运算得出的值即为表达式的结果。
114     private static double suffix_excute(List<String> list) {
115         Stack<Double> resultStack = new Stack<Double>();
116         Iterator<String> iter = list.iterator();
117         Double num1;
118         Double num2;
119         while (iter.hasNext()) {
120             String item = iter.next();
121             if (isOperator(item)) {
122                 num2 = resultStack.pop();
123                 num1 = resultStack.pop();
124                 resultStack.push(doOperator(num1, num2, item));
125             } else {
126                 resultStack.push(Double.parseDouble(item));
127             }
128         }
129         return resultStack.pop();
130     }
131 
132     // 比较两运算高低 1 1>2, 0 1=2 -1 1<2
133     private static int compareOperator(String operator1, String operator2) {
134         if ("*".equals(operator1) || "/".equals(operator1)) {
135             return ("-".equals(operator2) || "+".equals(operator2)) ? 1 : 0;
136         } else if ("-".equals(operator1) || "+".equals(operator1)) {
137             return ("*".equals(operator2) || "/".equals(operator2)) ? -1 : 0;
138         }
139         // 这个到不了
140         return 1;
141     }
142 
143     // +-*/基本运算
144     private static double doOperator(Double num1, Double num2, String operator) {
145         if ("+".equals(operator)) {
146             return num1 + num2;
147         } else if ("-".equals(operator)) {
148             return num1 - num2;
149         } else if ("*".equals(operator)) {
150             return num1 * num2;
151         } else if ("/".equals(operator)) {
152             return num1 / num2;
153         }
154         // 这个到不了
155         return -1;
156     }
157 
158     // 是否为运算符
159     private static Boolean isOperator(String value) {
160         return "(".equals(value) || ")".equals(value) || "+".equals(value) || "-".equals(value) || "*".equals(value)
161                 || "/".equals(value);
162     }
163 
164     public static void main(String[] args) {
165         try {
166             excute("-5*(10/(2*4.5-4)+(-3/1.5+4)*(-2))/(-2/1-(-1))+12");
167         } catch (Exception e) {
168             e.printStackTrace();
169         }
170     }
171 }
View Code

参考一看就懂的大神博客2

(四)设置参数控制生成运算符个数、计算数产生范围:

for(int i=0;i<num;i++){
                int n=random.nextInt(3)+3; //3-5个运算符
                int[] number=new int[n+1]; 
                String ex=new String();
                for(int j=0;j<=n;j++){
                    number[j]=random.nextInt(100)+1; //4-5个数字
                }

 

(五)调用ScriptEngine脚本:

static ScriptEngine jse = new ScriptEngineManager().getEngineByName("JavaScript"); 
try {
            BufferedWriter bw = new BufferedWriter(new FileWriter(path));
            bw.write("2016012078");  
            bw.newLine();
            for(String con:content){
                bw.write(con);  
                bw.newLine();
            }
            bw.close();
        } catch (IOException e1) {
            // TODO Auto-generated catch block
            e1.printStackTrace();//printStackTrace()方法是打印出异常,还显示调用信息.
        }

五、测试运行(成果)

程序的运行截图展示实现功能满足项目需求:

 

(一)运行环境jdk-8u161-windows-x64.exe;

输入java、javac、java -version,java -version回车会返回三行信息如下(第一行表Java语言的语法版本):

(二)命令行测试(清晰步骤见图):

 注意我在测试的时候发现命令行测试会出现gbk编码错误,所以要输入:javac -encoding utf-8 Main.java

(三) result.txt文件结果:

(四) Console运行结果:

 

六、满意代码(精彩)

(一) 随机产生num个运算式,将产生的运算式存入List集合:

char[] operator=new char[]{'+','-','*','/'};
			Random random=new Random();
			ArrayList<String> expression=new ArrayList<String>();
			for(int i=0;i<num;i++){
				int n=random.nextInt(3)+3; //3-5个运算符
				int[] number=new int[n+1]; 
				String ex=new String();
				for(int j=0;j<=n;j++){
					number[j]=random.nextInt(100)+1; //4-5个数字
				}
				for(int j=0;j<n;j++){
					int s=random.nextInt(4);//随机选择某运算符
					ex+=String.valueOf(number[j])+String.valueOf(operator[s]);
					if(s==3){number[j+1]=decide(number[j],number[j+1]);}
				}
				ex+=String.valueOf(number[n]);
				expression.add(ex);
			}

(二)整型转化为字符型时(Bug调试5小时),借助double类型:

 double result=0;
                try {
                     result = Double.parseDouble(String.valueOf(jse.eval(ex)));
                } catch (ScriptException e) {
                    e.printStackTrace();
                }
                if(Math.floor(result)==result&&result>0&&result<99999) {
                     System.out.println(ex + "=" + (int) result);
                     expression.add(ex);
                }
                else i--;                
            }

三)下面的方法为分式化解以及计算分数加减乘除:

public static String sim(int a,int b){
        int y = 1;
        //求分子分母的最小公因数
        for(int i=a;i>=1;i--){
            if(a%i==0&&b%i==0){
                y = i;
                break;
            }
        }
        int z = a/y;
        int x = b/y;
        if(z==0)//分子为0则结果为0
{ return "0"; } return ""+z+"/"+x; }

 ProperFraction类是分数类继承Operand类,包含分子,分母属性。加减乘除,约分,计算等方法。

public static ProperFraction cal(ProperFraction a,
                                               ProperFraction b, Symbol s){
    ProperFraction p = null;
    //计算分数加减乘除
    switch (s.getValue()){
        case "+":p = fracAdd(a.numa,a.numb,b.numa,b.numb);break;
        case "-":p = fracSub(a.numa,a.numb,b.numa,b.numb);break;
        case "*":p = fracMul(a.numa,a.numb,b.numa,b.numb);break;
        case "/":p = fractDiv(a.numa,a.numb,b.numa,b.numb);break;
    }
    return p;
}

代码去重,程序一次运行生成的题目不重复

任何两道题目不能通过有限次交换+和×左右的算术表达式变换为同一道题目。例如,56+ 45 = 和45 +56 = 是重复的题目,6 × 8 = 和8 × 6 = 也是重复的题目。在后期我会完善这个功能。

七、总结反思(提升)

 

  模块化这个词最早出现在研究工程设计的《Design Rules》中,模块化是以分治法为依据。

  简单说就是把软件整体划分,划分后的块组成了软件。我设计的程序通过需求分析构架软件、分析功能、运用类及函数,大而化小,逐步实现软件设计。Bug调试,我依旧坚持挺过来了,还记得最清楚的一个Bug,整型转化为字符型的一个小细节,花了5小时左右。

 

  这次项目,是一次很大的锻炼,我尝试解决平时没有遇到的问题,遇到问题会焦急。但是,看着自己解决了一个个模块,我也异常兴奋。这次项目,从最初的分析设计,中期编码,到后期调试,整个过程可以说,每一步都充满了挑战,但我相信艰辛过后就是甜蜜的幸福。我做到了,见到彩虹,依然微笑着。

  

  经验上,下次做项目,我会更加从大处着手,小细节交给后期,高效率也是一门艺术。

  

八、PSP展示(记录)

 

任务内容

计划共完成需要的时间(min)

实际完成需要的时间(min)

Planning

计划

400

811

·        Estimate

·   估计这个任务需要多少时间,并规划大致工作步骤

2天

4天

Development

开发

375

800

·        Analysis

·         需求分析 (包括学习新技术)

70

150

·        Design Spec

·         生成设计文档

20

15

·        Design Review

·         设计复审 (和同事审核设计文档)

3

5

·        Coding Standard

·         代码规范 (为目前的开发制定合适的规范)

8

10

·        Design

·         具体设计

10

20

·        Coding

·         具体编码

200

200

·        Code Review

·         代码复审

4

30

·        Test

·         测试(自我测试,修改代码,提交修改)

60

360

Reporting

报告

10

0

·         Test Report

·         测试报告

3

0

·         Size Measurement

·         计算工作量

2

1

·         Postmortem & Process Improvement Plan

·         事后总结, 并提出过程改进计划

10

10

  问题:这个环节重要的是让自己看到自己的估计和实际消耗时间,哪个环节耗时最多,哪个环节估计和实践相差巨大?为什么?

  

  估计和实际的总时间相差了近一倍,主要原因是代码能力不足,好好反思后,继续多多编程,认真写项目。在测试(自我测试,修改代码,提交修改)阶段耗时最多,同时也是和估计差距最大的一个环节,主要是因为自身对Bug调试的主观回避,但是这次之后,我会客观估计时间,做出更加合理的计划,对项目开发有一个更深刻的认识。

   我还有分享的就是我做项目的心情状态,我承认这个过程不是三言两语就可以让承受地压力风轻云淡,但是我始终有信念,任何一件事,花出时间和实践就会有收获!

    

1. 我们应该怎么做需求分析 https://www.cnblogs.com/mengfanrong/p/4594710.html

2.  java四则运算-通过逆波兰式求值http://tianyami.iteye.com/blog/1462498

雪儿言
原文地址:https://www.cnblogs.com/weixq351/p/8626838.html