四则运算(Java)--温铭淇,付夏阳
GitHub项目地址:
https://github.com/fxyJAVA/Calculation
四则运算项目要求:
程序处理用户需求的模式为:
Myapp.exe -n num -r size
Myapp.exe -e
-
基本功能列表:
(1)【实现】使用 -n 参数控制生成题目的个数。
(2)【实现】使用 -r 参数控制题目中数值(自然数、真分数和真分数分母)的范围。
(3)【实现】生成的题目中计算过程不能产生负数,也就是说算术表达式中如果存在形如e1 − e2的子表达式,那么e1 ≥ e2。
(4)【实现】生成的题目中如果存在形如e1 ÷ e2的子表达式,那么其结果应是真分数。
(5)【实现】每道题目中出现的运算符个数不超过3个。
(6)【实现】程序一次运行生成的题目不能重复,即任何两道题目不能通过有限次交换+和×左右的算术表达式变换为同一道题目。生成的题目存入执行程序的当前目录下的Exercises.txt文件
(7)【实现】在生成题目的同时,计算出所有题目的答案,并存入执行程序的当前目录下的Answers.txt文件。
(8)【实现】程序应能支持一万道题目的生成。
(9)【实现】程序支持对给定的题目文件和答案文件,判定答案中的对错并进行数量统计。
PSP表:
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 40 | 60 |
· Estimate | · 估计这个任务需要多少时间 | 40 | 60 |
Development | 开发 | 1280 | 1780 |
· Analysis | · 需求分析 (包括学习新技术) | 100 | 200 |
· Design Spec | · 生成设计文档 | 50 | 100 |
· Design Review | · 设计复审 (和同事审核设计文档) | 40 | 100 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 10 | 20 |
· Design | · 具体设计 | 50 | 150 |
· Coding | · 具体编码 | 800 | 900 |
· Code Review | · 代码复审 | 30 | 50 |
· Test | · 测试(自我测试,修改代码,提交修改) | 200 | 250 |
Reporting | 报告 | 120 | 150 |
· Test Report | · 测试报告 | 90 | 120 |
· Size Measurement | · 计算工作量 | 5 | 10 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 15 | 20 |
合计 | 1430 | 2090 |
解题思路:
初看题时,眼前的一道难关就是分数的问题,真分数,假分数,分数计算等。刚开始我和搭档温,对这一步的实现,首先我和温是分开实现的,我是单纯用字符串,字符串切分进行结果的计算,十分繁杂,且不利于后续的功能实现:加括号,顺序随机化等。后来发现温将数字写成一个类,十分惊喜于这个巧妙的设计,java本身就是面向对象的语言,我却忽略了这个重要的点,纯用面向过程思维来考虑了。取长补短走过了这一道难关,将数字定义为一个类极大便利了我们后续功能的实现。后续遇到的判重问题,我想的是用HashMap的特性,一个key对应一个value,key是唯一key。如果我将算式的结果作为key,那么value就是算式本身。我们可知,结果不同的算式一定不相同,因此仅这一步我们就过滤掉了重复的问题,但因为此方法有一定的误判可能,于是在后续补上了判重的方法,来降低误判的可能。
设计实现过程
以main()为中心,通过cmd传入参数到main(String[] args)获取操作和文件路径,根据参数来判断进行何种操作 。生成算式则调用Utils的静态方法生成,要写文件和检查则调用FileUtils的静态方法生成,而Fraction类则贯穿其中,是功能实现的核心。
效能分析
因为测试的时候未发现生成算式和写文件效率并未出很低的情况,在生成算式和写文件时,以10000条数据为例,测试仅用了383毫秒,最坏情况也不超过2秒,暂无思路做到进一步的方法优化。
代码说明
Main.java,通过args获取要执行的操作:
根据程序处理用户需求的模式
1)Myapp.exe -n num -r size
生成题目,其中num为题数,size为生成数的大小
调用FileUtils.creatFile(Utils.createSuanShi(num,size))
2)Myapp.exe -e exercisefile.txt -a answerfile.txt
判断答案的正确率,其中exercisefile.txt为生成练习题的路径 、answerfile.txt为答案的路径
调用FileUtils.check(exercisePath,answerPath);
import java.io.IOException;
public class main {
public static void main(String[] args) {
int num = 0;
int size = 0;
String exercisePath = null;
String answerPath = null;
if(args.length != 0){
for(int i = 0; i < args.length; i++){
//"-n指令"
if(args[i].equals("-n")) {
try {
num = Integer.parseInt(args[i+1]);
}catch (NumberFormatException e) {
System.out.println("参数有误!");
}catch (ArrayIndexOutOfBoundsException e){
System.out.println("参数有误!");
}
}
//"-r指令"
if(args[i].equals("-r")) {
try {
size = Integer.parseInt(args[i+1]);
}catch (NumberFormatException e) {
System.out.println("参数有误!");
}catch (ArrayIndexOutOfBoundsException e){
System.out.println("参数有误!");
}
}
if(args[i].equals("-e")){
try{
exercisePath = args[i+1];
}catch (ArrayIndexOutOfBoundsException e){
System.out.println("参数有误!");
}
}
if(args[i].equals("-a")){
try{
answerPath = args[i+1];
}catch (ArrayIndexOutOfBoundsException e){
System.out.println("参数有误!");
}
}
}
if(num != 0 && size != 0){
FileUtils.creatFile(Utils.createSuanShi(num,size));
}
if(exercisePath != null && answerPath != null){
try {
FileUtils.check(exercisePath,answerPath);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
以下是相应的util类:
首先是创造算式集合方法 createSuanShi,在这里传入决定生成的算式数目,算式取值范围;
createFun则是生成算式方法,根据传过来的要求数目,while循环生成算式,放入HashMap。
这里算式结果作为key,算式本身作为value。这样做的目的是判重,当我生成的结果在hashmap中存在value,我就判定该算式不成立,算入重复,但这样做会有错误过滤。因而在该方法中又重新定义了判重的方法,思路为:先判断结果是否相同->再判断运算符数目是否相同且相等->最后判断运算数是否相等,相等则我认为是重复算式,不计入map中。
createFra为生成算数,同时判断是否要生成分数
whoBig为判断算数a,b的大小,以防出现结果为负数
最后简要说说加入()的时机,因为传参是数组的缘故,所以根据计算符号的先后位置来决定是否加入括号,例如:我传入了运算符顺序是:+,—,* 在运算到乘法的时候,因为我已经计算了+,-的结果,所以我在这里需要判断前两位操作符是什么,若为+,—,则代表着我需要加入乘法前,给已生成的算式加上()。其他情况皆以此类推。
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
public class Utils {
public static HashMap<String, String> createSuanShi(int createNum, int size) {
if (size == 0) {
size = 100;
}
HashMap<String, String> weWant = new LinkedHashMap<>();
while (weWant.size() < createNum) {
int opNum = (int) (Math.random() * 3) + 1;
String[] op = new String[opNum];
for (int j = 0; j < opNum; j++) {
int temp = (int) (Math.random() * 4);
switch (temp) {
case 0: {
op[j] = "+";
break;
}
case 1: {
op[j] = "-";
break;
}
case 2: {
op[j] = "*";
break;
}
case 3: {
op[j] = "÷";
break;
}
}
}
String[] result = createFun(op, size).split("=");
//判重
if ((weWant.get(result[1]))!=null) {
String whoAreYou = weWant.get(result[1]);
int calChar =0;
for (int a=0;a<op.length;a++) {
if (whoAreYou.contains(op[a]))
calChar++;
}
if(calChar == op.length) {
StringBuilder sb = new StringBuilder(whoAreYou);
while(sb.toString().contains("(")) {
sb.deleteCharAt(sb.lastIndexOf("("));
}
while(sb.toString().contains(")")) {
sb.deleteCharAt(sb.lastIndexOf(")"));
}
StringBuilder sb2 = new StringBuilder(result[0]);
while(sb2.toString().contains("(")) {
sb2.deleteCharAt(sb2.lastIndexOf("("));
}
while(sb2.toString().contains(")")) {
sb2.deleteCharAt(sb2.lastIndexOf(")"));
}
String[] numArr1 = sb.toString().split("[\\+\\-\\*\\÷]");
String numArr2 = Arrays.toString(sb2.toString().split("[\\+\\-\\*\\÷]"));
int xxx=0;
for (int b = 0;b<numArr1.length;b++) {
if(numArr2.contains(numArr1[b])) {
xxx++;
}
}
if(xxx==numArr1.length) {
continue;
}
}
}
weWant.put(result[1], result[0]);
}
return weWant;
}
public static String createFun(String[] op, int size) {
StringBuffer suanShi = new StringBuffer();
//确定符号数
int length = op.length;
//初始化结果为0
Fraction result = new Fraction(0, 1);
for (String c : op) {
switch (c) {
case "+": {
if (suanShi.length() > 0 && suanShi != null) {
Fraction c1 = createFra(size);
result = result.add(c1);
if ((suanShi.toString().contains("*") || suanShi.toString().contains("÷")) && (int) (Math.random() * 2) == 0 && length == 2) {
suanShi.insert(0, c1.getFraction() + "+");
} else if (suanShi.toString().contains("*") && suanShi.toString().contains("÷") && (int) Math.random() * 3 == 0) {
suanShi.insert(0, c1.getFraction() + "+");
} else {
if (length == 3 && suanShi.toString().contains("÷") && op[0] == "-") {
suanShi.insert(0, "(");
suanShi.insert(suanShi.length(), ")");
}
suanShi.append("+" + c1.getFraction());
}
} else {
Fraction a = createFra(size);
Fraction b = createFra(size);
result = a.add(b);
suanShi.append(a.getFraction() + "+" + b.getFraction());
}
break;
}
case "-": {
if (suanShi.length() > 0 && suanShi != null) {
Fraction c1 = createFra(size);
boolean flag = whoBig(result, c1);
if (flag) {
result = c1.sub(result);
} else {
result = result.sub(c1);
}
if ((flag && suanShi.toString().contains("*") || suanShi.toString().contains("÷")) && (int) (Math.random() * 3) == 0 && (length == 2 || length == 3)) {
if (length == 3 && (suanShi.toString().contains("*") || suanShi.toString().contains("÷")) && op[1] == "+") {
suanShi.insert(0, "(");
suanShi.insert(suanShi.length(), ")");
}
suanShi.insert(0, c1.getFraction() + "-");
} else if (flag && suanShi.toString().contains("*") && suanShi.toString().contains("÷") && (int) (Math.random() * 3) == 0) {
if (length == 3 && (suanShi.toString().contains("*") || suanShi.toString().contains("÷")) && op[1].equals("+")) {
suanShi.insert(0, "(");
suanShi.insert(suanShi.length(), ")");
}
suanShi.insert(0, c1.getFraction() + "-");
} else {
if (flag) {
suanShi.insert(0, "(");
suanShi.insert(suanShi.length(), ")");
suanShi.insert(0, c1.getFraction() + "-");
} else {
suanShi.append("-" + c1.getFraction());
}
}
if (length == 3 && op[1].equals("-") && (op[2].equals("*") || op[2].equals("÷"))) {
suanShi.insert(0, "(");
suanShi.insert(suanShi.length(), ")");
}
} else {
Fraction a = createFra(size);
Fraction b = createFra(size);
if (whoBig(a, b)) {
result = b.sub(a);
suanShi.append(b.getFraction() + "-" + a.getFraction());
} else {
result = a.sub(b);
suanShi.append(a.getFraction() + "-" + b.getFraction());
}
}
break;
}
case "*": {
if (suanShi.length() > 0 && suanShi != null) {
Fraction c1 = createFra(size);
result = result.mul(c1);
if (length == 3 && (op[0].equals("+") || op[0].equals("-")) && (op[1].equals("+") || op[1].equals("-")) && !suanShi.toString().contains("(")) {
suanShi.insert(0, "(");
suanShi.insert(suanShi.length(), ")");
} else {
if ((length == 2 || length == 3) && ((suanShi.toString().contains("+") || suanShi.toString().contains("-"))) && !suanShi.toString().contains("(")) {
suanShi.insert(0, "(");
suanShi.insert(suanShi.length(), ")");
}
if (length == 3 && (suanShi.toString().contains("÷") && (suanShi.toString().contains("+") || suanShi.toString().contains("-"))) && !suanShi.toString().contains("((") && op[0] != "*") {
suanShi.insert(0, "(");
suanShi.insert(suanShi.length(), ")");
}
}
suanShi.append("*" + c1.getFraction());
} else {
Fraction a = createFra(size);
Fraction b = createFra(size);
result = a.mul(b);
suanShi.append(a.getFraction() + "*" + b.getFraction());
}
break;
}
case "÷": {
if (suanShi.length() > 0 && suanShi != null) {
Fraction c1 = createFra(size);
while (c1.getNumerator() == 0) {
c1 = createFra(size);
}
result = result.div(c1);
if ((length == 3 && (op[0].equals("+") || op[0].equals("-")) && (op[1].equals("+") || op[1].equals("-")) && !suanShi.toString().contains("("))) {
suanShi.insert(0, "(");
suanShi.insert(suanShi.length(), ")");
}
if ((length == 2 || length == 3) && (suanShi.toString().contains("+") || suanShi.toString().contains("-")) && (suanShi.toString().contains("*") && suanShi.toString().contains("÷"))) {
suanShi.insert(0, "(");
suanShi.insert(suanShi.length(), ")");
}
if (length == 3 && (suanShi.toString().contains("*")) && ((suanShi.toString().contains("+") || suanShi.toString().contains("-"))) && !suanShi.toString().contains("((") && !op[0].equals("*")) {
suanShi.insert(0, "(");
suanShi.insert(suanShi.length(), ")");
}
if (length == 3 && (suanShi.toString().contains("+") || suanShi.toString().contains("-")) && (op[2].equals("-") || op[2].equals("+"))) {
suanShi.insert(0, "(");
suanShi.insert(suanShi.length(), ")");
}
if (length == 3 && op[1].equals("÷") && (op[0].equals("+") || op[0].equals("-"))) {
suanShi.insert(0, "(");
suanShi.insert(suanShi.length(), ")");
}
suanShi.append("÷" + c1.getFraction());
} else {
Fraction a = createFra(size);
Fraction b = createFra(size);
while (b.getNumerator() == 0) {
b = createFra(size);
}
result = a.div(b);
suanShi.append(a.getFraction() + "÷" + b.getFraction());
}
break;
}
}
}
return suanShi.append("=" + (result.getNumerator() == 0 ? 0 : result.getFraction())).toString();
}
private static boolean whoBig(Fraction l, Fraction r) {
boolean flag = false;
if (l.getNumerator() * r.getDenominator() < l.getDenominator() * r.getNumerator()) {
flag = true;
}
return flag;
}
private static Fraction createFra(int size) {
if ((int) (Math.random() * 3) == 0) {
return new Fraction((int) (Math.random() * size + 1), (int) (Math.random() * size) + 2);
} else {
return new Fraction((int) (Math.random() * size + 1), 1);
}
}
}
数字类,得意于将数字定义为一个类,且集合了加减乘除,约分等对分数的处理,使得后面的流程可以专注于业务需求上,计算方面只需丢给该类的方法即可完成运算。
public class Fraction {
private int numerator;//分子
private int denominator;//分母
public Fraction(int a,int b){
setNumeratorAndDenominator(a,b);
}
public void setNumeratorAndDenominator(int a, int b){ // 设置分子和分母
int c = 1;
if(a!=0) {
c = largestCommonDivisor(a, b);// 计算最大公约数
}
numerator = a / c;
denominator = b / c;
}
public int largestCommonDivisor(int a,int b){ // 求a和b的最大公约数
if(a < b){
int c = a;
a = b;
b = c;
}
int r = a % b;
while(r != 0){
a = b;
b = r;
r = a % b;
}
return b;
}
public int getNumerator(){
return numerator;
}
public int getDenominator(){
return denominator;
}
public String getFraction(){ //获得分数的表达,根据分子分母的大小关系是否化为带分数
String str;
if(denominator == 1) { //分母为1,只看分子的大小
str = String.valueOf(this.numerator);
}else if(numerator == denominator){ //分子分母相等,输出1
str = "1";
}else if(numerator > denominator){ //分子大于分母,转化为带分数
int roundNumber = numerator / denominator;
int newNumerator = numerator - denominator * roundNumber;
str = roundNumber + "'" + newNumerator + "/" + this.denominator;
}else{ //否则按正常的分数输出
str = this.numerator + "/" + this.denominator;
}
return str;
}
public Fraction add(Fraction r){ // 加法运算,通分后再运算
int a = r.getNumerator();
int b = r.getDenominator();
int newNumerator = this.numerator * b + this.denominator * a;
int newDenominator = this.denominator * b;
Fraction result = new Fraction(newNumerator,newDenominator);
return result;
}
public Fraction sub(Fraction r){ // 减法运算
int a = r.getNumerator();
int b = r.getDenominator();
int newNumerator = numerator * b - denominator * a;
int newDenominator = denominator * b;
Fraction result = new Fraction(newNumerator,newDenominator);
return result;
}
public Fraction mul(Fraction r){ // 乘法运算
int a = r.getNumerator();
int b = r.getDenominator();
int newNumerator = this.numerator * a;
int newDenominator = this.denominator * b;
Fraction result = new Fraction(newNumerator,newDenominator);
return result;
}
public Fraction div(Fraction r){ // 除法运算
int a = r.getNumerator();
int b = r.getDenominator();
int newNumerator = numerator * b;
int newDenominator = denominator * a;
Fraction result = new Fraction(newNumerator,newDenominator);
return result;
}
}
FileUtils类主要用于生成文件,和读取文件
createFile为生成文件方法,前面通过utils方法获得算式集,迭代获取算式和结果值,通过字符流(PrintWriter)分别写入到两个文件:Exercises@日期.txt Answers@日期
check方法主要用于比对答案是否正确,通过获取传进来的question获取对应的答案文件读取答案与读取到的yourAnswer文件匹配,匹配结果写入到grade文件中,以及输出到控股台。
import java.io.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
public class FileUtils {
public static void creatFile(HashMap map) {
LocalDateTime time = LocalDateTime.now();
String timeFor = time.format(DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss"));
File question = new File("D:\\Exercises@"+timeFor+".txt");
File answer = new File("D:\\Answers@"+timeFor +".txt");
try {
question.createNewFile();
answer.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
try (PrintWriter questionWrite = new PrintWriter(new FileWriter(question, true));
PrintWriter answerWrite = new PrintWriter(new FileWriter(answer, true));) {
int jiShu = 0;
for (Object entry : map.entrySet()) {
Map.Entry<String, String> temp = (Map.Entry<String, String>) entry;
String key = temp.getKey();
String value = temp.getValue();
questionWrite.write(jiShu+1+". "+value+"= ");
questionWrite.write("\r\n");
questionWrite.write("\r\n");
answerWrite.write(jiShu+1+". "+key);
answerWrite.write("\r\n");
answerWrite.write("\r\n");
jiShu++;
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void check(String question,String yourAnswer) throws IOException {
String answers = question.split("@")[1];
File answersOff = new File("D:\\Answers@"+answers);
File yourAnswers = new File(yourAnswer);
if(!yourAnswers.exists()) {
System.out.println("未找到文件,请重新确认文件路径");
System.exit(0);
}
if(!answersOff.exists()) {
System.out.println("答案文件不存在,请确认");
System.exit(0);
}
LocalDateTime time = LocalDateTime.now();
String timeFor = time.format(DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss"));
File grade = new File("D:\\Grade@"+timeFor+".txt");
grade.createNewFile();
try(BufferedReader answersOffReader = new BufferedReader(new FileReader(answersOff));
BufferedReader yourAnswersReader = new BufferedReader(new FileReader(yourAnswers));
PrintWriter gradeWrite = new PrintWriter(new FileWriter(grade))) {
String temp ;
String ans ;
StringBuilder r = new StringBuilder();
r.append("(");
StringBuilder w = new StringBuilder();
w.append("(");
int right = 0;
int wrong = 0;
ans=answersOffReader.readLine();
while ((temp=yourAnswersReader.readLine())!=null ) {
if(temp.equals("\r\n") || temp.equals("")) {
continue;
}
while(ans==null || ans.equals("")) {
ans=answersOffReader.readLine();
}
String xuHao =ans.split("\\.")[0];
if(temp.split("\\.").length>1) {
ans =ans.split("\\.")[1].trim();
String tempAnswer = temp.split("\\.")[1].trim();
if(tempAnswer.equals(ans)) {
r.append(xuHao+",");
right++;
}else {
w.append(xuHao+",");
wrong++;
}
}else {
w.append(xuHao+",");
wrong++;
continue;
}
ans = answersOffReader.readLine();
}
if(r.toString().contains(",")) {
r.deleteCharAt(r.lastIndexOf(","));
}
if(w.toString().contains(",")) {
w.deleteCharAt(w.lastIndexOf(","));
}
r.insert(r.length(),")");
w.insert(w.length(),")");
gradeWrite.write("Correct:"+right+r.toString());
gradeWrite.write("\r\n");
gradeWrite.write("Wrong:"+wrong+w.toString());
System.out.println("Correct:"+right+r.toString());
System.out.println("Wrong:"+wrong+w.toString());
}
}
}
测试运行
答题者的答案需另起一个文档,格式参照正确答案
命令行:
题目:
答案:
判断答案:
生成文件:
覆盖率:
项目小结
通过这次项目实战,我们又复习了javaSE的一些知识,比如java的集合类HashMap,文件的输入输出流,面向对象等知识,还有些细节性的一些知识点,比如为什么用StringBuilder而不用STring和StringBuffer等。又因为是两人合作开发,在开发时就不能
一意孤行,要结合队友的思路,彼此交流,言语表达要使对方能理解你的思路,探讨不足和取长补短,这样才能进行下一步的深入研究,也才有了我们现在完成的项目。通过此次开发,我们都体会到了合作开发的优势,开始有了团队意识,写代码时,会留
意注释是否到位等,另外一项收获就是如何进行项目管理,版本控制,防止开发时出现版本冲突等问题,较好的进行版本迭代。