本文主要針對一些簡單的時間議題做探討,後半段會提及實際工作常問到的 Q & A : 時間切割問題。

時間議題 在不同工作相信會有不同議題產生,下面大多是筆者之前接案時遇過的問題 (問題要詳述會花時間,此處不提) ,

當時還傻傻的受限於 MFC::CDate、MFC::CTime  (當時沒有 MFC::CDateTime) 功能不足,

自己慢慢擴充,直到後來翻了 Standard Library 後,才知道怎麼做是可接受的方式。

 

話說,時間切割問題筆者也很常在其他論壇看到,不過還真沒看過有人提出轉換方式。

 

 

0. 別期待與時間高度相關之專案,具有可攜性 / time.h 概述

 

標準函式庫裡面的時間函式相信各位網友都知道,去 time.h / ctime 裡面查就有了,但基本上與「時間」有較高要求之處理,很難單純要求標準函式庫幫忙做好,於是大多還必須再依賴於開發 / 測試環境,及編譯器支援。

舉一個例子:目前標準函式庫裡,找不到「設定目前系統時間」之函式 ( 我翻遍了 c++ reference ,應該是找不到吧?),沒靠編譯器支援,大概就是用 command / script 完成,Win32 底下有支 API 叫 SetSystemTime 可協助。再舉一個例子:取得檔案三種時間 (Access、Create、Modify),這也是沒標準函式支援。

即使如此,但大多 APIs 設計時間/日期函式,概念上均與 time.h / ctime 之時間函式相仿,故說明時還是以標準函式庫做為說明。

 

1. UTC / GTM / LOCAL TIME

 

在 ctime / time.h 裡,特別注意到一些敘述指的是 UTC / GTM 還是 Local Time,UTC 指的是 Universal Time Coordinated,全球統一時間。GTM  為格林威治時間,UTC 與 GTM 是完全一樣的。

而所謂的 Local Time,在安裝作業系統時就會有所提示,使用欲顯示時間的時區 (或者是 BIOS 裡面也可調吧),目前台灣一般設定,是 GTM + 8hrs,對岸也是這組設定 (其實講 "設定" 是不好、不對的說法,不過這裡略過長長的說明 )。所以在網路上看別人用中文討論時,會強調 "某兩個函式" 取出來的時間會差八小時,原因是一個是 UTC / GTM ,一個是 Local Time,只是剛好探討的 Local Time 比 GTM 多了 8 小時而已。

 

2. time.h 概述

 

另外往下看之前,希望能到 c++ reference 把  ctime / time.h 相關函式 view 過一遍。概要如下

 

(a) time_t 

實際上是一個整數 (或無號數)

 

(b) struct tm 

 存時間的結構體,注意到標準函式庫裡提到,有些系統會有「潤秒」問題。

tm_year 為年份,從 1900 年計算(1900當 0);tm_mon 為月份,0~11

tm_wday 是星期幾,0代表星期日,依此類推;

tm_isdst 是日光節省時間 (在夏季的時候,會將時間調早一小時,早一小時起床,早一小時睡覺,節省能源)

tm_yday 是當日為該年之第幾天 (從 0 開始算)

 

值得注意的是,有份沒經筆者證實的網路文章, tm_dist 必須設負值,方可忽略潤秒之處理 (可能需查 unix 系統文件)

不過有經 C99 文件證實的是,tm_isdst 為正值為零時,會導致 mktime (struct 轉 time_t ) 將因日光節省時間而有所不同,

 

(c) diff_time

計算時間差為 diff_time,引數為 time_t,傳回兩個時間的差值 (double, 秒)。

 

(d) time_t <---> struct tm

time_t 轉 struct tm (UTC / GTM) : gmtime ;

time_t 轉 struct tm (Local Time) : localtime ; 

struct tm (Local Time) 轉 time_t : mktime;

struct tm (UTC / GTM ) 轉 time_t : 目前沒這東西

 

(e) struct tm ---> str

asctime , strftime

 

(f)  time_t ---> str

ctime 

 

 

3. 關於時間的儲存方式與轉換

 

