///isleap.c / isleap.cpp -- for HWK# @Week14 -- by tsaiwn@csie.nctu.edu.tw
///// 這也故意寫成當作 .c 和 .cpp 都沒問題 !
/// 切換到不同國家模式, 測試 1700 年, 1800, 1900 是不是閏年 ?
/// Q -- 結束程式; A -- 美國; T 或 C -- 台灣/中國; J -- 日本; I -- 義大利/羅馬
/// 切換 Country Code 後會記住何時 Switch from Julian calendar to Gregorian calendar
/// 可以輸入一個數 表示 年; 或兩個數表示 月 年; 或三個數 日 月 年
///再次提醒, 各個國家從 Julian calendar 切換到 Gregorian calendar 的日期不同, 可用 ncal -p 查看。
/// ncal -p 是Unix/Linux的命令: https://nxmnpg.lemoda.net/1/ncal
/// 目前isleap.c只會根據記住的 Country Code(四種切換)設定的 adjustYear 測試是否 Year 為閏年 ?
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#define DEBUGPP
typedef enum {
NONE=0, USA=1, TW=2, CN=3, JP=4, IT=5, // USA/UK, TW, CN, JP, IT,
} SP_MODE; // 用來表示在哪個國家使用公曆(西元曆)
/// 以上 typedef 定義一個新型別 SP_MODE, 其實它就是 int, 但這樣用起來有可讀性且比較不會不小心弄錯
/// 因為 ncal 命令 -s 切換 Country Code, -p 列出 Country Code 和切曆法切換日期, 所以用 SP_MODE
/// 注意, USA 故意從 1 開始, 因考量後面要用 -USA, -TW, -CN, .. 負數用來表示特殊意義 !
extern int yy, mm, dd; // year month day ; 弄成 Global 變數, 省得傳來傳去 :-)
extern int adjustYear, adjustMonth, adjustDay, dayAdd;
extern SP_MODE spmode; // country code (see ncal -p; ncal -s CN )
extern int isleap(int); // 是否為閏年?
extern int findDMY(int ans[ ], char* p); // 從 char p[ ] 裡面找出 日 月 年 放入 int ans[ ]
extern int dow0101(int year); // Day Of Week of year/01/01
extern void hello( ), prtDebug( ), setMode( ), prtMode( int ); // set and print CCODE
extern const char* CCODE[]; // Country Code, 對應到 SP_MODE 內
extern int ADJ_YEAR[ ], ADJ_MONTH[ ], ADJ_DAY[ ], DAY_ADD[ ]; // 曆法調整的日期
extern const char* DOW[ ]; // Day Of Week, DOW[0] 是代表 Sunday 星期日 的字串
///原則上如果要拆成多個 .c / .cpp 檔案, 建議都要有以上"宣告",
///所以, 通常把以上弄成 .h 檔案, 然後每個 .c / .cpp 都要 #include "該.h檔案"
////////////////////////
///Definition (定義) -- 注意, 以下變數的"定義"不要放入 .h 檔案!
///// 再說一次, 通常各個或者多個 .c / .cpp 共用到的 Global 變數建議"定義"在 main( ) 所在那個原始檔案!!
/// 如果你到現在還沒搞清楚"宣告"vs."定義", 仔細看這篇:https://www.cprogramming.com/declare_vs_define.html
/// 也看看這篇: https://pediaa.com/what-is-the-difference-between-declaration-and-definition-in-c/
int dd, mm, yy; // Global 變數; 日, 月, 年
int adjustYear, adjustMonth, adjustDay, dayAdd; //例如 1752, 9, 2, 11 表示09/02隔日 3+11是14日
SP_MODE spmode = USA; // default use USA as the Country Code (see ncal -p ) 注意 -p 的國碼都2字
const char* CCODE[] = { // 用來印出用的國家代碼; 注意美國故意用 "USA" 不是兩個字母
"WHAT", "USA", "TW", "CN", "JP", "IT", // Country Code
}; // 注意這和以下幾個 調整日相關的 array 都故意 [0] 這元素不用 !
const char* DOW[] = { // Day Of Week; 你也可以改用英文 "Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"
"日", "一", "二", "三", "四", "五", "六",
}; // 別忘了分號;
int ADJ_YEAR[ ] = {0, 1752, 1911, 1911, 1918, 1582,}; // do NOT forget ";"
int ADJ_MONTH[ ]= {0, 9, 12, 12, 12, 10,} ;
int ADJ_DAY[ ] = {0, 2, 18, 18, 18, 4,};
int DAY_ADD[ ] = {0, 11, 13, 13, 13, 10, };
/// 以上 是 曆法調整資訊 for 各國(0, USA, TW, CN, JP, IT); 例如美國 1752/09/02 隔日是 03+11 為 14日
///// 也可以用 struct 把上述該些調整資訊 aggregate 在一起; Linux 的 ncal.c 原始碼就是那樣寫。
///// 可自己搜尋 ncal.c source code apple 找到 Apple 公司公開的 open source 之 ncal.c 原始碼。
/// ++ /// 以下 isleap(year) 判斷 year 是否為閏年 --- 注意會參考 Global變數 adjustYear 曆法調整年分
/// 注意只用到 Switch from Julian calendar to Gregorian calendar 的 "年"
// 所以, 你也可以改寫以下用參數傳入調整年, 例如 isleap(int year, int adjustYear) { ...
int isleap(int year) { //判斷是否為閏年, 超級簡單:-)
if(year < 4 ) return 0; // 不是閏年 ; 也不處理公元前, 因那是不規則的 !
if(year <= adjustYear) return (year%4)==0; // Julian calendar (儒略曆)
// 以上是因 Julian calendar (儒略曆)規定只要除以四餘數 0 就是閏年。
if(year%400 == 0) return 1; // after switch to Gregorian calendar (格里曆)
if(year%100 == 0) return 0; // 不是 Leap year
if(year%4 == 0) return 1; // yes
return 0; // 消除所有一切的不可能, 剩下的就是答案 :-) 不是閏年 !
} // isleap(
/// /// 以下這 findDMY( ) 跟之前給過 readLongs( ) 類似 ! 也跟前面 findNums( ) 類似 !
// 在 findDMY( ) 內, 會順便 根據輸入資料 可能偷偷修改 yy
int findDMY(int gg[ ], char* p) { // grab d m y from char p[ ] and put into int gg[ ]
char*pOLD = p;
char c = toupper(*p);
if(c == 'Q' || c=='E') return -999; // quit
if(c == 'T') return -TW; // 因為正整數要用來表示讀取到幾個整數在 int gg[ ]
if(c == 'C') return -CN;
if(c == 'J' || c=='N') return -JP; // Japan / Nippon 日本
if(c == 'I' || c=='R') return -IT; // IT or 羅馬
if(c == 'A') return -USA; // American == US
if(c == 'U') return -USA; // US
/// 如果第一個字不是以上任一個, 就當作輸入了一個數值(年) 或 兩個數值(月 年) 或者 三個數值(日 月 年)
gg[0] = strtol(p, &p, 10); // 搜尋 cplusplus strtol 就可找到 strtol( )手冊
mm = 12;
yy = 2024; // 先假設是 2024 年; 就是說你啥都沒打那就用 2024 年 12 月
if(pOLD == p) return 0; // strtol( ) 手冊規定這樣表示 沒有讀取到任何啥數值
pOLD = p; // 再次記住現在的 p
yy = gg[0]; // 先假設這是 Year, 放入 yy 這 Global 變數
gg[1] = strtol(p, &p, 10); // strtol( ) 回傳是 long, 會自動被轉成 int 才放入 int gg[ ]
if(pOLD == p) return 1; // 只讀到 Year 放 gg[0]
pOLD = p; // backup p again
yy = gg[1]; // 目前 Year 變成在這 gg[1]
gg[2] = strtol(p, &p, 10); // try to scan 3rd integer number
if(pOLD == p) return 2; // 前已經讀到了 Month 和 Year ; 但 gg[2] 沒讀到
yy = gg[2]; // 既然讀到 3 個數, gg[2] 才是 Year
return 3; // gg[0] 是 Day, gg[1]是 Month, gg[2] 是 Year
} // findDMY(
/// /// ///
const char* fmt[ ]= { "Unknown", // fmt[0]
" Got year %d\n", // fmt[1]
" Got year %d, month %d\n",
" Got yy-mm-dd : %.4d-%.2d-%.2d\n",
};
const char*MSG_LEAP[ ] = {"NOT Leap year.", "閏年。", };
/// /// ====== main( ) program ====== /// /// ///
int main( ) {
int ans, dmy[9]; // yy, mm, dd are Global
setMode( ); // 根據 spmode 設定各相關資料 adjustYear, adjustMonth, adjustDay, dayAdd
hello( ); // hints
char buf[99];
while(38) {
prtDebug( );
fprintf(stderr, "QTCJAI or type year or M Y or D M Y: ");
fgets(buf, sizeof buf, stdin);
ans = findDMY(dmy, buf);
if(ans < -888) break;
if(ans < 0 ) {
prtMode( -ans ); continue;
}
mm = 12;
yy = 2024; // 都沒輸入就當作輸入 12 2024
if(ans==1) { yy = dmy[0]; } // 只輸入個整數那就是 Year 在 dmy[0]
if(ans==2) { yy = dmy[1]; mm = dmy[0]; } // Year 變成在 dmy[1]
if(ans==3) { yy = dmy[2]; mm = dmy[1]; dd = dmy[0]; }
if(ans==0) ans = 2; // 都沒入輸入就當作輸入 12 2024 所以 ans 是 2 表示兩個數
/// 根據 ans 用不同格式 fmt[ans] 印出 yy, mm, dd
fprintf(stderr, fmt[ans], yy, mm, dd); ///+++ --- +++ --- +++ --- 參數太多不會有問題 !
ans = isleap(yy); // 0 平年, 1 閏年
printf(" === Year %.4d is %s\n", yy, MSG_LEAP[ans]);
int d = dow0101(yy); // 算出 yy 這年的 01/01 是星期幾 (0..6) Day Of Week
d = d % 7; // 確保 0..6
printf(" %d 年第一天 %.4d/%.2d/%.2d 是 星期 %s\n", yy, yy, 1, 1, DOW[d]);
/// 注意目前 dow( ) 還須稍作修改否則在調整年的調整月可能是錯的
if(mm < 1 || mm > 12)mm = 1; // 防呆一下 :-)
if(dd <1 || dd > 31)dd=1; // 注意這裡 二月和小月沒處理喔
extern int dow(int, int,int); //故意在此宣告
d= dow(yy, mm, dd); // yy-mm-dd 是星期幾 ?
printf(" [%.4d-%.2d-%.2d] is 星期%s\n", yy,mm,dd,DOW[d]);
} // while
for(ans=4; ans<13; ans+=2) printf("%*s, ", ans, "再見");printf("\n");
for(ans=4; ans<13; ans+=2)printf("%-*s, ", ans, "再見"); // ans 值會先填入 "*"處
printf("Bye.\n");
return 0;
} // main(
////// //////
/// setMode( ) 沒使用參數, 會根據 Global 變數 spmode 把曆法調整相關變數設定好
void setMode( ) {
adjustYear = ADJ_YEAR[ spmode ];
adjustMonth = ADJ_MONTH[ spmode ];
adjustDay = ADJ_DAY[ spmode ];
dayAdd = DAY_ADD[ spmode ];;
} // setMode(
/// 以下 prtMode(int mode) 是在 mode 變動時傳過來讓這設定並且印出切換訊息
void prtMode(int mode) { // 注意參數故意用 int
spmode = (SP_MODE) mode; // C++ 一定要明確寫 (cast type); C 則比較隨便可以不明寫 cast
setMode( ); // 注意 setMode( ) 故意寫成不接受參數, 全部用 Global :-)
fprintf(stderr, " +Switch to Country code %s\n", CCODE[spmode] );
}// prtMode(
void hello( ) {
fprintf(stderr, "Q to quit, TCJUAI change Country,"
" or type one number for year\n"
" or 2 numbers for Month Year, or 3 numbers for D M Y\n"
" will check the year to see whether it is a Leap year or NOT.\n");
} // hello(
///以下 dow0101(int year) 可以幫忙"算"出 year/01/01 是星期幾, 0 表示 Sunday
// 這個函數會用到 calendar switch 的相關資訊; 這樣要增加其他切換國家都不必來改這 !
int dow0101(int year){
int d = 6; // 01/01/01 是 Saturday (這是主要參考點)
// 365 = 7 * 52 + 1 所以若 year 前面一年是平年, 則 d 一年 +1; 閏年要 +2
/// .. 意思就是說, 若今年是平年, 明年同一個月日的星期幾比今年的星期幾多 1 (閏年則多 2)
d += (year - 1); // year 之前的年先都假裝算做平年; 所以從 0001 年到 year 要加入(year -1)
int numLeap = (year -1) /4; // Julian claendar 只要除以四除得盡都是閏年
d += numLeap; // 每個閏年要多 +1 (前面說了, 閏年該 +2, 剛剛算作平年已經 + 1 了)
d = d%7; // 每週是 七天啦
if(year <= adjustYear) return d; // 調整年以及以前這就是答案
/// 接著要處理 year 在調整年之後的 year/01/01 (即 year > adjustYear)
// adjustYear 之後要再 Shift Day Of Week (用到 dayAdd, 且是否閏年之規則 Gregorian calendar 改啦!)
/// 其實, dayAdd 可以根據 adjustYear 計算出來, 就是說其實不必用 array 記住該調整幾天 !
/// 因為 dayAdd 就是在 adjustYear 以前錯算了幾個閏年, 包括公元前少算兩次閏年,
/// .. 以及公元開始到 adjustYear 共多算了幾個閏年(kk), 阿就 kk-2 就是 dayAdd 啦。
d += (14 - dayAdd); // dayAdd == 11 for USA, 13 for TW/CN/JP; 10 for IT/Rome 義大利/羅馬
// 上句 +14 避免把 d 弄成負數, 一星期是七天, 所以要加入 七的倍數, 把 14 改 21 或者 28 都可以。
// 接下來是要遵守 adjustYear 之後的現行閏年規則, 因為前面的計算都是用儒略曆的 year%4 ==0 算閏年;
int leap100 = (year - adjustYear/100*100 -1) /100; //不是閏年要扣掉; 有幾個 除以100除得盡的?
int leap400 = (year - adjustYear/400*400 -1) / 400; //這是閏年要加回去; 有幾個除以400除得盡的?
d = d + 9999999/7*7 + leap400 - leap100; // +9999999/7*7 (確保七的倍數); 防止d變 negative 數
while(d < 0) d += 7; // 雖說感覺 d 應該不可能 小於 0, 但是再多個防呆無妨啦; 這樣應可算到地球毀滅:-)
d = d%7; // Day Of Week 星期幾
return d; // 這就是 year/01/01 是 星期 d ; d 是 0 則表示星期日
} // dow0101(
/// 以下 dow( ) 會用到 dow0101( ) 以及 calendar switch from 儒略曆 to 格里曆 的 轉換資訊
/// dow(year, mm, dd) --- Day Of Week 找出 year/mm/dd 是 星期幾
int dow(int year, int mm, int dd) { // 計算 year/mm/dd 是 星期幾; 注意這裡 mm, dd 不是 Global喔
static int nds[ ]={0, 31, 28, 31, 30, 31, 30, 31,31,30, 31,30,31,}; //0, 1..12 故意 0 不用
int d = dow0101(year); // 先算出 year/01/01 是星期幾
int i, days = dd-1; // 仔細想想, 為何用 dd-1 ?
// 算出 year/01/01 到 year/mm/dd 相差幾天, 放 days
for(i=1; i < mm; ++i) days += nds[i]; // 加入 這個月(mm)以前的每個月之日數
if(mm >= 3) if( isleap(year) ) days += 1; // 閏年 2 月多一天
// 注意還必須考慮曆法調整日期喔!
if(year==adjustYear && mm == adjustMonth && dd > adjustDay) { // 注意這幾個 Global 變數
// 這個留給你自己想 :-) Hint: 要用到 dayAdd
} // if
d += days; // days 是 year/mm/dd - year/01/01 有幾天
return d%7; // 這就是答案
}// dow(
/// 以下用來印出幫忙DEBUG的訊息
void prtDebug( ) { // print some DEBUG message
#ifdef DEBUGPP
fprintf(stderr, " +Current YY/MM/DD : %.4d/%.2d/%.2d\n", yy, mm, dd);
fprintf(stderr, " CCode %s, Adjust +%d days after %.4d/%.2d/%.2d\n",
CCODE[spmode], dayAdd, adjustYear, adjustMonth, adjustDay);
#endif
} //prtDebug(
/// END of isleap.c / isleap.cpp
// https://www.tondering.dk/claus/cal/chinese.php (ncal / Calendar FAQ)
////// //////