這是一個明明可以靠顏值吃飯,而偏偏要選擇才華的課設。不,口誤,這是一個明明可以靠Excel表格做出,卻偏偏選擇要用C++實現的課設。開局一張圖,內容全靠編(編碼的編),開始上圖吧~~~
Part 1.數據文件處理
原始數據文件是Earthquake_database1965-2016.txt,由於裏面有些數據存在空缺、字串數不定的問題,咱首先用Excel將其導入,稍作修改。比如對分隔月/日/年以及時:分:秒的符號“/”和“:”進行替換,將它們改爲一個空格。特別地,由於地震類型(Type)存在Earthquake、Nuclear Explosion和Rock Burst三種類型,字串數爲1或2,爲了避免讀取txt文件造成影響,我們統一將Nuclear Explosion替換爲數字0、將Earthquake替換爲數字1、將Rock Burst替換爲數字2,最終在查詢顯示信息的時候,咱根據數字代號輸出它們的中文名:核爆地震、自然地震和巖爆地震。這樣,屬於字符型數據的就是最後四組數據:Magnitude Type(震級類型)、ID(地震的編號)、Source(監測地震數據信息的來源)、Status(監測的地震數據狀態)。我們將數值型數據和字符型數據分別以txt形式導出,並分別命名爲:“Earthquake_database數值數據.txt”和“Earthquake_ database字符數據.txt”。
Part 2.封裝與繼承設計
對題目要求的各種參數以及設計功能定位,咱將其分爲3大類:
1).字符型數據(讀取後僅用於輸出顯示),根據它們所佔最大字符數,直接將其定義爲字符型數組(全局變量):
#include<iostream>
#include<stdbool.h>
#include<math.h>
using namespace std;
const int N=23412;//總數據量
//字符型數據作全局變量
char MagnitudeType[5];//震級類型
char ID[20];//地震的編號
char Source[15];//監測地震數據信息的來源
char Status[10];//監測的地震數據狀態
2).非時間/日期類數值數據,將其封裝成Parameter類:
class Parameter{//數值型參數
public:
float Latitude[N];//緯度
float Longitude[N];//經度
float Depth[N];//地震深度
float Magnitude[N];//震級
int Type[N];//地震產生的類型
};
3).時間/日期類數值數據,將其封裝成DataTime類,因將其內部設計的函數會訪問、調用Parameter類中的數據,因此我們使用公有繼承的方式,將其設定爲Parameter類的派生類:
class DataTime:public Parameter{//日期與時刻
public:
int Day[N];
int Month[N];
int Year[N];
int Second[N];
int Minute[N];
int Hour[N];
int TargetData[N];//在一定日期範圍內的地震行值
int TargetTime[N];//在一定時間範圍內的地震行值
int ScreenData();//篩選在日期範圍內的地震行值
int ScreenTime();//篩選在時間範圍內的地震行值
void Query(bool Choice);//按時間/日期查詢功能
void Sort(int Choice);//對應功能3~5的排序函數
void Statistic(bool Choice);//對應功能6~7的統計函數
private:
int Start[3];
int End[3];
};//統計指定位置/時間範圍的地震監測信息
然後,對DataTime類中的函數進行類外定義:
(1).ScreenTime()函數:用於篩選選定時間範圍的數據。
算法思想:將用戶輸入的起始和終止時/分/秒分別存入Start和End數組,並對起始與終止時間輸入正確性進行判別,一旦出現起始時間比初始時間晚的情況,會輸出”輸入錯誤“的提醒並返回整數1;對輸入正確的情況,判斷已讀取數據時間是否在該範圍,並依次將符合的數據存入數組TargetTime中,返回值爲整數0。
int DataTime::ScreenTime(){//查詢指定時間範圍的地震監測信息
cout<<"時間輸入格式:時/分/秒(用'/'隔開)"<<endl;
cout<<"請輸入起始時間:"<<endl;
scanf("%d/%d/%d",&Start[2],&Start[1],&Start[0]);
cout<<"請輸入終止時間:"<<endl;
scanf("%d/%d/%d",&End[2],&End[1],&End[0]);
int Min,Max;
Min=Start[2]*3600+Start[1]*60+Start[0];
Max=End[2]*3600+End[1]*60+End[0];
if(Min>Max){cout<<"輸入錯誤"<<endl;return 1;}
else{
for(int i=0,j=0;i<N;i++)
if((Hour[i]*3600+Minute[i]*60+Second[i]>=Min)&&
(Hour[i]*3600+Minute[i]*60+Second[i]<=Max)){
TargetTime[j]=i;j++;
}
return 0;
}
}
(2).ScreenData()函數:用於篩選選定日期範圍的數據。
算法思想:類似ScreenTime()函數,將用戶輸入的起始和終止月/日/年分別存入Start和End數組,並對起始與終止日期輸入正確性進行判別,一旦出現起始時間比初始時間晚的情況,會輸出”輸入錯誤“的提醒並返回整數1;對輸入正確的情況,判斷已讀取數據時間是否在該範圍,並依次將符合的數據存入數組TargetData中,返回值爲整數0。
int DataTime::ScreenData(){//查詢指定日期範圍的地震監測信息
cout<<"日期輸入格式:月/日/年(用'/'隔開)"<<endl;
cout<<"請輸入起始日期:"<<endl;
scanf("%d/%d/%d",&Start[1],&Start[0],&Start[2]);
cout<<"請輸入終止日期:"<<endl;
scanf("%d/%d/%d",&End[1],&End[0],&End[2]);
bool s=true;
if(Start[2]>End[2])s=false;//防錯系統
else if(End[2]==Start[2]){
if(Start[1]>End[1])s=false;
else if(Start[1]==End[1]&&Start[0]>End[0])s=false;
}
if(s){//防錯系統
int j=0;
for(int i=0;i<N;i++){
int m=0;
if(Year[i]<End[2])m+=1;
else if(Year[i]==End[2]){
if(Month[i]<End[1])m+=1;
else if(Month[i]==End[1]&&Day[i]<=End[0])m+=1;
}
if(m==1){
if(Year[i]>Start[2])m+=1;
else if(Year[i]==Start[2]){
if(Month[i]>Start[1])m+=1;
else if(Month[i]==Start[1]&&Day[i]>=Start[0])m+=1;
}
}
if(m==2){TargetData[j]=i;j++;}
}
for(int i=j;i<N;i++)TargetData[i]=0;
return 0;
}
else{cout<<"輸入錯誤"<<endl;return 1;}
}
值得注意的是,ScreenData()與ScreenTime()對數據是否在用戶輸入界限範圍的判斷方式有所不同:一天=24小時=1440分鐘=86400秒,ScreenTime()的判斷方式就是將時/分/秒轉化爲秒制,這樣誰大誰小就很直觀啦;然鵝,年有閏年、平年之分,每個月的天數也無法達成統一標準,那分情況討論,結果會是:1.數據中的年份是大於、等於、還是小於起始年份?小於的情況直接排除,大於的情況可以直接進入與終止年份的對比判斷,等於的情況還要看月份是否大於或等於起始月份?等於起始月份的話還要判斷天數是否大於起始日數。然後就算2.數據中的年份是大於、等於、還是小於終止年份……以此類推,這麼多種情況,一個個寫成if-else語句豈不是要暈了?沒關係,我們可以換一種方式去判斷,無需將每種情況一個個列出,判斷思想如下:A.判斷年份是否滿足小於等於終止年份的條件,如果小於,初始值爲m的變量自增1;否則,如果等於,就判斷是否滿足小於終止月份或者等於終止月份並且天數小於終止天數的情況,這兩種並列情況滿足一種m的變量自增1。在此,所有符合條件的情況都會使m=1,不符合條件的情況都無法改變m的原值0。同理,B.年/月/日如果滿足起始條件的話,m又會自增1。最終篩選出同時滿足A和B條件的情況,即m=2的情況。
PS過渡段:剛纔講解的兩個函數都是int型函數,防錯系統會將它們的正確與否情況以0和1的形式反饋。接下來講解的是功能函數,它們會在開始檢驗ScreenData()/ScreenTime()的值是否爲0,如果是1,則不執行其功能,系統會直接返回用戶操作界面;等於0則繼續執行對應功能。
(3).查詢函數Query(bool Choice):題目設定的查詢範圍有時間與日期之分,Query函數形參Choice是C++語言特有的bool類型,當形參傳值爲true/非0整數時,整型數組Array會複製TargetData篩選出來的每個數據行值;當形參傳值爲false/0時,整型數組Array會複製TargetTime篩選出來的每個數據行值。然後依次檢索個各行數據的值是否等於Array,由於數組TargetData和TargetTime存儲的行值均以從小到大的順序排列,因此按從小到大的順序依次檢索比對不會遺漏行值。對於符合的數據,不僅將已存入的數值數據輸出,還要加上之前未錄入的字符數據,打開文件,按數據行值定位並輸出字符,接着關閉文件,循環執行此步驟,輸出所有符合時間/日期範圍的數據。
void DataTime::Query(bool Choice){
int Array[N],s=0;
if(Choice==true){if(ScreenData()==1)s=1;memcpy(Array,TargetData,sizeof(int)*N);}
else{if(ScreenTime()==1)s=1;memcpy(Array,TargetTime,sizeof(int)*N);}
if(!s){
for(int i=0,j=0;i<N;i++){
if(i==Array[j]){
FILE *fp2;
fp2=fopen("Earthquake_database字符數據.txt","r");
for(int k=0;k<=i;k++)fscanf(fp2,"%s%s%s%s\n",&MagnitudeType,&ID,&Source,&Status);
fclose(fp2);
cout<<Month[i]<<"/"<<Day[i]<<"/"<<Year[i]<<" ";
cout<<Hour[i]<<":"<<Minute[i]<<":"<<Second[i]<<" ";
cout<<Latitude[i]<<" "<<Longitude[i]<<" ";
if(!Type[i])cout<<"核爆地震 ";
else if(Type[i]==1)cout<<"自然地震 ";
else cout<<"巖爆地震 ";
cout<<Depth[i]<<"km "<<Magnitude[i]<<"級 ";
cout<<MagnitudeType<<" "<<ID<<" "<<Source<<" "<<Status<<endl;
j++;
}
}
}
}
(4).排序函數Sort(int Choice):依照用戶選擇的排序對象,形參Choice有3種返回值,分別爲:路徑A.Choice=0->輸入日期和經緯度範圍->路徑1/路徑2:1-選擇排序緯度->Choice=1,2-選擇排序經度->Choice=2;路徑B.Choice=3->輸入日期和經緯度範圍->排序地震震級;路徑C.Choice=4->輸入日期和經緯度範圍->排序地震深度。然後基於switch語句,將選擇對象複製到數組Array中,使用冒泡排序法確定對象的順序,依次按照從小到大排序而得到的行值存入數組Temp中,並藉助for循環依次輸出Temp各行值所對應的數據。此函數除了防錯功能外,還給用戶提供了是否選擇經緯度範圍的功能,減少了無需選擇經緯度範圍的用戶不必要的操作。
void DataTime::Sort(int Choice){
if(ScreenData()==0){
cout<<"是否選擇經緯度範圍?是,輸入1;否,輸入0."<<endl;
float Min[2],Max[2];
bool A;
scanf("%d",&A);
if(A){
cout<<"經/緯度範圍輸入格式:下限/上限(用'/'隔開)"<<endl;
cout<<"請輸入經度範圍:(-180~180)"<<endl;
scanf("%f/%f",&Min[0],&Max[0]);
cout<<"請輸入緯度範圍:(-90~90)"<<endl;
scanf("%f/%f",&Min[1],&Max[1]);
}
else{
Min[0]=Min[1]=0.0;
Max[0]=180.0;Max[1]=90.0;
}
if(Choice<3){
cout<<"請選擇排序對象:緯度排序,輸入1;經度排序,輸入2"<<endl;
scanf("%d",&Choice);
}
int Array[N],Temp[N],n;
FILE *fp3;
fp3=fopen("排序功能.txt","a");
fprintf(fp3,"排序法則:小->大 排序對象:");
switch(Choice){//選擇排序對象
case 1:memcpy(Array,Latitude,sizeof(int)*N);fprintf(fp3,"緯度Latitude");break;
case 2:memcpy(Array,Longitude,sizeof(int)*N);fprintf(fp3,"經度Longitude");break;
case 3:memcpy(Array,Magnitude,sizeof(int)*N);fprintf(fp3,"震級Magnitude");break;
case 4:memcpy(Array,Depth,sizeof(int)*N);fprintf(fp3,"深度Depth");break;
}
fprintf(fp3,"\n日期範圍:%d/%d/%d~%d/%d/%d 經度範圍:%f~%f 緯度範圍:%f~%f\n",
Start[1],Start[0],Start[2],End[2],End[1],End[0],Min[0],Max[0],Min[1],Max[1]);
memcpy(Temp,TargetData,sizeof(int)*N);
for(int i=0,j=0;i<N;i++){
if(TargetData[i])j++;
else{n=j-1;break;}
}
for(int i=0;i<n;i++)
for(int j=0;j<n-i;j++)
if(Array[Temp[j]]>Array[Temp[j+1]]){
int t=Temp[j];
Temp[j]=Temp[j+1];
Temp[j+1]=t;
}
for(int i=0,s=0;i<n;i++){
if(Longitude[Temp[i]]<=Max[0]&&Longitude[Temp[i]]>=Min[0]
&&Latitude[Temp[i]]<=Max[1]&&Latitude[Temp[i]]>=Min[1]){
for(int j=0;j<n;j++)if(Temp[i]==TargetData[j])s++;
if(s){s=0;
cout<<Month[Temp[i]]<<"/"<<Day[Temp[i]]<<"/"<<Year[Temp[i]]<<" ";
cout<<Hour[Temp[i]]<<":"<<Minute[Temp[i]]<<":"<<Second[Temp[i]]<<" ";
cout<<Latitude[Temp[i]]<<" "<<Longitude[Temp[i]]<<" ";
fprintf(fp3,"%d/%d/%d %d:%d:%d %f %f ",Month[Temp[i]],Day[Temp[i]],
Year[Temp[i]],Hour[Temp[i]],Minute[Temp[i]],Second[Temp[i]],
Latitude[Temp[i]],Longitude[Temp[i]]);
if(!Type[Temp[i]]){cout<<"核爆地震 ";fprintf(fp3,"核爆地震 ");}
else if(Type[Temp[i]]==1){cout<<"自然地震 ";fprintf(fp3,"自然地震 ");}
else{cout<<"巖爆地震 ";fprintf(fp3,"巖爆地震 "); }
cout<<Depth[Temp[i]]<<"km "<<Magnitude[Temp[i]]<<"級 ";
fprintf(fp3,"%fkm %f級 ",Depth[Temp[i]],Magnitude[Temp[i]]);
FILE *fp2;
fp2=fopen("Earthquake_database字符數據.txt","r");
for(int x=0;x<=Temp[i];x++)fscanf(fp2,"%s%s%s%s",&MagnitudeType,&ID,&Source,&Status);
cout<<MagnitudeType<<" "<<ID<<" "<<Source<<" "<<Status<<endl;
fprintf(fp3,"%s %s %s %s\n",MagnitudeType,ID,Source,Status);
fclose(fp2);
}
}
}
fclose(fp3);
}
}
(5).這是最後一項功能,也是最簡單的功能——統計。Statistic(bool Choice)函數中,當形參傳值爲true/非0整數時,對選定經緯度和時間範圍的數據進行篩選;當形參傳值爲false/0時,對選定時間範圍的核爆地震類型數據進行篩選。
void DataTime::Statistic(bool Choice){
if(Choice)ScreenTime();//輸入時間範圍
int sum=0;
float Min[2],Max[2];//輸入經/緯度範圍
cout<<"經/緯度範圍輸入格式:下限/上限(用'/'隔開)"<<endl;
cout<<"請輸入經度範圍(-180~180):"<<endl;
scanf("%f/%f",&Min[0],&Max[0]);
cout<<"請輸入緯度範圍(-90~90):"<<endl;
scanf("%f/%f",&Min[1],&Max[1]);
//防錯系統
if(Min[1]>Max[1]||Min[0]>Min[0]||fabs(Max[1])>90||fabs(Max[0])>180)cout<<"輸入錯誤"<<endl;
else{for(int i=0,j=0;i<N;i++){
if(Choice==true){
if(i==TargetTime[j]){j++;
if(Longitude[i]<Max[0]&&Longitude[i]>Min[0]
&&Latitude[i]<Max[1]&&Latitude[i]>Min[1])sum++;
}
}
else if(Type[i]==0){
if(Longitude[i]<Max[0]&&Longitude[i]>Min[0]
&&Latitude[i]<Max[1]&&Latitude[i]>Min[1])sum++;
}
}
if(Choice==true)cout<<"在此範圍內的地震次數爲:"<<sum<<endl;
else cout<<"在此範圍內的核爆地震次數爲:"<<sum<<endl;
}
}
Part 3.主函數
在主要功能被封裝後,主函數的任務就精簡得多了:打開並讀取、存儲裝有數值數據的txt文件->基於switch框架對用戶選擇的功能進行函數調用->用戶退出系統->程序結束。
int main(){
DataTime DataTime;//實例化DataTime類
Parameter Parameter;//實例化Parameter類
FILE *fp1;
fp1=fopen("Earthquake_database數值數據.txt","r");
for(int i=0;i<N;i++){
fscanf(fp1,"%d%d%d%d%d%d%f%f%d%f%f",&DataTime.Month[i],&DataTime.Day[i],&DataTime.Year[i],
&DataTime.Hour[i],&DataTime.Minute[i],&DataTime.Second[i],&DataTime.Latitude[i],
&DataTime.Longitude[i],&DataTime.Type[i],&DataTime.Depth[i],
&DataTime.Magnitude[i]);
}
fclose(fp1);
while(1){
cout<<"歡迎使用Tracer地震監測系統,請選擇系統功能:"<<endl;
cout<<"1.查詢指定時間段地震監測數據,輸入1;2.查詢指定日期範圍地震監測信息,輸入2;"<<endl;
cout<<"3.按照地震發生的經/緯度排序,輸入3;4.按照地震的震級強度進行排序,輸入4;"<<endl;
cout<<"5.按照地震發生的深度進行排序,輸入5;6.統計一定範圍/時間內地震發生的次數,輸入6;"<<endl;
cout<<"7.在一定範圍內對因核爆發生的地震進行統計,輸入7;退出系統,輸入8."<<endl;
cout<<"------------------------------------------------------------------------------"<<endl;
int function,s=0;
scanf("%d",&function);
switch(function){
case 1:DataTime.Query(0);break;
case 2:DataTime.Query(1);break;
case 3:DataTime.Sort(0);break;
case 4:DataTime.Sort(3);break;
case 5:DataTime.Sort(4);break;
case 6:DataTime.Statistic(1);break;
case 7:DataTime.Statistic(0);break;
case 8:s=1;break;
}
cout<<endl;
if(s==1){cout<<"歡迎下次使用Tracer地震監測系統"<<endl;
break;
}
}
return 0;
}
Part 4.系統測試與拓展
(1).操作界面
(2).各項功能
如果輸入錯誤,您還會受到溫馨提醒喲~
最後,如果不想繼續使用系統,可以直接退出~
生成文件情況:
(3).拓展部分:如果你有世界地圖,可以將這些數據點標繪在地圖上,從而可以得出哪個位置核爆最多。
將經緯度數據導入Excel表格並做出數據分佈座標圖,套上半透明的世界地圖(最好要標有明確的經緯度參考點):
最後,感謝耐心觀看,如有錯誤,歡迎指正QAQ.