一文搞懂计算机世界的时间问题

634 阅读8分钟

前言

协调世界时(UTC)、格林威治时间(GMT)、儒略日(Julian Day)、时区(Timezone)、1970-01-01、1601-01-01......

大事记

时间大事件发明人说明诞生的新名词
公元前4713年1月1日12:00:00Julian day 的起点法国学者 Joseph Justus Scliger 在1583年所创为了纪念他的父亲,意大利学者 Julius Caesar ScaligerJulianDay
公元前45年1月1日Julian calendar(儒略历) 颁布Julius Caesar(时任罗马共和国独裁官)JulianCalendar
1582年10月15日00:00:00废除 Julian calendar,改用 Gregorian calendar(格里历),也就是我们说的公历。Pope Gregory XIII(时任罗马教皇格里高利十三世)1582年的10月份少了10天,从10月4日直接跳到15日,因旧历算法有偏差强行加上10天公历
1601年1月1日00:00:00FILETIME 起点微软程序员以400年为一周期,1601年至2000年为一周期,Windows NT诞生于该周期内,它也是17世纪的第一天。FILETIME
1884年以英国格林威治天文台的经线为本初子午线,即0度经线大英帝国日不落帝国为了海上霸权的扩张计划,早在十七世纪就开始进行天体观测GMT
1970年1月1日00:00:00Unix Epoch Time(UNIX 纪元时间、UNIX 时间戳)UNIX 程序员UNIX 年诞生于1970年前后UnixTimestamp、EpochTime
1972年UTC正式成为国际标准时间(又称协调世界时、世界标准时间)国际无线电咨询委员会原子钟UTC、ZuluTime
  • GMT和UTC区别:GMT是前世界标准时,UTC是现世界标准时。

    • GMT: Greenwich Mean Time(格林威治标准时间),它规定太阳每天经过位于英国伦敦郊区的皇家格林威治天文台的时间为中午12点。
    • UTC: 采用原子钟,在精度方面碾压以天文观测为主的GMT。
    • 实际编程中,这两个时间值是一样的

时间函数

C语言

 // 返回从 1970-01-01 00:00:00 +00:00 开始至今的秒数(以下简称UNIX时间戳)
 time_t time(time_t *seconds);
 ​
 struct tm {
     int tm_sec;         // 秒,范围从 0 到 59
     int tm_min;         // 分,范围从 0 到 59
     int tm_hour;        // 小时,范围从 0 到 23
     int tm_mday;        // 一月中的第几天,范围从 1 到 31
     int tm_mon;         // 月份,范围从 0 到 11
     int tm_year;        // 自 1900 起的年数
     int tm_wday;        // 一周中的第几天,范围从 0 到 6
     int tm_yday;        // 一年中的第几天,范围从 0 到 365
     int tm_isdst;       // 夏令时
 };
 ​
 // UNIX时间戳 -> 本地时间结构体
 struct tm* localtime(time_t const* const sourceTime);
 // 本地时间结构体 -> UNIX时间戳
 time_t mktime(struct tm *timeptr);
 ​
 // UNIX时间戳 -> 格林威治标准时间(UTC+00:00)
 struct tm *gmtime(const time_t *sourceTime);
 ​
 // 时间结构体 -> ASC时间字符串
 // 示例:Mon Jun  3 06:08:39 2024
 char *asctime(const struct tm *timeptr);
 ​
 // 时间戳 -> 本地时区下的ASC时间字符串
 // 示例:Mon Jun  3 14:08:39 2024
 char *ctime(const time_t *timer);
  • 为什么tm_year是从1900开始而不是1970

    • 计算机早期使用2位数存储年份,1980年存储为80,因此,相对于1900,将tm_year直接打印成两位数就十分便利,显然在当时是合理的。
    • 后来到了2000年,很多只能处理2位数年限的计算机就出现了千年虫问题(Y2K)因为年限又从00年重新开始了。

Windows API

 typedef struct _SYSTEMTIME {  
                             WORD wYear;         // 年,范围从 1601 到 30827 
                             WORD wMonth;        // 月,范围从 1 到 12
                             WORD wDayOfWeek;    // 一周中的第几天,范围从 0 到 6
                             WORD wDay;          // 日,范围从 1 到 31
                             WORD wHour;         // 时,范围从 0 到 23
                             WORD wMinute;       // 分,范围从 0 到 59
                             WORD wSecond;       // 秒,范围从 0 到 59
                             WORD wMilliseconds; // 毫秒,范围从 0 到 999
 } SYSTEMTIME, *PSYSTEMTIME;
 ​
 // 自 1601年1月1日UTC+00:00 以来的 100 纳秒间隔数
 typedef struct _FILETIME {  
                           DWORD dwLowDateTime;  
                           DWORD dwHighDateTime;  
 } FILETIME, *PFILETIME;
 ​
 // SYSTEMTIME
 void GetSystemTime(LPSYSTEMTIME lpSystemTime); // UTC+00:00
 void GetLocalTime(LPSYSTEMTIME lpSystemTime); // 本地时间结构体
 ​
 // FILETIME
 void GetSystemTimeAsFileTime(LPFILETIME); // UTC+00:00时间
 BOOL FileTimeToLocalFileTime(const FILETIME*, LPFILETIME); // UTC+00:00时间 -> 本地时间
 BOOL LocalFileTimeToFileTime(const FILETIME*, LPFILETIME); // 本地时间 -> UTC+00:00时间
 ​
 // SYSTEMTIME <--> FILETIME
 BOOL SystemTimeToFileTime(const SYSTEMTIME*, LPFILETIME); // 时间结构体 -> FILETIME
 BOOL FileTimeToSystemTime(const FILETIME*, LPSYSTEMTIME); // FILETIME -> 时间结构体
 ​
 class CFileTime : public FILETIME {
 public:
     static CFileTime GetCurrentTime(); // UTC+00:00时间
 };
 ​
 class CTime {
 public:
     CTime(const FILETIME&); // FILETIME -> UNIX时间戳(精度:秒)
     static CTime GetCurrentTime(); // UNIX时间戳(精度:秒)
     struct tm* GetGmtTm(struct tm* ptm) const; // UNIX时间戳 -> 格林威治标准时间(UTC+00:00)
     struct tm* GetLocalTm(struct tm* ptm) const; // UNIX时间戳 -> 本地时间结构体
     bool GetAsSystemTime(SYSTEMTIME& st) const; // UNIX时间戳 -> 本地时间结构体
     
     // 本地时间:年月月时分秒
     int GetYear() const throw();
     int GetMonth() const throw();
     int GetDay() const throw();
     int GetHour() const throw();
     int GetMinute() const throw();
     int GetSecond() const throw();
     
 private:
     __time64_t m_time; // UNIX时间戳(精度:秒)
 };

