業務代碼中常和時間打交道, 例如時間串, 時間戳之間轉換. 當前時間是否是同一天等. 這裡就是為瞭解決這個問題.分享了這篇博文. ...
引言
業務代碼中遇到這樣需求, 1. 二者是同一天嗎, 2. 時間戳和時間串來迴轉, 3. 其它擴展需求 等.
C寫代碼同樣需要處理這方面時間問題. 本文就是為瞭解決這個問題. 相比其它時間庫, 這裡做了一些擴展. 一般而言
一天開始時間為 00:00:00 , 這裡 可以配置一天的開始時間.
舉一個實際用的業務例子. 暴雪游戲, 魔獸世界 或者 爐石傳說, 每次活動刷新都是以 05:00:00 開始.
這裡說明瞭什麼呢, 可以理解為這類游戲世界里, 時間迴圈的起點就是"05:00:00". 認為是一天的開始.
同樣我們的寫的sctimeutil.h 介面中有一個配置 一天的開始時間.
當然代碼一定是跨平臺的. 首先我們看一下 sctimeutil.h 介面設計思路如下:
#ifndef _H_SCTIMEUTIL #define _H_SCTIMEUTIL #include <time.h> #include <stdbool.h> // 為Visual Studio導入一些和linux上優質思路 #if defined(_MSC_VER) #include <Windows.h> /* * 返回當前得到的時間結構體, 高仿linux上調用 * pt : const time_t * , 輸入的時間戳指針 * ptm : struct tm * , 輸出的時間結構體 * : 返回 ptm 值 */ #define localtime_r(pt, ptm) localtime_s(ptm, pt), ptm /* * Linux sys/time.h 中獲取時間函數在Windows上一種移植實現 * tv : 返回結果包含秒數和微秒數 * tz : 包含的時區,在window上這個變數沒有用不返回 * : 預設返回0 */ extern int gettimeofday(struct timeval* tv, void* tz); #endif // 定義每天是開始為 0時0分0秒 #define _INT_MINSECOND (60) #define _INT_HOURSECOND (3600) #define _INT_DAYSECOND (24UL*_INT_HOURSECOND) #define _INT_DAYSTART (8UL*_INT_HOURSECOND) // 定義每天新的開始時間 #define _INT_DAYNEWSTART (0UL*_INT_HOURSECOND + 0*_INT_MINSECOND + 0) // struct tm 中 tm_year, tm_mon 用的偏移量 #define _INT_YEAROFFSET (1900) #define _INT_MONOFFSET (1) // 定義時間串類型 #define _INT_STULEN (32) typedef char stime_t[_INT_STULEN]; /* * 將 [2016-7-10 21:22:34] 格式字元串轉成時間戳 * tstr : 時間串分隔符只能是單位元組的. * pt : 返回得到的時間戳 * otm : 返回得到的時間結構體 * : 返回這個字元串轉成的時間戳, -1表示構造失敗 */ extern bool stu_gettime(stime_t tstr, time_t * pt, struct tm * otm); /* * 判斷當前時間戳是否是同一天的. * lt : 判斷時間一 * rt : 判斷時間二 * : 返回true表示是同一天, 返回false表示不是 */ extern bool stu_tisday(time_t lt, time_t rt); /* * 判斷當前時間戳是否是同一周的. * lt : 判斷時間一 * rt : 判斷時間二 * : 返回true表示是同一周, 返回false表示不是 */ extern bool stu_tisweek(time_t lt, time_t rt); /* * 將時間戳轉成時間串 [2016-7-10 22:38:34] * nt : 當前待轉的時間戳 * tstr : 保存的轉後時間戳位置 * : 返回傳入tstr的首地址 */ extern char * stu_gettstr(time_t nt, stime_t tstr); /* * 得到當前時間戳 [2016-7-10 22:38:34] * tstr : 保存的轉後時間戳位置 * : 返回傳入tstr的首地址 */ extern char * stu_getntstr(stime_t tstr); /* * 判斷當前時間串是否是同一天的. * ls : 判斷時間一 * rs : 判斷時間二 * : 返回true表示是同一天, 返回false表示不是 */ extern bool stu_sisday(stime_t ls, stime_t rs); /* * 判斷當前時間串是否是同一周的. * ls : 判斷時間一 * rs : 判斷時間二 * : 返回true表示是同一周, 返回false表示不是 */ extern bool stu_sisweek(stime_t ls, stime_t rs); #endif // !_H_SCTIMEUTIL
設計介面瞭解後, 後面會詳細解說. (代碼對齊確實不好弄, 要是博客園直接能讓VS複製的代碼格式, 到富文本框中不改變, 那得多好.)
設計師才是王道.
前言
先看巨集配置, 主要的就是
// 定義每天新的開始時間 #define _INT_DAYNEWSTART (0UL*_INT_HOURSECOND + 0*_INT_MINSECOND + 0)
假如需要設置一天開始為05:00:00, 那就將第一個0變成5就可以了. 後面還定義了時間串類型
// 定義時間串類型 #define _INT_STULEN (32) typedef char stime_t[_INT_STULEN];
預設就是 char [32]長度的字元串. 統一類型在棧上並節省一個長度參數, 否則一般得到時間串 至少 char [], int 兩個參數.
其它的變數巨集, 都是為了去掉魔法數字用的. 最前面有段為VS導入擴展功能的巨集
// 為Visual Studio導入一些和linux上優質思路 #if defined(_MSC_VER) #include <Windows.h> /* * 返回當前得到的時間結構體, 高仿linux上調用 * pt : const time_t * , 輸入的時間戳指針 * ptm : struct tm * , 輸出的時間結構體 * : 返回 ptm 值 */ #define localtime_r(pt, ptm) localtime_s(ptm, pt), ptm /* * Linux sys/time.h 中獲取時間函數在Windows上一種移植實現 * tv : 返回結果包含秒數和微秒數 * tz : 包含的時區,在window上這個變數沒有用不返回 * : 預設返回0 */ extern int gettimeofday(struct timeval* tv, void* tz); #endif
這兩個''聲明''在跨平臺C代碼中出現頻率比較高. 第一個是安全可重入的將時間戳變成時間結構體. 第二個是得到更高精度的時間表示.
這裡分析一下對於linux上 有這麼定義
struct tm *localtime_r(const time_t *timep, struct tm *result);
window 上是下麵定義的
static __inline errno_t __CRTDECL localtime_s(struct tm * _Tm, const time_t * _Time) { return _localtime32_s(_Tm, _Time); }
簡單的理解為, 位置顛倒了. 其實而言, 細節上還是有不同. 扯一點二者平臺都有優點,
但從這個api上而言, 我覺得linux上localtime_r 更自然些, 返回值放在最後面. 隨著工作深入也發現,
有些api window設計也很出彩. 但是總的而言, 如果二選一, 還是覺得linux 設計的比window漂亮, 精妙, 自然.
扯回來, 上面為了更大程度上相容linux, 就在window上採用linux函數設計思路. 是不是有點意思.(另外一種跨平臺巨集設計思路)
對於 gettimeofday window源碼實現如下
// 為Visual Studio導入一些和linux上優質思路 #if defined(_MSC_VER) /* * Linux sys/time.h 中獲取時間函數在Windows上一種移植實現 * tv : 返回結果包含秒數和微秒數 * tz : 包含的時區,在window上這個變數沒有用不返回 * : 預設返回0 */ int gettimeofday(struct timeval* tv, void* tz) { struct tm st; SYSTEMTIME wtm; GetLocalTime(&wtm); st.tm_year = wtm.wYear - _INT_YEAROFFSET; st.tm_mon = wtm.wMonth - _INT_MONOFFSET; // window的計數更好些 st.tm_mday = wtm.wDay; st.tm_hour = wtm.wHour; st.tm_min = wtm.wMinute; st.tm_sec = wtm.wSecond; st.tm_isdst = -1; // 不考慮夏令時 tv->tv_sec = (long)mktime(&st); // 32位使用數據強轉 tv->tv_usec = wtm.wMilliseconds * 1000; // 毫秒轉成微秒 return 0; } #endif
上面兩個巨集, 分別是 1900和1, 這個是 struct tm 特性決定的, 這點window api功能設計的好.
#ifndef _TM_DEFINED struct tm { int tm_sec; /* seconds after the minute - [0,59] */ int tm_min; /* minutes after the hour - [0,59] */ int tm_hour; /* hours since midnight - [0,23] */ int tm_mday; /* day of the month - [1,31] */ int tm_mon; /* months since January - [0,11] */ int tm_year; /* years since 1900 */ int tm_wday; /* days since Sunday - [0,6] */ int tm_yday; /* days since January 1 - [0,365] */ int tm_isdst; /* daylight savings time flag */ }; #define _TM_DEFINED #endif
上面就是 struct tm 結構體實現. 時間操作還是比較不統一, 需要一定經驗, 多趟坑. 這裡時間預設從1900起計算, 也有1970起計算的. 例如(man 手冊中內容)
The ctime(), gmtime() and localtime() functions all take an argument of data type time_t which represents cal‐
endar time. When interpreted as an absolute time value, it represents the number of seconds elapsed since
00:00:00 on January 1, 1970, Coordinated Universal Time (UTC).
需要用到的時候再弄清楚吧. 推薦一定要用可重入的, 否則程式跑起來只能頭大了.
正文
現在準備剖析一下特定的代碼, 最後也會貼一下全部代碼. 運行一個測試demo. 首先看時間串得到時間戳時間結構體的介面實現
/* * 將 [2016-7-10 21:22:34] 格式字元串轉成時間戳 * tstr : 時間串分隔符只能是單位元組的. * pt : 返回得到的時間戳 * otm : 返回得到的時間結構體 * : 返回這個字元串轉成的時間戳, -1表示構造失敗 */ bool stu_gettime(stime_t tstr, time_t * pt, struct tm * otm) { time_t t; struct tm st; if(NULL == tstr) return false; int rt = sscanf(tstr, "%d%*c%d%*c%d%*c%d%*c%d%*c%d", &st.tm_year, &st.tm_mon, &st.tm_mday, &st.tm_hour, &st.tm_min, &st.tm_sec); if(6 != rt) return false; st.tm_year -= _INT_YEAROFFSET; st.tm_mon -= _INT_MONOFFSET; // 得到時間戳, 失敗返回false if((t = mktime(&st)) == -1 ) return false; // 返回最終結果 if(pt) *pt = t; if(otm) *otm = st; return true; }
思路很清晰, 解析時間串, 得到年月日時分秒, 最後通過mktime得到想要的. 期間需要註意的是 struct tm 必須 year 1900起, mon 從0開始記.
執行mktime之後會補充玩 st 時間結構體 並且返回當前時間戳.
判斷兩個時間戳是否是同一天也很巧妙
/* * 判斷當前時間戳是否是同一天的. * lt : 判斷時間一 * rt : 判斷時間二 * : 返回true表示是同一天, 返回false表示不是 */ bool stu_tisday(time_t lt, time_t rt) { // 得到是各自第幾天的 lt = (lt + _INT_DAYSTART - _INT_DAYNEWSTART) / _INT_DAYSECOND; rt = (rt + _INT_DAYSTART - _INT_DAYNEWSTART) / _INT_DAYSECOND; return lt == rt; }
這裡用到一個巨集
#define _INT_DAYSTART (8UL*_INT_HOURSECOND)
這個由來是咱們通過mktime 得到的是 標準時區時間, 而中國是東八區, 快了它8h. 詳細科普可以看下下麵資料
GMT(Greenwich Mean Time) 格林威治時間
即本初子午線的時間,一般作為全球時間的基準參考時間。據說是以格林威治天文臺命名的。
UTC(Universal Time Coordinated) 世界標準時間或世界協調時間
協調世界時是以原子時秒長為基礎,在時刻上儘量接近於世界時的一種時間計量系統。
UTC時間和GMT時間其實是同一個時間,只不過UTC時間的單位是秒。定期會進行校準,校準的方式是發佈閏秒,
即有兩個同樣的秒。記住,UTC是GMT的以秒為單位的計時。
CST(China Standard Time) 中國標準時間
可以認為新聞聯播嘟嘟嘟的時間。也就是東八區的時間。當GMT為0點的時候,我們已經8點了。
我們的時間需要在GMT的時間上加八個小時
有了這些知識, 後面 將兩個時間戳變成中國的時間戳. 再除以一天的秒數, 得到當前多少天了. 這裡需要註意一下, 這是伺服器級別的時間判斷.
如果對於軍工級別, 宇宙飛船等級別, 真不行, 因為一年民用要麼365要麼366, 其實一年 查了粗略資料有
地球圍繞太陽公轉一周(即360度)的時間應該為365日6時9分10秒,即為一個恆星年。
地球的某點獲得兩次兩次直射的間隔是365日5時48分46秒(更加精確:365天5小時48分45.975456秒),即為一個回歸年。
總而言之, 電腦還是離散數學級別, 精度只能大致算算, 因此咱們精度到天, 上面演算法是可以得了, 對於特別意外的那就看臉了.
(時間判斷也是程式國際化會遇到的一個坑, 必遇到滴~~)
從這裡覺得軟體開發還是偏藝術些, 科學是嚴謹的, 是符合一切數學規律的, 誤差是可以詳細分析的.
再看看另一個設計, 比較簡單, 時間戳得到時間串
/* * 將時間戳轉成時間串 [2016-7-10 22:38:34] * nt : 當前待轉的時間戳 * tstr : 保存的轉後時間戳位置 * : 返回傳入tstr的首地址 */ char * stu_gettstr(time_t nt, stime_t tstr) { struct tm st; localtime_r(&nt, &st); strftime(tstr, sizeof(stime_t), "%F %X", &st); return tstr; }
思路很直白吧,最後解釋測試一波了. 首先看main.c
#include <stdio.h> #include <stdlib.h> #include "sctimeutil.h" static void _sctime_puts(stime_t tstr) { printf("sizeof tstr = %lu, %lu\n", sizeof tstr, sizeof(stime_t)); } /* * 處理時間 time 的測試主函數 * */ int main(int argc, char * argv[]) { bool rt; time_t t; struct tm tm; stime_t ts; rt = stu_gettime("2016-7-12 11:27:00", &t, &tm); printf("tm.tm_year = %d, tm.tm_yday = %d\n", tm.tm_year, tm.tm_yday); // 測試數組長度 _sctime_puts(NULL); rt = stu_sisweek("2016-7-11 20:59:59", "2016-7-17 23:59:59"); printf("rt = %d\n", rt); rt = stu_sisweek("2016-7-11 0:0:0", "2016-7-18 0:0:0"); printf("rt = %d\n", rt); // 輸出當前時間量 puts(stu_getntstr(ts)); return 0; }
附加代碼文件
sctimeutil.h
#ifndef _H_SCTIMEUTIL #define _H_SCTIMEUTIL #include <time.h> #include <stdbool.h> // 為Visual Studio導入一些和linux上優質思路 #if defined(_MSC_VER) #include <Windows.h> /* * 返回當前得到的時間結構體, 高仿linux上調用 * pt : const time_t * , 輸入的時間戳指針 * ptm : struct tm * , 輸出的時間結構體 * : 返回 ptm 值 */ #define localtime_r(pt, ptm) localtime_s(ptm, pt), ptm /* * Linux sys/time.h 中獲取時間函數在Windows上一種移植實現 * tv : 返回結果包含秒數和微秒數 * tz : 包含的時區,在window上這個變數沒有用不返回 * : 預設返回0 */ extern int gettimeofday(struct timeval* tv, void* tz); #endif // 定義每天是開始為 0時0分0秒 #define _INT_MINSECOND (60) #define _INT_HOURSECOND (3600) #define _INT_DAYSECOND (24UL*_INT_HOURSECOND) #define _INT_DAYSTART (8UL*_INT_HOURSECOND) // 定義每天新的開始時間 #define _INT_DAYNEWSTART (0UL*_INT_HOURSECOND + 0*_INT_MINSECOND + 0) // struct tm 中 tm_year, tm_mon 用的偏移量 #define _INT_YEAROFFSET (1900) #define _INT_MONOFFSET (1) // 定義時間串類型 #define _INT_STULEN (32) typedef char stime_t[_INT_STULEN]; /* * 將 [2016-7-10 21:22:34] 格式字元串轉成時間戳 * tstr : 時間串分隔符只能是單位元組的. * pt : 返回得到的時間戳 * otm : 返回得到的時間結構體 * : 返回這個字元串轉成的時間戳, -1表示構造失敗 */ extern bool stu_gettime(stime_t tstr, time_t * pt, struct tm * otm); /* * 判斷當前時間戳是否是同一天的. * lt : 判斷時間一 * rt : 判斷時間二 * : 返回true表示是同一天, 返回false表示不是 */ extern bool stu_tisday(time_t lt, time_t rt); /* * 判斷當前時間戳是否是同一周的. * lt : 判斷時間一 * rt : 判斷時間二 * : 返回true表示是同一周, 返回false表示不是 */ extern bool stu_tisweek(time_t lt, time_t rt); /* * 將時間戳轉成時間串 [2016-7-10 22:38:34] * nt : 當前待轉的時間戳 * tstr : 保存的轉後時間戳位置 * : 返回傳入tstr的首地址 */ extern char * stu_gettstr(time_t nt, stime_t tstr); /* * 得到當前時間戳 [2016-7-10 22:38:34] * tstr : 保存的轉後時間戳位置 * : 返回傳入tstr的首地址 */ extern char * stu_getntstr(stime_t tstr); /* * 判斷當前時間串是否是同一天的. * ls : 判斷時間一 * rs : 判斷時間二 * : 返回true表示是同一天, 返回false表示不是 */ extern bool stu_sisday(stime_t ls, stime_t rs); /* * 判斷當前時間串是否是同一周的. * ls : 判斷時間一 * rs : 判斷時間二 * : 返回true表示是同一周, 返回false表示不是 */ extern bool stu_sisweek(stime_t ls, stime_t rs); #endif // !_H_SCTIMEUTILView Code
sctimeutil.c
#include "sctimeutil.h" #include <stdio.h> // 為Visual Studio導入一些和linux上優質思路 #if defined(_MSC_VER) /* * Linux sys/time.h 中獲取時間函數在Windows上一種移植實現 * tv : 返回結果包含秒數和微秒數 * tz : 包含的時區,在window上這個變數沒有用不返回 * : 預設返回0 */ int gettimeofday(struct timeval* tv, void* tz) { struct tm st; SYSTEMTIME wtm; GetLocalTime(&wtm); st.tm_year = wtm.wYear - _INT_YEAROFFSET; st.tm_mon = wtm.wMonth - _INT_MONOFFSET; // window的計數更好些 st.tm_mday = wtm.wDay; st.tm_hour = wtm.wHour; st.tm_min = wtm.wMinute; st.tm_sec = wtm.wSecond; st.tm_isdst = -1; // 不考慮夏令時 tv->tv_sec = (long)mktime(&st); // 32位使用數據強轉 tv->tv_usec = wtm.wMilliseconds * 1000; // 毫秒轉成微秒 return 0; } #endif /* * 將 [2016-7-10 21:22:34] 格式字元串轉成時間戳 * tstr : 時間串分隔符只能是單位元組的. * pt : 返回得到的時間戳 * otm : 返回得到的時間結構體 * : 返回這個字元串轉成的時間戳, -1表示構造失敗 */ bool stu_gettime(stime_t tstr, time_t * pt, struct tm * otm) { time_t t; struct tm st; if(NULL == tstr) return false; int rt = sscanf(tstr, "%d%*c%d%*c%d%*c%d%*c%d%*c%d", &st.tm_year, &st.tm_mon, &st.tm_mday, &st.tm_hour, &st.tm_min, &st.tm_sec); if(6 != rt) return false; st.tm_year -= _INT_YEAROFFSET; st.tm_mon -= _INT_MONOFFSET; // 得到時間戳, 失敗返回false if((t = mktime(&st)) == -1 ) return false; // 返回最終結果 if(pt) *pt = t; if(otm) *otm = st; return true; } /* * 判斷當前時間戳是否是同一天的. * lt : 判斷時間一 * rt : 判斷時間二 * : 返回true表示是同一天, 返回false表示不是 */ bool stu_tisday(time_t lt, time_t rt) { // 得到是各自第幾天的 lt = (lt + _INT_DAYSTART - _INT_DAYNEWSTART) / _INT_DAYSECOND; rt = (rt + _INT_DAYSTART - _INT_DAYNEWSTART) / _INT_DAYSECOND; return lt == rt; } /* * 判斷當前時間戳是否是同一周的. * lt : 判斷時間一 * rt : 判斷時間二 * : 返回true表示是同一周, 返回false表示不是 */ bool stu_tisweek(time_t lt, time_t rt) { time_t mt; struct tm st; lt -= _INT_DAYNEWSTART; rt -= _INT_DAYNEWSTART; if(lt < rt) { //得到最大時間, 保存在lt中 mt = lt; lt = rt; rt = mt; } // 得到lt 表示的當前時間 localtime_r(<, &st); // 得到當前時間到周一起點的時間差 st.tm_wday = 0 == st.tm_wday ? 7 : st.tm_wday; mt = (st.tm_wday - 1) * _INT_DAYSECOND + st.tm_hour * _INT_HOURSECOND + st.tm_min * _INT_MINSECOND + st.tm_sec; // [min, lt], lt = max(lt, rt) 就表示在同一周內 return rt >= lt - mt; } /* * 將時間戳轉成時間串 [2016-7-10 22:38:34] * nt : 當前待轉的時間戳 * tstr : 保存的轉後時間戳位置 * : 返回傳入tstr的首地址 */ char * stu_gettstr(time_t nt, stime_t tstr) { struct tm st; localtime_r(&nt, &st); strftime(tstr, sizeof(stime_t), "%F %X", &st); return tstr; } /* * 得到當前時間戳 [2016-7-10 22:38:34] * tstr : 保存的轉後時間戳位置 * : 返回傳入tstr的首地址 */ char * stu_getntstr(stime_t tstr) { return stu_gettstr(time(NULL), tstr); } /* * 判斷當前時間串是否是同一天的. * ls : 判斷時間一 * rs : 判斷時間二 * : 返回true表示是同一天, 返回false表示不是 */ bool stu_sisday(stime_t ls, stime_t rs) { time_t lt, rt; // 解析失敗直接返回結果 if(!stu_gettime(ls, <, NULL) || !stu_gettime(rs, &rt, NULL)) return false; return stu_tisday(lt, rt); } /* * 判斷當前時間串是否是同一周的.可以優化 * ls : 判斷時間一 * rs : 判斷時間二 * : 返回true表示是同一周, 返回false表示不是 */ bool stu_sisweek(stime_t ls, stime_t rs) { time_t lt, rt; // 解析失敗直接返回結果 if(!stu_gettime(ls, <, NULL) || !stu_gettime(rs, &rt, NULL)) return false; return stu_tisweek(lt, rt); }View Code
編譯命令
gcc -Wall -ggdb2 -o main.out main.c sctimeutil.c
測試結果 達到預期
window上測試也一樣. 有興趣的可以將思路用在自己的項目中. 思路比代碼有底蘊. 軟體開發, 馬龍還是無法簡單的表達咱們職業的.
設計師覺得更貼切些. 用雙手描繪色彩務實的人們.
後記
錯誤是難免的, 歡迎吐槽, 再打補丁修複~~
附加 :
晚上將代碼加入simplec框架中, 創造了更有意思的代碼, 真心值得學習研究 . 萬能時間轉換函數. 性能也好了許多. 拋棄了sscanf 這種大塊頭函數.
// 從時間串中提取出來年月日時分秒 static bool _stu_gettm(stime_t tstr, struct tm * otm) { int * py, * es; char c; int sum; if ((!tstr) || !(c = *tstr) || c < '0' || c > '9') return false; py = &otm->tm_year; es = &otm->tm_sec; sum = 0; while ((c = *tstr) && py >= es) { if (c >= '0' && c <= '9') { sum = 10 * sum + c - '0'; ++tstr; continue; } *py-- = sum; sum = 0; // 去掉特殊字元, 一直找到下一個數字 while ((c = *++tstr) && (c<'0' || c>'9')) ; } // 非法, 最後解析出錯 if (py != es) return false; *es = sum; // 保存最後秒數據 return true; } /* * 將 [2016-7-10 21:22:34] 格式字元串轉成時間戳 * tstr : 時間串分隔符只能是單位元組的. * pt : 返回得到的時間戳 * otm : 返回得到的時間結構體 * : 返回這個字元串轉成的時間戳, -1表示構造失敗 */ bool stu_gettime(stime_t tstr, time_t * pt, struct tm * otm) { time_t t; struct tm st; // 先解析年月日時分秒 if (!_stu_gettm(tstr, &st)) return false; st.tm_year -= _INT_YEAROFFSET; st.tm_mon -= _INT_MONOFFSET; // 得到時間戳, 失敗返回false if ((t = mktime(&st)) == -1) return false; // 返回最終結果 if (pt) *pt = t; if (otm) *otm = st; return true; }