結對項目:四則運算表達式生成器(C語言)
GitHub:https://github.com/peter-ye-code/Question-Builder
合作者:葉學濤(3118005024)
溫德華(3118005021)
一、需求
四則運算表達式生成器的全部功能:
-
使用 -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文件。
-
在生成題目的同時,計算出所有題目的答案,並存入執行程序的當前目錄下的Answers.txt文件。
-
程序應能支持一萬道題目的生成。
-
程序支持對給定的題目文件和答案文件,判定答案中的對錯並進行數量統計,輸入參數如下:Myapp.exe -e .txt -a .txt,統計結果輸出到文件Grade.txt,格式如下:
Correct: 5 (1, 3, 5, 7, 9)
Wrong: 5 (2, 4, 6, 8, 10)
其中“:”后面的數字5表示對/錯的題目的數量,括號內的是對/錯題目的編號。為簡單起見,假設輸入的題目都是按照順序編號的符合規范的題目。
未完成的功能:(4)功能中的真分數功能未能實現
二、PSP表格
PSP2.1 |
Personal Software Process Stages |
預估耗時(分鍾) |
實際耗時(分鍾) |
Planning |
計划 |
30 |
40 |
· Estimate |
· 估計這個任務需要多少時間 |
30 |
40 |
Development |
開發 |
1150 |
1200 |
· Analysis |
· 需求分析 (包括學習新技術) |
200 |
250 |
· Design Spec |
· 生成設計文檔 |
30 |
30 |
· Design Review |
· 設計復審 (和同事審核設計文檔) |
10 |
15 |
· Coding Standard |
· 代碼規范 (為目前的開發制定合適的規范) |
20 |
30 |
· Design |
· 具體設計 |
500 |
435 |
· Code Review |
· 代碼復審 |
120 |
90 |
· Test |
· 測試(自我測試,修改代碼,提交修改) |
300 |
350 |
Reporting |
報告 |
60 |
70 |
· Test Report |
· 測試報告 |
20 |
25 |
· Size Measurement |
· 計算工作量 |
10 |
10 |
· Postmortem & Process Improvement Plan |
· 事后總結, 並提出過程改進計划 |
30 |
35 |
合計 |
1240 |
1310 |
三、設計實現過程
1、設計思路
采用隨機法生成運算表達式,運算順序則采用中綴表達式轉后綴表達式,結果及運算數則使用棧來進行存取。
2、模塊函數關系
四、主要代碼
生成表達式
int CreatQuestion (int m,int digital_index,int *answer) { char str[250] = {'0'}; //定義一個字符數組存放算術表達式 int x, num, ran_num, j = 0; num = random_number( 3 );//生成隨機運算符數目 //循環生成算術表達式 for ( int k = 0; k < num; k++ ) { ran_num = random_number ( m ); //生成一個參與表達式的數值,m是數值范圍 (1~10) x = 1; while ( ran_num / x ) { x *= 10; } x /= 10;//x=10,x=1 while ( x )//x=10執行兩次 { str[j++] = ran_num / x + '0';//將數字型轉換為字符串型 ran_num = ran_num % x; x /= 10; } str[j++] = random_symbol(); //隨機生成一個運算符 } //生成一個隨機結尾數值 ran_num = random_number( m ); x = 1; while ( ran_num / x ) { x *= 10; } x /= 10; while ( x ) { str[j++] = ran_num / x + '0'; ran_num = ran_num % x; x /= 10; } int result = Answer(str,digital_index); *answer = result; if(result>=0){ WriteQuestion(str,digital_index); WriteAnswer(digital_index,result); } }
計算答案
//生成答案模塊 int Answer(char *str,int digital_index) { char* p =str; int num[100] = {0};//數字數組 char symbol[100] = {0};//運算符數組 int index = 0; //索引 char stack[100] = {0};//運算符棧 int stacki[100] = {0};//結果棧 int top = -1;//棧的索引 -1表示棧為空 int temp = 0;//正在拼寫的數字 int flag = 0;//表示當前是否正在拼寫數字 //中綴表達式轉化為逆波蘭表達式 //最后的結果就是得到兩個數組,分別是num和symbol while(1){ if(*p>='0' && *p<='9')//讀到了一個數字 { flag = 1; temp *= 10;//第一個為數字時temp=0,第二個為數字時才乘以10 temp += *p - '0';//0的ASCII值為48 } else//讀到了一個符號,或字符串已經結束了 { if(flag)//拼寫完1個數字先將數字輸出 { num[index] = temp; symbol[index] = '!'; index++; flag = temp =0;//重新置0 } if(!*p)//如果字符串已經結束 { //最終,將棧元素全部出棧 while(top>=0) symbol[index++]=stack[top--]; break;//跳出循環 } else// 字符串還未結束 { if(top == -1||*p == '(') //如果棧空時,或符號為左括號 stack[++top] = *p;//入棧 else if(*p == ')')//如果是右括號,出棧到左括號 { while(top>=0&&stack[top]!='(')//如果不為左括號。出棧 { symbol[index++]=stack[top--]; } --top;//如果為左括號,--top } else if(*p == '*'||*p == '/')//如果新符號是乘除 { while(top>=0&&(stack[top]=='*'||stack[top]=='/'))//乘除出棧 { symbol[index++]=stack[top--]; } stack[++top] = *p;//新符號入棧 } else //如果新運算符是加減 { while(top>=0&&stack[top]!='(')//四則運算出棧 { symbol[index++]=stack[top--]; } stack[++top] = *p;//新符號入棧 } } } p++; } //逆波蘭表達式求解 top =-1; int temp1 = 0; int temp2 = 0; for(int i =0;i<index;i++){ if(symbol[i]=='!'){//這時的i索引表示的是數字,數字入棧stacki stacki[++top] = num[i]; } //符號運算 else { temp1 = stacki[top--]; temp2 = stacki[top--]; switch (symbol[i]) { case '+': stacki[++top] = temp2 + temp1;//將結果入棧 break; case '-': stacki[++top] = temp2 - temp1;//后面減去前面 break; case '*': stacki[++top] = temp2 * temp1; break; case '/': stacki[++top] = temp2 / temp1; break; } } } //最終結果是stacki[0] int result = stacki[0]; // printf("%s=%d\n",str,result); // WriteAnswer(digital_index,result); return result; }
生成答案文件
void WriteQuestion(char *str,int digital_index){ int k=0; FILE *fp; fp=fopen("Exercises.txt","a");//若文件不存在則建立該文件,這里不能用w,要用a fprintf(fp,"%d:",digital_index); while(str[k]!=NULL){ if(str[k] == '+' ||str[k] == '-' ||str[k] == '*' ||str[k] == '/' || str[k] == '='){ fprintf(fp," %c ",str[k]); }else fprintf(fp,"%c",str[k]); k++; } fprintf(fp," =\n"); fclose(fp); }
生成題目文件
void WriteAnswer(int digital_index,int result){ FILE *fp; fp=fopen("Answers.txt","a");//若文件不存在則建立該文件,這里不能用w,要用a fprintf(fp,"%d:",digital_index); fprintf(fp,"%d\n",result); fclose(fp); }
檢查答案並且生成報告文件
void CheckAnswer(char exercisefile[],char answerfile[]){ FILE *fp1,*fp2,*fp3; fp1=fopen("Answers.txt","r"); fp2=fopen(answerfile,"r"); fp3=fopen("Grade.txt","w"); int index=1; //題目序號 char correct_answer[30]={}; //存放正確答案 char answer[30]={}; //存放從答案文件取出的答案 int c_num=0,w_num=0; //用於計算對和錯的總題數 int c_index[10000]={}; //用於儲存對的和錯的題號 int w_index[10000]={}; while(fgets(correct_answer,30,fp1)!=NULL && fgets(answer,30,fp2)!=NULL) { if(strcmp(correct_answer,answer)==0) //比較前n個字節的大小 { c_index[c_num++]=index; index++; } else if(strcmp(correct_answer,answer)!=0) { w_index[w_num++]=index; index++; } } fprintf(fp3,"Correct: %d (",c_num); for(int i=0;i<c_num;i++) { fprintf(fp3,"%d",c_index[i]); if(i!=c_num-1) fprintf(fp3,","); } fprintf(fp3,")\n"); fprintf(fp3,"Wrong: %d (",w_num); for(int i=0;i<w_num;i++) { fprintf(fp3,"%d",w_index[i]); if(i!=w_num-1) fprintf(fp3,","); } fprintf(fp3,")\n"); fclose(fp1); fclose(fp2); fclose(fp3); }
主函數
int main(int argc,char*argv[]){ if(argc<2){ printf("you must input argc!"); return 0; } srand((int)time(0));//設置rand()產生隨機數時的隨機數種子 FILE *fp1,*fp2; int n,r; if(!strcmp(argv[1],"-n") && !strcmp(argv[3],"-r")){//生成題目和答案 fp1=fopen("Exercises.txt","w"); fp2=fopen("Answers.txt","w"); fclose(fp1); fclose(fp2); n=atoi(argv[2]); r=atoi(argv[4]); int answer; int digital_index=1; while(digital_index<=n){ CreatQuestion(r,digital_index,&answer); if(answer>=0){ digital_index++; } } }else if(!strcmp(argv[1],"-e") && !strcmp(argv[3],"-a")){//檢查答案 CheckAnswer(argv[2],argv[4]); } return 0; }
五、測試運行
1、生成10道題目及答案文件
2、生成1W道題目及答案文件
3、檢查文件的生成
六、經驗及總結
- 葉學濤:
- PSP表的實際花費時間和預估耗時差不多,這次任務的話花在學習新技術的時間上會比較多一些,主要學習的是調場度算法和逆波蘭轉換式。
- 因為前期對需求分析不夠,導致真分數這一部分的功能未能實現,這是比較遺憾的一點。
- 在進行答案檢查這一部分功能的實現,花的時間比較多,本來想的方法是比較復雜的,因此花費了不少時間在思考上,后來換了一種方法,也能達到目標,但代碼量明顯減少了。
- 結對的感受:結對的感受就是能互相發現問題,使開發效率更高,但有一點就是因為在家里的原因,無法准確地溝通,這一點導致了需求分析沒有做好,有一部分功能未能實現。
- 溫德華:
- 這次的結對項目中,大致了解了中綴表達式轉后綴表達式以及對於多個模塊函數的嵌套調用,也學會了棧的更深層次的使用。
- 真分數方面是困擾着我的難題,知道項目結束仍然沒能解決,沒能很完全地實現所給功能。
- 同時也明白了自身的諸多不足,對於設計整個程序的總體模塊分布及大致框架比較模糊,無法將多個函數巧妙地結合起來實現。
- 這次的結對項目,可以說大部分是同伴完成的,我自身卻一直停留在了真分數的困擾中,如果沒有他獨挑大梁,我怕是完成不了這次的項目。