POCO

 class Poco::Timestamp {
 public:
     std::time_t epochTime() const(); // UNIX时间戳(精度:秒)
     INT64 utcTime() const; // 1582年10月15日00:00:00至今的100纳秒个数(即公历第一天至今)
     INT64 epochMicroseconds() const; // UNIX时间戳(精度:微秒)
     
     // FILETIME <--> Poco::Timestamp
     static Timestamp fromFileTimeNP(UInt32 fileTimeLow, UInt32 fileTimeHigh);
     void toFileTimeNP(UInt32& fileTimeLow, UInt32& fileTimeHigh) const;
     
 private:
     INT64 _ts; // UNIX时间戳(精度:微秒)
 };
 ​
 class Poco::DateTime {
 public:
     // 想要表示1582-10-15 00:00:00之前的时间,可以用Julian day表示,正常年月日表示的话,计算结果都是错的
     double julianDay() const; // 当前时间距离Julian day相差的总天数(公元前4713年1月1日12:00:00)
     
 private:
     INT64 _utcTime; // 1582-10-15 00:00:00至当前时间所经历的100纳秒个数
     short _year;
     short _month;
     short _day;
     short _hour; // UTC+00:00
     short _minute;
     short _second;
     short _millisecond;
     short _microsecond;
 };
 ​
 class Poco::LocalDateTime {
 private:
     DateTime _dateTime; // 本地时间
     int      _tzd; // 与UTC+00:00相差的秒数(如中国是28800=8*3600)
 };

常量

116444736000000000 的由来:

 // 1601-01-01 00:00:00 到 1970-01-01 00:00:00,共经历了多少个100纳秒
 // diff.QuadPart = 116444736000000000
 ULARGE_INTEGER diff;
 diff.LowPart  = 0xD53E8000;
 diff.HighPart = 0x019DB1DE;

122192928000000000 的由来:

 // 1582-10-15 00:00:00 到 1970-01-01 00:00:00,共经历了多少个100纳秒
 // diff.QuadPart = 122192928000000000
 ULARGE_INTEGER diff;
 diff.LowPart  = 0x01B21DD2;
 diff.HighPart = 0x13814000;

2299160.5 的由来

 // 公元前4713-01-01 12:00:00 到 1582-10-15 00:00:00,共经历了多少天
 // 即 Julian day 第一天,到公历第一天,共经历了多少天
 ​
 // 计算公式:总天数 = int(365.25 * (y + 4712)) + int(30.61 * (m + 1)) + d - 63.5
 double calculate(int y, int m, int d)
 {
     double ret = int(365.25 * (y + 4712)) + int(30.61 * (m + 1)) + d - 63.5;
     return ret;
 }
 ​
 int main() {
     auto days = calculate(1582, 10, 5); // days = 2299160.5
 }

86400000 的由来

 // 一天24小时的总毫秒数:
 assert(86400, 24 * 3600 * 1000);

时间格式化

ISO 8601

格式:YYYY-MM-DDTHH:MM:SS±HH:MM

示例:

  • 2023-06-04T14:23:45Z (UTC时间)
  • 2023-06-04T16:23:45+02:00 (UTC+2)

RFC 822

已被 RFC 1123 取代

格式:Day, D Mon YY HH:MM:SS GMT

  • Day取值:Mon/Tue/Wed/Thu/Fri/Sat/Sun
  • Mon取值:Jan/Feb/Mar/Apr/May/Jun/Jul/Aug/Sep/Oct/Nov/Dec

示例:

  • Sat, 1 Jan 05 12:00:00 +0100
  • Sat, 1 Jan 05 11:00:00 GMT

RFC 1123

取代 RFC 822,年份改用4位数字

示例:

  • Sat, 1 Jan 2005 12:00:00 +0100
  • Sat, 1 Jan 2005 11:00:00 GMT

RFC 2616

与 RFC 1123 相近,天数不足2位补0,广泛应用于 HTTP 协议中

示例:

  • Sat, 01 Jan 2005 12:00:00 +0100
  • Sat, 01 Jan 2005 11:00:00 GMT

RFC 850

已被 RFC 1036 取代

示例:

  • Saturday, 1-Jan-05 12:00:00 +0100
  • Saturday, 1-Jan-05 11:00:00 GMT

RFC 1036

示例:

  • Saturday, 1 Jan 05 12:00:00 +0100
  • Saturday, 1 Jan 05 11:00:00 GMT

asctime

示例:

  • Sat Jan 1 12:00:00 2005