不論是不是標準函式庫,時間的儲存方式大多有兩套:一套是結構體,存 年、月、日、時、分、秒、星期、一年之第幾天 等資訊;另一套是用「整數」去儲存,這個整數的意義就要看該函式之說明。目前用整數去儲存之時間格式,普遍性都是以「距離哪天的時間點」為時間差。

舉個例,time_t 實質上是一個整數,代表意義是 距離 UTC (世界標準時間) 1970 年,一月一日,00:00:00 所經過之秒數,這個整數在我的系統裡是被定義成 32 位元有號數 ( __int32 ,另一可能是 __int64,depends on system ),若以 32 位元有號數來講,代表最多可歷經 2147483647 秒,一年以365.25 天來粗算的話,約可表達 68.05 年,換句話說,到 2038 年時,它會有溢位的可能,至於到 2038 年的哪時會溢位,嗯,我沒興趣再精算了。

不過我想如果快到 2038 年時 (還有25年) ,又如果那時 C/C++ 還沒被淘汰的話,或許到時便會要求至少要 64 bits 整數實作 time_t 。

另一種時間儲存方法是 struct,它主要是顯示一些資訊,提供 coder 使用,電腦真正在計算時,還是以「整數」方式為佳。另外,struct 結構不一定會有,但以整數表示時間的資料型態一定會有。若有 struct 時間結構體時,通常還會再附上一些 (struct 時間結構體 <-----> 整數時間表示)   之轉換函式。

 

又以 Win32 API 來講,像 FILETIME 本身雖是一個結構體,但本質上是一個整數,因這個結構體存放的是兩個 unsigned,一個 high-word,一個 lo-word,代表從 UTC 1601 年 1 月 1 日 00:00,經過了 幾百個 nano-secs,而相對應轉換的結構體是 SYSTEMTIME ,相換轉換函式包含了FileTimeToSystemTimeSystemTimeToFileTime。 FILETIME 以 64-bits 來算的話,從 1601 年必需再經過約 5.8 萬年以上才可能溢位。 

 

( 2^64-1  ) / 10^7 = 1.844674407 x 10^12 (secs)

1.844674407 x 10^12 (secs) / 3600  = 512409557 (hours)

512409557/  24  = 21350398 (days)

21350398 / 365.25 = 58454 (years)

 

4. 潤年問題

 

這裡不會講曆法,不會講古邏馬,不會講農民曆、不會講原子時與世界時... etc。

從程式邏輯上,廣為人知是 4 年一潤、100 不潤、400 又潤,目前的系統大概是從這規則。事實上一年 (正確的說法是一個迴歸年,tropical year) 是 365.24220 天,去做修正,故另一種說法是, 4 的倍數潤年,100 倍數不潤,400 倍數潤,4000 倍數不潤,由於 4000 倍數離現在實在是差太遠了,所以大概也還沒被納入規則過。所以程式碼大概都是這麼寫的。


int is_leap_year(int year)
{
    return (year%400==0) || ( (year%4==0) && (year%100!=0) ) ;
}


這算好算。另一個問題是,給定 [year_begin, year_end],其中有幾個潤年?這問題要寫的好就不容易了。

最無腦的寫法

 

int leap_year_cnt(int year_beg, int year_end)
{
    int c=0;
    while(year_beg <= year_end ){
        c += is_leap_year(year_beg);
        ++year_beg;
    }
    return c;
}

 

 好一點的方式是,先從 year_beg 開始找到 4 的倍數,每次 + 4 進入,再逐一判斷。

筆者的想法如下。

(1) 計算 [1, year_beg] 中,有幾個潤年

int c1 = year_beg / 4 - (year_beg / 100) + (year_beg / 400);

(2) 計算 [1, year_end] 中,有幾個潤年

int c2 = year_end / 4 - (year_end / 100) + (year_end / 400);

(3) c2 - c1 代表 (year_beg, year_end] 中有幾個潤年,故最後再額外判斷 year_beg 是否為潤年。

 

int leap_year_cnt2(int year_beg, int year_end)
{
    int c1 = year_beg / 4 - (year_beg / 100) + (year_beg / 400);
    int c2 = year_end / 4 - (year_end / 100) + (year_end / 400);
    return c2-c1 + is_leap_year(year_beg) ;
}

 

