寫在前面:
coding.net代碼地址: https://git.coding.net/ForeverSevrous/PersonalProject.git
測試效果見src外生成的result.txt
需求分析:
- 接收一個參數n,然后隨機產生n道四則運算練習題。
- 每個數字在0和100之間,運算符3個到5個之間。
- 每個練習題至少包含2種運算符。
- 題中不可以出現負數和非整數。
- 支持有括號的運算式,算式中存在的括號必須大於2個,且不得超過運算符的個數。
- 支持真分數的出題與運算,只涵蓋加減法。
- 將學號和生成的練習題及對應答案輸出到文件“result.txt”中,不輸出額外信息。
功能設計:
基本功能:
生成符合題目要求的四則運算題目並輸出到文件中。
擴展功能:
隨機出現含有正確格式的括號的式子。
生成使用真分數運算的式子。
設計實現:
在實現過程中,我只新建了一個Main類,其他各塊功能均由Main類的內部方法實現。
main函數:
主函數,包含接收參數n,判斷參數n的合法性,調用函數產生四則運算式,計算運算式結果並輸出到文件中的功能。
newE函數:
生成一個符合要求的四則運算式(含有括號)。
Fraction函數:
生成一個符合要求的使用真分數進行加減的四則運算式。
divideExactly函數:
在生成運算式用到“÷”時,判斷此時的計算是否能整除,如果不能整除,則返回一個新的可以整除的整數。
calculate函數:
在生成所有四則運算式后,計算所有運算式的結果,並將結果分別附加到每一個四則運算式的尾部。 在計算過程中如果發現某個
式子結果為負數則重新生成一個四則運算式來保證式子的非負性。
write函數:
將calculate類生成的結果寫入到已有的文件“result.txt”中。
算法詳解:
生成題目:
在生成題目過程中,我先通過隨機數確定運算符的個數(3-5個),然后通過循環取隨機數確定參與運算的數字(比運算符多一個),實現如下:
1 Random rd = new Random(); 2 //產生運算符的個數,3-5個 3 int on=rd.nextInt(3)+3; 4 //產生比運算符多一個的隨機數字 5 int[] nums = new int[on+1]; 6 for(int j=0;j<=on;j++){ 7 nums[j]=rd.nextInt(101); 8 }
之后解決括號相關問題,具體內容見下文【比較滿意的代碼片段--片段三】
生成真分數運算式的具體內容見下文【比較滿意的代碼片段--片段四】
計算題目:
在求解題目過程中,為了處理運算符的優先級問題,我使用了Java中的ScriptEngine接口和ScriptEngineManager類,具體代碼如下:
1 ScriptEngine se = new ScriptEngineManager().getEngineByName("JavaScript");
使用這個對象需要導入以下三個包:
1 import javax.script.ScriptEngine; 2 import javax.script.ScriptEngineManager; 3 import javax.script.ScriptException;
而且使用這個對象時必須使用異常捕獲,否則會報錯。使用這個對象的.eval(String),就可以很方便地得出腳本String的值。
測試運行:
控制台輸出如下(eclipse):
文件內容如下:
[雖然控制台模式下兩種式子分開輸出,但是文件中是混合到一起的]
命令行輸出如下:
[在這里需要提示一點,使用eclipse時在控制台獲取數據一般使用Scanner in = new Scanner(System.in),但是這種方式在命令行模式下無法使用Java Main 20這種句子獲取數據“20”,而是必須在第一次回車后才能獲取參數。所以為了使用Java Main 20作為命令,應使用n=Integer.parseInt(args[0]);來獲取輸入的參數。]
實現了加括號的功能,我讓我的題按照1/3的幾率出現帶有括號的題目,在所有帶有括號的題目里隨機出現一對括號或者兩對括號,雖然題目要求中有“算式中存在的括號必須大於2個”,但又有“且不得超過運算符的個數”,所以考慮到這個,我在所有三個運算符的式子里都不出現兩對即四個括號,只在四個和五個運算符的式子里出現四個括號。
實現了分數運算功能,分數題約占總題數的1/4。
比較滿意的代碼片段:
片段一:
在本次作業中,老師要求使用“÷”來作為輸出時的除號,但計算機在計算結果時使用的是“/”來作為除號,所以為了正確的計算出結果,我將生成的運算式分成兩種,一種除號使用“÷”,用做輸出,另一種除號使用“/”,用作計算,分別放在兩個ArryList中。
1 ArrayList<String> ex1=new ArrayList<String>();//用於顯示,使用÷ 2 ArrayList<String> ex2=new ArrayList<String>();//用於計算,使用/
片段二:
為了使所出的題有更好的鍛煉效果,我添加一小段代碼使得相鄰的兩個運算符不會是同一種。這樣使我出的題更多樣化,更能鍛煉人的計算能力。
1 int[] os=new int[2]; 2 os[j%2]=rd.nextInt(4);//隨機選擇一個運算符 3 int o;//本次添加的運算符 4 if(j==0){ 5 o=os[0]; 6 } 7 else{ 8 while(os[j%2]==os[(j+1)%2]){ 9 os[j%2]=rd.nextInt(4); 10 } 11 o=os[j%2]; 12 }
片段三:
對於加入括號這一部分功能,我是這樣做的:
我發現左括號一定在數字的左邊,右括號一定在數字的右邊,這就給我的括號插入帶來了便利。我將情況分為有3個運算符,有4個運算符,有5個運算符共三種情況,通過switch...case...語句分別對每種情況進行處理,得到括號對應的位置,記錄到數組中。當出現括號的配對錯誤或者無意義時,更新導致問題出現的兩個位置,直到完全正常。
我通過隨機數的大小范圍[rd.nextInt(10)<3]來規定出現帶括號的式子的概率,增加題目的多樣性。
1 if(rd.nextInt(10)<3){ 2 have=true; 3 switch(on){ 4 case 3: 5 position=new int[1][2]; 6 position[0][0]=rd.nextInt(3)+1; 7 if(position[0][0]==3){ 8 position[0][1]=100; 9 }else{ 10 if(rd.nextInt(10)<5){ 11 position[0][1]=position[0][0]+1; 12 }else{ 13 position[0][1]=position[0][0]+2; 14 } 15 }break; 16 case 4: 17 …… 18 break; 19 case 5: 20 …… 21 break; 22 } 23 }
[此部分代碼過長,故僅展示有三個運算符的情形]
在生成括號的位置之后,在組合數字和運算符的過程中找准時機進行插入。
[此部分代碼較長,故僅展示有一對括號的情形]
1 if(position.length==1){ 2 if(position[0][0]==(j+1)){ 3 ex_1+=String.valueOf(brackets[0])+nums[j]; 4 ex_2+=String.valueOf(brackets[0])+nums[j]; 5 flag++;//未配對的左括號的個數 6 }else if(position[0][1]==(j+1)){ 7 ex_1+=nums[j]+String.valueOf(brackets[1]); 8 ex_2+=nums[j]+String.valueOf(brackets[1]); 9 flag--;添加一個右括號后未配對左括號個數減一 10 } 11 else{ 12 ex_1+=nums[j]; 13 ex_2+=nums[j]; 14 } 15 }else{ 16 …… 17 }
【在連接符號的時候必須使用String.valueOf(),不然無法連接。】
片段四:
對於分數出題這里,我是這樣做的:
建一個二維數組存放分子和分母,在隨機出這些數時,每當出完一對分子和分母后,就進行化簡運算,代碼如下:
1 int k=nums[i][0]; 2 while(k>1){//化簡 3 if((nums[i][1]%k==0)&&(nums[i][0]%k==0)){ 4 nums[i][1]/=k; 5 nums[i][0]/=k; 6 if(nums[i][0]<k){ 7 k=nums[i][0]; 8 continue; 9 } 10 } 11 k--; 12 }
由於在分數運算這里只存在加減運算,所以不必考慮優先級的問題,一個一個數字挨着算下來即可。每次我都先連接運算式,然后計算這一步的結果,存到最后一個運算過的數字的位置,然后循環直到所有的數字全部運算完畢,最后將結果連接到尾部,返回這個運算式字符串。在主函數中我將這些分數運算式統一存到一個ArrayList中,當做參數傳入write方法中,由write方法輸出到文件中。
軟件設計的模塊化原則:
我將我的程序分為六個部分,分別完成六種功能。除了主函數,我寫了五個函數來完成這個程序中的五大塊功能,分別是:
- 生成一個符合標准的四則運算式
- 檢查數字是否可以整除,如不能則不斷改變,直到獲得可以整除的數字。
- 將所有的四則運算式計算出結果,並在計算過程中將不符合條件的式子進行替換。
- 生成一個使用真分數運算的式子。
- 將整合完畢的所有式子和我的學號輸出到文件中。
這樣完成這個需求所需要的幾個大的步驟都被我分開在幾個模塊里了。將代碼按照不同的功能分開后,想要修改哪一個部分的代碼只需要在相應的函數中修改即可,就不用像沒有分開時那樣在一大段代碼中尋找了。
在完成項目的過程中,我本來將生成運算式的代碼放到了主函數中,但是在之后優化時,我發現這樣寫代碼的話就很不靈活,如果想要在別的地方也得到一個符合要求的四則運算式就變得很繁瑣,於是我就將這一部分代碼也拿出來做成了單獨的一個函數。
經過這樣的修改我徹底將這段程序變成了幾個模塊,在一定程度上讓我的程序有了更好的獨立性,穩定性和可移植性。
PSP:
PSP2.1 |
任務內容 |
計划共完成需要的時間(min) |
實際完成需要的時間(min) |
Planning |
計划 |
10 |
8 |
· Estimate |
· 估計這個任務需要多少時間,並規划大致工作步驟 |
10 |
15 |
Development |
開發 |
423 |
665 |
· Analysis |
· 需求分析 (包括學習新技術) |
10 |
20 |
· Design Spec |
· 生成設計文檔 |
10 |
10 |
· Design Review |
· 設計復審 (和同事審核設計文檔) |
5 |
5 |
· Coding Standard |
· 代碼規范 (為目前的開發制定合適的規范) |
3 |
5 |
· Design |
· 具體設計 |
10 |
10 |
· Coding |
· 具體編碼 |
360 |
480 |
· Code Review |
· 代碼復審 |
10 |
15 |
· Test |
· 測試(自我測試,修改代碼,提交修改) |
15 |
120 |
Reporting |
報告 |
15 |
23 |
· Test Report |
· 測試報告 |
6 |
10 |
· Size Measurement |
· 計算工作量 |
3 |
3 |
· Postmortem & Process Improvement Plan |
· 事后總結, 並提出過程改進計划 |
6 |
10 |
在整個個人項目的完成過程中,我在具體編碼的這個方面花費的時間最多,這主要是因為我對於編寫程序還不是很熟悉,這歸咎於我平時理論看得多,實踐做得少,所以突然要開始上手具體編寫程序時就有一些懵了,雖然知道有這些方法可以使用,但是在具體使用的時候還是會出現這樣那樣的錯誤;更有一些只是知道可以這樣做,具體應該怎樣做還要隨時檢索,這就大大拖慢了我的速度。經過這一次的個人項目,讓我認識到了我在實踐方面的不足,也激勵我在今后的學習中多動手,多實踐。
在測試階段我需要的時間估計和實踐相差巨大。我本來以為在這個階段就只需要用不同方法多運行幾次,測試一下即可,可是我漏了我的代碼會出現bug這種情形,所以為了將代碼的功能調試到在所有情況下都正常,我花費了大量時間使用Debug模式尋找,修改錯誤。