结对项目:四则运算(C语言)
github地址:https://github.com/nilonger/arithmetic
结对伙伴:杨锐龙+黄海钊
一、项目要求
1.1 题目:实现一个自动生成小学四则运算题目的命令行程序(也可以用图像界面,具有相似功能)。
1.2 说明:
-
真分数:1/2, 1/3, 2/3, 1/4, 1’1/2, …。
-
自然数:0, 1, 2, …。
-
运算符:+, −, ×, ÷。
-
括号:(, )。
-
等号:=。
-
分隔符:空格(用于四则运算符和等号前后)。
-
算术表达式:
e = n | e1 + e2 | e1 − e2 | e1 × e2 | e1 ÷ e2 | (e),
其中e, e1和e2为表达式,n为自然数或真分数。
-
四则运算题目:e = ,其中e为算术表达式。
1.3 项目要求
-
(完成) 使用 -n 参数控制生成题目的个数,例如 Myapp.exe -n 10 将生成10个题目。
-
(完成) 使用 -r 参数控制题目中数值(自然数、真分数和真分数分母)的范围,例如 Myapp.exe -r 10 将生成10以内(不包括10)的四则运算题目。该参数可以设置为1或其他自然数。该参数必须给定,否则程序报错并给出帮助信息。
-
(完成) 生成的题目中计算过程不能产生负数,也就是说算术表达式中如果存在形如e1− e2的子表达式,那么e1≥ e2。
-
(完成) 生成的题目中如果存在形如e1÷ e2的子表达式,那么其结果应是真分数。
-
(完成) 每道题目中出现的运算符个数不超过3个。
-
程序一次运行生成的题目不能重复,即任何两道题目不能通过有限次交换+和×左右的算术表达式变换为同一道题目。例如,23 + 45 = 和45 + 23 = 是重复的题目,6 × 8 = 和8 × 6 = 也是重复的题目。3+(2+1)和1+2+3这两个题目是重复的,由于+是左结合的,1+2+3等价于(1+2)+3,也就是3+(1+2),也就是3+(2+1)。但是1+2+3和3+2+1是不重复的两道题,因为1+2+3等价于(1+2)+3,而3+2+1等价于(3+2)+1,它们之间不能通过有限次交换变成同一个题目。
-
生成的题目存入执行程序的当前目录下的Exercises.txt文件,格式如下:
-
四则运算题目1
-
四则运算题目2
……
其中真分数在输入输出时采用如下格式,真分数五分之三表示为3/5,真分数二又八分之三表示为2’3/8。
-
在生成题目的同时,计算出所有题目的答案,并存入执行程序的当前目录下的Answers.txt文件,格式如下:
答案1
答案2
-
真分数的运算如下例所示:1/6 + 1/8 = 7/24。
-
程序应能支持一万道题目的生成。
-
程序支持对给定的题目文件和答案文件,判定答案中的对错并进行数量统计,输入参数如下:
Myapp.exe -e .txt -a .txt 统计结果输出到文件Grade.txt,格式如下:
Correct: 5 (1, 3, 5, 7, 9)
Wrong: 5 (2, 4, 6, 8, 10)
其中“:”后面的数字5表示对/错的题目的数量,括号内的是对/错题目的编号。为简单起见,假设输入的题目都是按照顺序编号的符合规范的题目。
二、PSP表
PSP2.1 |
Personal Software Process Stages |
预估耗时(分钟) |
实际耗时(分钟) |
Planning |
计划 |
100 |
200 |
· Estimate |
· 估计这个任务需要多少时间 |
60 |
70 |
Development |
开发 |
6*24*60 |
5*24*60 |
· Analysis |
· 需求分析 (包括学习新技术) |
60 |
50 |
· Design Spec |
· 生成设计文档 |
20 |
20 |
· Design Review |
· 设计复审 (和同事审核设计文档) |
30 |
30 |
· Coding Standard |
· 代码规范 (为目前的开发制定合适的规范) |
20 |
20 |
· Design |
· 具体设计 |
100 |
150 |
· Coding |
· 具体编码 |
4*24*60 |
4*24*60 |
· Code Review |
· 代码复审 |
100 |
150 |
· Test |
· 测试(自我测试,修改代码,提交修改) |
40 |
60 |
Reporting |
报告 |
20 |
20 |
· Test Report |
· 测试报告 |
10 |
30 |
· Size Measurement |
· 计算工作量 |
10 |
30 |
· Postmortem & Process Improvement Plan |
· 事后总结, 并提出过程改进计划 |
30 |
30 |
合计 |
|
6*24*60 |
5*24*60 |
三、解题思路
1、先实现生成式子的功能。
(1)先写好生成操作数和包含1~3个操作符的式子的四个函数,再通过一个函数一起调用,随机生成带括号的式子
(2)除号和乘号÷、×先用*、#代替
(3)C语言里面的除号和乘号÷、×不能直接从文件读出来,所以就放弃了从文件里读出来计算的想法,改为生成的时候,把式子放进数组再来计算,计算不小于0,才把数组存进文件,存进文件的时候把*、#这两个符号替换成÷、×
2、计算功能。
(1)只定义了一个分数结构体,因为想到计算的时候 整数也可以转化为分数,如2可以化为2/1
(2)定义两个栈,手撸栈的各个操作(C语言没办法),一个栈用来压入分数,一个用来压入操作符
3、比较题目文件,判断答案文件中的对错。
(1)这个可以说是取巧了一下,因为文件的生成,帮随着答案的生成,所以比较的是 标准答案文件 和 新答案文件
(2)原本是想从文件里面再读出式子拿来计算,但是写进文件后有了符号÷、×,但是那两个符号÷、×读出来会出错,因为他们不是普通字符型数据的长度。
4、主函数。
用输入参数的方式在cmd运行,很好的分开了生成式子(同时生成答案)和比较答案这两个步骤
四、设计实现过程
五、代码说明
头文件和相关结构体和栈的定义:
#include<stdio.h> #include<stdlib.h> #include <time.h> #include<string.h> #include<io.h> #include<math.h> #define MINSIZE 256 #define MAXSIZE 1024 #define OK 1 #define ERROR 0 int r,n; //生成数的最大值 和 式子数量 typedef int Status; char* oneOperator(); char* creatOperator(); char* threeOperator(); char* twoOperator(); char *creatFormula(int y); char* creatSnum(); Status Answer_Sq(char formula[],int y); typedef struct StackNode{ //分数 栈结点 int fm; //分母 int fz; //分子 struct StackNode *next; }node1; typedef struct Stackop { //运算符 栈结点 int data; struct Stackop *next; }node2; typedef struct stack1 { //分数栈 node1 *top; int length; }StackSq1; typedef struct stack2 { //运算符栈 node2 *top; int length; }StackSq2;
栈的相关操作:
Status InitStack_Sq1(StackSq1 &S){ //初始化 运算数 的栈 S.top=NULL; S.length=0; return OK; } Status InitStack_Sq2(StackSq2 &S){ //初始化 运算符 的栈 S.top=NULL; S.length=0; return OK; } Status StackEmpty_Sq(StackSq2 S){ // 对 运算符 判空,若为空则返回TURE,否则返回FALSE if(S.length==0) return OK; else return ERROR; } Status Push_Sq1(StackSq1 &S,int fenzi,int fenmu){ //分数 进栈 node1 *p; p=(node1 *)malloc(sizeof (node1)); p->fm=fenmu; p->fz=fenzi; p->next=S.top; S.top=p; S.length++; return OK; } Status Push_Sq2(StackSq2 &S,int e){ //运算符 进栈 node2 *p; p=(node2 *)malloc(sizeof (node2)); p->data=e; p->next=S.top; S.top=p; S.length++; return OK; } node1 Pop_Sq1(StackSq1 &S){ //记得类型是 node1 node1 A; node1 *p=S.top; A.fm=p->fm; A.fz=p->fz; S.top=p->next; free(p); S.length--; return A; } Status Pop_Sq2(StackSq2 &S){ // 运算符栈顶 出栈 int e; node2 *p=S.top; e=p->data; S.top=p->next; free(p); S.length--; return e; } Status Get_Top(StackSq2 S){ //取 运算符 栈顶元素 (不出栈) if(S.top==NULL)return ERROR; return S.top->data; }
生成式子的相关函数:
char* creatOperator() //生成运算符 { srand((unsigned)time(NULL) + rand()); char* c = new char[2]; int a = rand() % 4; //int a = 1; switch (a) { case 1: strcpy(c, "+"); break; case 2: strcpy(c, "-"); break; case 3: strcpy(c, "*"); break; case 0: strcpy(c, "#"); break; default: break; } return c; } char* creatSnum() //生成运算数 { srand((unsigned)time(NULL) + rand()); char* string = NULL; char string1[MAXSIZE] = {}; string = string1; char c[MAXSIZE] = {}; int tag = rand() % 2; int num1=0,num2=0; if (tag == 0) { num1 = rand() % (r)+1; //不要(size+1),保证整数不为 0 sprintf(c, "%d", num1); strcat(string1, c); } else { num1 = rand() %(r-1) ; //最大为 m-2,方便后面算法 if (num1 != 0) { sprintf(c, "%d", num1); strcat(string1, c); strcat(string1, "/"); } while (num2 == 0 || num2 <= num1) { num2 = rand() % r; //最大为 m-1 } sprintf(c, "%d", num2); strcat(string1, c); } return string; } char* oneOperator() //一个操作符的式子 { srand((unsigned)time(NULL) + rand()); char string[MINSIZE] = {}; char* posture = string; char c[MINSIZE] = {}; strcpy(c, creatSnum()); strcat(string, c); strcpy(c,creatOperator()); strcat(string, c); //printf("%s\n", string); strcpy(c, creatSnum()); strcat(string, c); //printf("%s\n", string); return posture; } char* twoOperator() //两个操作符得式子 { srand((unsigned)time(NULL) + rand()); int flag = 0; int tag=0; char string[128] = {}; char* posture = string; char c[MAXSIZE] = {}; flag = rand() % 2; if (flag == 0) { strcpy(c, "("); strcat(string, c); tag = 1; } strcpy(c, creatSnum()); strcat(string, c); strcpy(c,creatOperator()); strcat(string, c); strcpy(c, creatSnum()); strcat(string, c); flag = rand() % 2; if (flag == 0) { if (tag == 1) { strcpy(c, ")"); strcat(string, c); } tag = 0; } strcpy(c,creatOperator()); strcat(string, c); strcpy(c, creatSnum()); strcat(string, c); if (tag == 1) { strcpy(c, ")"); strcat(string, c); } //printf("%s\n", string); return posture; } char* threeOperator() //三个操作符得式子 { srand((unsigned)time(NULL) + rand()); char string[128] = {}; char* posture = string; char c[MAXSIZE] = {}; strcpy(c, creatSnum()); strcat(string, c); strcpy(c,creatOperator()); strcat(string, c); strcpy(c, creatSnum()); strcat(string, c); strcpy(c,creatOperator()); strcat(string, c); strcpy(c, creatSnum()); strcat(string, c); strcpy(c,creatOperator()); strcat(string, c); strcpy(c, creatSnum()); strcat(string, c); return posture; } char pan[5]={"-1"}; char *mp=pan; char duan[5]={"1"}; char *np=duan; char *creatFormula(int y) //生成式子 { srand((unsigned)time(NULL) + rand()); // char op[3]; char string[MAXSIZE] = {}; char* posture = string; int a = rand() % 3; //生成的随机数,即运算符个数 //printf("%d\n", a); FILE *fp; fp=fopen("test.txt","a"); int i=0; char divi[5]={"÷"}; //用于后面存进文件 char mult[5]={"×"}; switch (a) { case 0:strcpy(string, oneOperator()); // printf("%s\n",posture); //和文件作比较 break; case 1:strcpy(string, twoOperator()); // printf("%s\n",posture); break; case 2:strcpy(string, threeOperator()); // printf("%s\n",posture); break; default:break; } if(Answer_Sq(posture,y)<0) //计算的时候用 数组 算,打印则要转化一下 { fclose(fp); return mp; // 因为是 char 型函数,不能直接返回-1 } else { // fprintf(fp,"%d.%s = \n",y,posture); fprintf(fp,"%d.",y); while(string[i]!='\0') { if(string[i]=='#') { fprintf(fp,"%s",divi); i++; continue; } else if(string[i]=='*') { fprintf(fp,"%s",mult); i++; continue; } fprintf(fp,"%c",string[i]); i++; } fprintf(fp,"=%c",'\n'); fclose(fp); return np; } // fclose(fp); // return posture; }
优先级判断,中间值计算,化简,生成答案文件等函数:
int Priority(char op) //判断操作符优先级 { switch (op) { case '(': return 4; case '/': return 3; case '*': case '#': //除号 return 2; case '+': case '-': return 1; default : return 0; } } int gcd(int x,int y)//辗转相除法 ,两整数的最大公约数 { if(y==0) return x; return gcd(y,x%y); } node1 CalculatorSq(node1 a,node1 b,int c){ // 两个数的计算 node1 d; switch(c) { case'+': d.fz=b.fz*a.fm + b.fm*a.fz; d.fm=a.fm*b.fm; break; case'-': d.fz=b.fz*a.fm - b.fm*a.fz; d.fm=a.fm*b.fm; break; case'*': d.fz=b.fz*a.fz; d.fm=a.fm*b.fm; break; case'#': case'/': d.fz=b.fz*a.fm; d.fm=b.fm*a.fz; break; default:break; } return d; } void Simpli_Fenshu(int z,int m,char strings[]) { //分数化简(将原始分数化为 真分数、带分数、整数) int a,b,c; char h[20]={}; if(z%m==0) { a=z/m; itoa(a,strings,10); //把 a转化为字符,存在strings[]中 // printf("%s\n",strings); //可以直接在 exe窗口 看答案 } else if(z>m) { a=z/m; b=z-a*m; c=gcd(b,m); //求新的分子分母的 最大公因数 itoa(a,strings,10); strcat(strings,"'"); sprintf(h,"%d",b/c); strcat(strings,h); strcat(strings,"/"); sprintf(h,"%d",m/c); strcat(strings,h); // printf("%s\n",strings); } else if(z<m) { c=gcd(z,m); a=z/c; b=m/c; itoa(a,strings,10); strcat(strings,"/"); itoa(b,h,10); strcat(strings,h); // printf("%s\n",strings); } } Status Answer_Sq(char formula[],int y){ //计算答案 ,formula[] 为传进来的式子 StackSq1 S_num; //分数栈 StackSq2 S_ope; //运算符 栈 int i=0,tmp=0; node1 A,B,C; //结点 int D; //运算符 FILE *fp; fp=fopen("answer.txt","a"); if(InitStack_Sq1(S_num)!=OK || InitStack_Sq2(S_ope)!=OK) { printf("初始化栈失败"); exit(1); } while(formula[i]!='\0' || StackEmpty_Sq(S_ope)!=OK) { if(formula[i]>='0'&&formula[i]<='9') //判断数字 { tmp=tmp*10+formula[i]-'0'; //后面还是数字,继续 i++; if(formula[i]>'9' || formula[i]<'0') //判断 后一个 是否 字符 { Push_Sq1(S_num,tmp,1); //把整数化为 分数 进栈 tmp=0; //清零,用于记录下一个 数 } } else //运算符 { //等级高的 栈顶是左括号',str[i]不是右括号 运算符栈为空 if((Priority(formula[i]) > Priority(Get_Top(S_ope))) || (Get_Top(S_ope)=='(' && formula[i]!=')') || (StackEmpty_Sq(S_ope)==OK)) { Push_Sq2(S_ope,formula[i]); //运算符 进栈 i++; continue; } if(Get_Top(S_ope)=='(' && formula[i]==')') { Pop_Sq2(S_ope); //栈顶 括号 出栈,不计算 i++; continue; } // 新的运算符优先级 比 运算符栈顶的 低 数字空了,运算符还没空 新的运算符是右括号 if((Priority(formula[i]) <= Priority(Get_Top(S_ope))) || (formula[i]=='\0'&&StackEmpty_Sq(S_ope)!=OK) || formula[i]==')') { A=Pop_Sq1(S_num); //出栈 两个数 B=Pop_Sq1(S_num); D=Pop_Sq2(S_ope); //出运算符 // printf("aaa%d/%d\n",A.fz,A.fm); //这些注释都是为了在exe窗口显示 计算过程 // printf("bbb%d/%d\n",B.fz,B.fm); // printf("ccc%c\n",D); C=CalculatorSq(A,B,D); //计算中间值 (注意这里!!!) // printf("daan%d/%d\n",C.fz,C.fm); Push_Sq1(S_num,C.fz,C.fm); //中间值为正值,入栈 continue; } } } if(C.fz<0) { fclose(fp); return -1; // 负数就返回 -1 } char daan[20]={}; //用来存答案 Simpli_Fenshu(C.fz,C.fm,daan); //返回简化后的答案 fprintf(fp,"%d.%s\n",y,daan); fclose(fp); return 1; }
判断答案文件对错函数:
void CheckAnswer(char eflie1[],char afile2[]){ FILE *p1,*p2,*p3; p1=fopen("answer.txt","r"); p2=fopen(afile2,"r"); //传进来的文件名会变 p3=fopen("Grade.txt","w"); char answer1[20]={}; //放标准答案的数组 char answer2[20]={}; //放新答案文件 fgets出来的答案 int y=1; //第一条式子 int c=0,w=0; //用于计算对和错的总题数 int correct_d[10000]={}; //用于储存 对的和错的题号 int wrong_d[10000]={}; while(fgets(answer1,20,p1)!=NULL && fgets(answer2,20,p2)!=NULL) { //不要fgets 10个字符 // printf("%s",answer1); // printf("222%s",answer2); if(strcmp(answer1,answer2)==0) //比较前n个字节的大小 { correct_d[c++]=y; y++; } else if(strcmp(answer1,answer2)!=0) { wrong_d[w++]=y; y++; } } fprintf(p3,"Correct: %d (",c); for(int i=0;i<c;i++) { fprintf(p3,"%d",correct_d[i]); if(i!=c-1) fprintf(p3,","); } fprintf(p3,")\n"); fprintf(p3,"Wrong: %d (",w); for(int i=0;i<w;i++) { fprintf(p3,"%d",wrong_d[i]); if(i!=w-1) fprintf(p3,","); } fprintf(p3,")"); fclose(p1); fclose(p2); fclose(p3); }
带参数的主函数:
int main(int argc,char *argv[]){ char *line; int y=1; if(argc<3) //参数没有 4个 { printf("你输错了或 去cmd运行!!!\n"); return 0; } if(strcmp(argv[1],"-n")==0 && strcmp(argv[3],"-r")==0) { n=atoi(argv[2]); //字符串变为整数 r=atoi(argv[4]); srand((unsigned)time(NULL)+rand()); //加个种子,随机数不同 while(n>0) { line=creatFormula(y); if(atoi(line)<0)continue; y++; n--; } } else if(!strcmp(argv[1],"-e") && !strcmp(argv[3],"-a")) { CheckAnswer(argv[2],argv[4]); } return 0; }
六、测试运行
1、测试生成
2、判断答案对错:
10000道题目:
七、项目小结
杨锐龙:
1、对于这次结对项目,可以说的是我是比较懒的,要不是海钊在项目发出来两三天后开始一直催着我,可能后来项目的完成会变得比较紧张,在完成项目的过程中,我们的分歧感觉很大。
2、我们没有规划好谁完成什么功能,可以说都是独立做自己的内容,然后看谁实现的功能更多更好然后决定用谁的方法,这当中也出现了不少的矛盾,因为我两都是独立写的,沟通多但是效果不好,导致有时候他问我怎么办的时候我也没有好的思路给他,或者说根本就不知道他要干嘛(这可能是这次结对项目最遗憾的地方了)。
3、虽说如此,但是也是有好的一点地方,比如在生成式子的那个阶段,他的方法用得比我更好,以至于到后来我完成自己项目的时候,还是引用了他的方法,给我带来了很大的方便,虽然沟通效果不好,但是我们也是有经常分享自己的代码成果,从中也可以get到一些不同于我的思路,比如主函数我开始是没用用参数的,在后面生成和对比答案的时候带来了一定的麻烦,但是想到了他给我的代码中出现过的带参数的主函数,也就顺便解决了出现的问题。
4、这次的结对项目可以遗憾地说是个人项目,是我们(或者我)没有get到结对项目的点上,只是多了一个伙伴相互分享代码,但是值得高兴的是我们都用自己的想法完成了项目,只是花费很多的时间。
黄海钊:
这是我第一次跟别人合作做项目,我觉得合作最大得好处就是可以共享知识,但是也带来了一个很大得问题,就是矛盾。我们有进行讨论,但是在讨论过程中意见不一,各有各的想法,导致讨论没能有太大作用。最终在共享思路的情况下各自完成代码,取其优而选着。虽说完成的不是很好,但也是经历了一个不错的过程。