5. 計算一年中的第幾天 / 求一年中的第幾天


(5.1 ) 假設給定 yyyy 年 mm 月 dd 日,要計算該天是當年的第 n 天 (假設 1/1 是第 1 天);

(5.2)  假設給定 yyyy 年的第 n 天,求得當天為 mm 月 dd 日。

 

關鍵只有一個,建立一個表,裡面放的是累計天數,其它的可順利推出。程式碼不做 error defect,均假設輸入是正常,大致如下。

 

const int column_days[] = { \
  0, 0, 31, 59, 90, 120, 151, \
  181, 212, 243, 273, 304, 334};
int find_nday(int year, int month, int day)
{
    return column_days[month] + day + \
        (month > 2 && is_leap_year(year));
}

 

這裡有個偷雞的方法可以求,但效率「可能」會差一些 (除非在特殊情況)。首先我們知道 struct tm 可以轉成 time_t,最後再用 time_t 轉回 struct tim,再取得裡面的 tm_wday 就是了。這個方法除了可以知道是一年中的第幾天之外,還可以順便算算它是星期幾。

 

#include <stdio.h>
#include <time.h>
int find_nday(int year, int month, int day)
{
    struct tm  src={0};
    struct tm  *dst ;
    time_t     rawtime;

    // 設定 struct
    src.tm_year = year-1900;
    src.tm_mon = month-1;
    src.tm_mday = day;
src.tm_isdst = -1 ;
    rawtime = mktime(&src); // 轉到 time_t
    dst = localtime (&rawtime);// 再轉回 struct
    return dst->tm_wday;
    return dst->tm_yday+1;
}

int main()
{
    int y, m, d;
    while(scanf("%d%d%d", &y,&m,&d)==3)
        printf("%d\n", find_nday(y, m, d));
    return 0;
}

 

有人會問:這裡用的 struct tm 轉成 time_t ,是設定 年、月、日、時、分、秒,其中的 tm_yday (一年中的第幾天) 和 tm_wday ( 星期幾) 都沒用到,說不定 mktime 會吃 tm_yday,而不是吃年月日。

嗯,這作法筆者查過了,C99 7.23.2.3 裡面有強調,用 mktime 將 struct tm (local time) 轉成 time_t 時,tm_wday 及 tm_yday 會被忽略,只用其他的值 (那不就是指年月日時分秒 及 日光節約時 了嗎!) 去轉成相對的 time_t,所以這作法可以放心使用,同時也說明了,它不能設定 tm_yday,去查是哪一天。

要從 ndays 轉成日期概念上也如上面建立陣面概念類似, 只是建立的表不同。code 隨手寫寫,要效率高就要再化簡過


int mon_days[] = { \
    0, 31, 28, 31, 30, 31, \
    30, 31, 31, 30, 31, 30, 31};
void nday_to_date(
    int year, int ndays,
    int *month, int *day)
{
    int m ; // temp of month
    mon_days[2] = 28 + is_leap_year(year);
    for(m=1 ; ndays>mon_days[m] ; ++m)
        ndays-=mon_days[m];
    if(ndays==0)
        ndays=mon_days[--m];
    *day = ndays, *month=m;
}


 

6. 算星期

 

別用定義 (公元元年 1 月 1 日是星期日) 去算,一方面已不實用,曆法中間有改過幾次;一方面 loop 跑很慢,所以下面公式也是在 15xx 年 之後適用。

主要有兩個公式,一個是基拉爾森算式,另一個是蔡勒公式 (有改善版)。

基拉爾算式用看的就大概就清楚,主要是蔡勒公式,( 目前網上翻似乎沒用 C 語言寫正確的?),參考如下。

 

// --------------------------------------------------------
// 基姆拉爾森算式
int weekday(int y, int m, int d)
{
     return (d+2*m+3*(m+1)/5+y+y/4-y/100+y/400)%7;

}

// --------------------------------------------------------
// Zeller
int Zeller(int Y, int m, int d)
{
    int c, y, w; // c : center , y : last two dig of year , w : days of week
    if(m==1 || m==2) m+=12, --Y;
    y = Y % 100, c=Y/100;
    w = y + y/4 + c/4 - 2*c + 26*(m+1)/10+d-1; // negative possible
    return (w%7+7)%7;
}

