// isleap.c       ./index.html (HWK# ncal)
///========================

///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)
////// //////