// --------------------------------------------------------
// fix zeller
int Zeller2(int Y, int m, int d)
{
    int r[] = {0, 6, 2, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4};
    int y = Y % 100, c=Y/100;
    int w;

    if(is_leap_year(y)) r[1]=5, r[2]=1; //leap_year fix
    w = y / 4 + y%7+ 2*(c%4) + r[m] + d;
    return w % 7;
}

 


通常不會直接用上述這些公式求,有些系統用的公式可能有用到浮點數之 bit 運算。

當然還必須強調,可以用 mktime,再轉回 localtime,這樣就知道是星期幾了。


 

7. 給定兩個日期(年月日),計算相差幾天

 

有些情況,是要算兩個日期相差幾天,在這問題下,我們先算的是,兩個日期相差幾秒,為何?因為轉成 time_t 可以輕易得到。

int y1, m1, d1; /* 開始年月日 */
int y2, m2, d2; /* 結束年月日 */

(1) y1, m1, d1 填入 struct tm begin。
(2) y2, m2, d2 填入 struct tm end。
(3) time_t raw_begin = mktime(&begin);
(4) time_t raw_end = mktime(&end);
(5) 傳回 (raw_end - raw_begin)

 

#include <stdio.h>
#include <time.h>

int diff_secs(int y1, int m1, int d1,
              int y2, int m2, int d2)
{
    struct tm begin={0,0,0,d1,m1-1,y1-1900};
    struct tm end={0,0,0,d2,m2-1,y2-1900};
    time_t rawtime1, rawtime2;

    rawtime1 = mktime(&begin);
    rawtime2 = mktime(&end);
    return mktime(&end) - mktime(&begin);
    return (rawtime2 - rawtime1);

}

double Sec2Min(int secs) {return secs / 60.0; }
double Sec2Hour(int secs) {return secs / 3600.0; }
double Sec2Day(int secs) {return secs / 86400.0;}

int main()
{
    int y1, y2, m1, m2, d1, d2, Tsecs;
    while( scanf("%d%d%d%d%d%d",&y1,&m1,&d1,&y2,&m2,&d2)==6){
        Tsecs = diff_secs(y1,m1,d1,y2,m2,d2);
        printf("Td (secs) = %d\n",  Tsecs);
        printf("Td (mins) = %f\n",  Sec2Min(Tsecs));
        printf("Td (hours) = %f\n",  Sec2Hour(Tsecs));
        printf("Td (days) = %f\n",  Sec2Day(Tsecs));
    }
    return 0;
}

 

其他要改成相差幾天、相差幾小時,用一樣方法計算。

 

8. 給定兩個時間(年月日時分秒),做 n 等分切割

 

有上一個概念之後,要做切割也不是問題。

 

(0) 中間有 n 等份的「時間區間」故有 n+1 [0, n] 個點,第 0 個點是 begin,第 n 個點是 end, 中間間隔為 (end-begin) / n,另 0 等份是不存在的。

(1) 取得 time_t raw_begin, raw_end

(2) 取得 raw_end - raw_begin,相差秒數

(3) 取得每等份之時間間隔:Td = (double)(raw_end - raw_begin) / n;

(4) 確切間隔之時間點,虛碼大致如下。

time_t t0 = raw_begin; // 一開始的參考點
for(i=1; i<n; ++i) { // 第 i 份區間結束點
    time_t ti = (time_t) (raw_begin + i * Td + 0.5);
    printf("T%d = %s\n", i, ctime(ti);
}
time_t tn = raw_end; // 第 n 份區間結束點

 

#include <stdio.h>
#include <time.h>

int TimeDiv(
    int y1, int m1, int d1, int h1, int mm1, int s1,
    int y2, int m2, int d2, int h2, int mm2, int s2,
    time_t *tpoint, int n)
{
    double Td;
    int i;
    struct tm begin={s1,mm1,h1,d1,m1-1,y1-1900,0,0,-1};
    struct tm end={s2,mm2,h2,d2,m2-1,y2-1900,0,0,-1};
    time_t raw_begin = mktime(&begin);
    time_t raw_end   = mktime(&end);

    if(n < 1 || raw_begin > raw_end) return 0; // fail

    Td = \
        (double)(mktime(&end) - raw_begin) /  n;

    for(tpoint[0] = raw_begin, i=1; i <  n; ++i)
        tpoint[i] = raw_begin + (time_t)(Td * i+0.5);
    tpoint[n] = raw_end;
    return 1;
}

int main()
{
    int y1, m1, d1, h1, mm1, s1; // 時間1, 年月日時分秒
    int y2, m2, d2, h2, mm2, s2; // 時間2, 年月日時分秒
    int i, n;         // 切成 n 等份
    time_t Tdiv[100]={0}; // 切割點, 假設最大100筆
    while( scanf("%d%d%d%d%d%d%d%d%d%d%d%d%d",
            &y1,&m1,&d1,&h1,&mm1,&s1,
            &y2,&m2,&d2,&h2,&mm2,&s2,&n)==13){

        TimeDiv(y1,m1,d1,h1,mm1,s1,y2,m2,d2,h2,mm2,s2,Tdiv,n);
        // 輸出時間點
        for(i=0; i<n+1; ++i) {
            // ctime 會自動加 new-line
            printf("%s", ctime(Tdiv+i));
        }
    }
    return 0;
}

 

相對的,若已知開始時間點 struct tm begin,要遞增 struct tm td ,往後取 n 個,作法與上相似。

 

 

9. 合法偵錯

 

上面程式碼省略了事前正確性判斷 (個人習慣由 caller 端去做),日期、時間輸入是否正確,應再額外判斷,怎麼判斷我想就不贅述了。

 

 

10. Others

 

幾個議題筆者沒切迫性需求沒再研究(其實還不是犯懶),作法很差,有好的作法或參考文章歡迎提供。

 

(1) 網路系統時間同步 : 這只是單純的練習題,但考慮到網路品質及傳遞時間問題應不是件簡單事,沒深入研究。

(2) 直接由 time_t 求星期幾、該年的第幾天 (不經過 struct tm 轉換):目前沒太多時間再去研究這個,有興趣可再研究。

 



edisonx 發表在 痞客邦 PIXNET 留言(2) 人氣()


留言列表 (2)

發表留言
  • 路過
  • 基姆拉爾森算式的code錯了!!
  • 大強
  • e大您好:
    請問一下我從NTP server取得一個secsSince1900的unsigned long,想把此值轉成年月日時分秒,看到您"struct tm 可以轉成 time_t"這個範例中,不知要如何將secsSince1900的值代入程式中,可以請教您嗎?謝謝!
  • 我先假設你的 C/C++ 有一定程度的了解,若不是的話我想我能提供的協助非常有限。另我必須強調,我並沒有考慮到 C++0x / C++11 / C++14 / C++17 等較新標準的做法或更改。

    我想你要的是從 time_t 轉成 struct tm,這個在這篇文章裡面有提到,使用 gmtime 或 localtime,我猜你要的應該是 localtime,暫用這個做敘述。

    要用 time_t 轉成 struct tm ,基本上不難,但一開始的限制有點不同, C/C++ 的 time_t 是從 UTC 1970/1/1 所經歷的秒數,而你是從 1900/1/1 所經歷的秒數,所以必須再前置做一些運算去處理,至於細節怎麼做,方法就很多了,像是可以算出這 70 年來的秒數做 offset ( 當然這個動作是叫程式做或直接查資料 )。

    另一個方法呼叫的 funcs 會比較多,另我沒拿到一個參考的數值/答案,不敢斷定我的想法是正確的,就此先打住。

    edisonx 於 2016/05/07 15:22 回覆

您尚未登入,將以訪客身份留言。亦可以上方服務帳號登入留言

請輸入暱稱 ( 最多顯示 6 個中文字元 )

請輸入標題 ( 最多顯示 9 個中文字元 )

請輸入內容 ( 最多 140 個中文字元 )

請輸入左方認證碼:

看不懂,換張圖

請輸入驗證碼