这篇文章将会夹带慢慢的干货,详细地讨论UTC时间和Linux时间戳转换的算法,这个经典的算法必将使你有一天会受用。  

1  32位的UNIX时间戳  


我们之前的文章有介绍过UNIX操作系统是伴随着C语言一起诞生的,其诞生的时间大致是在20世纪70年代左右,当时的计算机都是以8位,16位的数据宽度去处理数据的,因此在当时那个年代,找一个32位的寄存器来存储时间,是一件比较奢侈的事情。
  
最早的UNIX时间戳就只是用一个32位的寄存器来存储的。而UNIX时间戳规定,UTC/GMT的1970年1月1日0时0分0秒为UNIX时间戳的起始计数时刻,即UNIX时间戳从这个时间点开始技术。由于使用的变量是一个32位的二进制数,因此这个计数器的最大计数长度为2^32=4,294,967,296s,所以在UNIX时间戳的诞生之初,它最大时间只能累积计数到2038年左右。
  
但是,这是不是说到了2038年这个计数器溢出之后会发生类似“千年虫”之类的危机呢?这个问题,大家无需杞人忧天,因为现在的桌面和移动处理器早已经跨入64位CPU时代了,这个UNIX时间戳计数器的长度也早已经被扩充到64位。而我们今天使用32位的UNIX时间戳计数器,完全是为了类似STM32这种处理器来讲述的。

2  UNIX时间戳转UTC时间
2.1 时分秒的转换
  
前面我们已经说过,UNIX时间戳是以UTC/GMT时间的1970年1月1日0时0分0秒为起始时刻进行计数的。考虑到这个UNIX时间戳的计数器的数据长度为32位,我们不妨先将这个UNIX时间戳计数器定义为一个变量,其名称为“u32UnixTimeStamp”。
  
首先,基于上述的阐述,我们思考下,当这个1970年1月1日0时0分0秒时刻过了1s,那么“u32UnixTimeStamp”变量的值也会自加1,变成1。
  
随着时间的流逝,“u32UnixTimeStamp”的值不断地自加,当“u32UnixTimeStamp”变成60的时候,我们回过头来再看看UTC时间,由于此时已经过了60s,因此UTC时间的秒数值将会溢出被清掉,分数值将会被置1。这个时候UTC的时间变成了1970年1月1日0时1分0秒.
  
再随着时间的流逝,当“u32UnixTimeStamp”的值变成3600的时候,此时1个小时已经悄然流逝,这个时候UTC的时间变成了1970年1月1日1时0分0秒。
  
86400秒流逝之后,“u32UnixTimeStamp”的值变成86400,此时正好24个小时过去,此时UTC时间变成了1970年1月2日0时0分0秒。
  
其实,当UNIX时间戳的计数值小于86400的时候,我们很容易就能写出转换成UTC时间的程序,因为小时数就是“u32UnixTimeStamp”对3200取模,分数就是将不能凑满小时的“u32UnixTimeStamp”对60取模,剩余不能凑满分数的“u32UnixTimeStamp”即为当前时间的秒数,具体代码如图2所示。
ANNN7n.png
图2 计算一天之内时分秒的代码
  1. #include <stdio.h>
  2. #include "timex_test1.h"
  3. utc_t UtcTime;
  4. int main(void)
  5. {
  6. int retVal;
  7. int u32UnixTimeStamp = 0;
  8. int hour, minute, sec;
  9. printf("input the UNIX time stamp:");
  10. scanf("%d", &u32UnixTimeStamp);
  11. if(u32UnixTimeStamp >= 86400)
  12. {
  13. printf("input out of range!\n");
  14. retVal = 1;
  15. }
  16. else
  17. {
  18. hour = u32UnixTimeStamp / 3600; //计算小时
  19. minute = (u32UnixTimeStamp - hour * 3600) / 60; //计算分
  20. sec = (u32UnixTimeStamp - hour * 3600 - minute * 60); //计算秒
  21. UtcTime.year = 1970;
  22. UtcTime.month = 1;
  23. UtcTime.date = 1;
  24. UtcTime.hour = 0 + hour;
  25. UtcTime.minute = 0 + minute;
  26. UtcTime.second = 0 + sec;
  27. printf("\nUTC time is : %dy - %dm - %dd\n", UtcTime.year, UtcTime.month, UtcTime.date);
  28. printf("\nUTC time is : %dh - %dm : %ds\n", UtcTime.hour, UtcTime.minute, UtcTime.second);
  29. retVal = 0;
  30. }
  31. return retVal;
  32. }
  1. #ifndef __TIMEX_H_
  2. #define __TIMEX_H_
  3. /*定义UTC时间结构体类型*/
  4. typedef struct
  5. {
  6. int year;
  7. int month;
  8. int date;
  9. int hour;
  10. int minute;
  11. int second;
  12. } utc_t;
  13. #endif
以上的代码是计算“u32UnixTimeStamp”小于86400时当前UTC时间的时分秒代码。其实,细心的读者可能会发现,这个86400数值代表的即是一天之内的UTC时分秒,那么如果当前的“u32UnixTimeStamp”计数值大于86400,我们该怎么处理呢?  


我们先来从最简单的情况来推导。如果一个“u32UnixTimeStamp”为86401,那么此时我们可以知道,这个UNIX时间戳其实就是86400 + 1,已知当“u32UnixTimeStamp”为86400时,UTC时间将会变成1970年1月2日0时0分0秒,那么此时86401即为1970年1月2日0时0分0秒 + 1秒,即1970年1月2日0时0分1秒。由这个简单的推导,我们又可以将大于86400的数值拉到0~86400的范围之内了,同时,还可以得到一个和日期相关的“原子”单位,即“日”。

2.2 年月日的转换
  
接下来,我们将要再对“日”的上一层单位进行讨论,即“月”数值,这也将是这个程序最为复杂的一部分内容。
  
这个复杂点主要体现在两个方面:
        (1)每个月的天数不等。众所周知,一年中每个月的天数都是不同的,1,3,5,7,8,10,12为大月,一个月有31天;4,6,9,11为小月,一个月有30天。
        (2)闰年平年的影响。由于公历的偏差,导致了一年中最为特殊的一个月份2月,当此年为闰年时,2月份有29天,此年为平年时,2月份有28天。
  
上面两个原因,导致了年月日计算的复杂性。
  
但是,困难只是表面上的,我们仔细思考下,就很容易得出规律。这个规律的突破口即为闰年出现的时间,因为闰年每四年出现一次,那么我们可以列出从1970年开始的几个年份。如图3所示。
iu6Nza.png
图3 1970年开始的年份闰年平年分布  


由于闰年每四年出现一次,因此我们由图3中可以得出一个简单方法,即可以从1790年开始,每四年组成一个集合,每一个集合的都是由1年闰年加上3年平年组成的,它们的时间都是相等的,即126230400秒。
  
因此这个月数的求解步骤就可以变成:
        (1)计算从1970年开始到当前的UNIX时间戳为止,一共过了多少个“集合年(平年+闰年)”;
        (2)计算出当前的UNIX时间戳位于本“集合年“的哪一年,这样就可以判断当年年份是平年还是闰年;
        (3)判断了当前年份是平年还是闰年之后,就可以推算出2月份有多少天,然后可以根据上述的递归法,求解出当前位于某一月,某一天。

首先,我们先来写代码,求出当前的年份。如图4所示。  
AfuMfa.png
图4 计算当前年份代码

得出当前年份之后,我们就可以很容易使用“能被4整除且不能被100整除,或者能被400整除的年份是闰年”这一条规则算出当年年份是闰年还是平年。
  
接着,我们可以直接将当前年份剩余的时间戳结合平年还是闰年,查表计算出当前的月份。
  
最后代码如图5所示。

      2emYNr.png       
图5 UNIX时间戳转UTC时间代码
  1. #include <stdio.h>
  2. #include "timex_test2.h"
  3. utc_t UtcTime;
  4. int main(void)
  5. {
  6. int retVal;
  7. int u32UnixTimeStamp = 0;
  8. int hour, minute, sec;
  9. int flat_year_month_day[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
  10. int leap_year_month_day[13] = {0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
  11. int year_temp = 0;
  12. int day_temp = 0;
  13. unsigned int cnt_temp = 0;
  14. int is_leap_or_flat_year;
  15. int i;
  16. printf("input the UNIX time stamp:");
  17. scanf("%d", &u32UnixTimeStamp);
  18. cnt_temp = u32UnixTimeStamp;
  19. /*判断当前UNIX时间戳经过了多少个4年*/
  20. while(cnt_temp >= 126230400)
  21. {
  22. year_temp ++;
  23. cnt_temp = cnt_temp - 126230400;
  24. }
  25. /*计算出当前的4年周期起始年份*/
  26. UtcTime.year = UNIX_TIME_YEAR + (4 * year_temp);
  27. /*计算出当前的年份*/
  28.   /*这部分代码可使用循环做精简,为了直观,我将其写开*/
  29. if(cnt_temp >= 31536000)
  30. {
  31. UtcTime.year ++;
  32. cnt_temp -= 31536000;
  33. /*Flat year*/
  34. if(cnt_temp >= 31536000)
  35. {
  36. UtcTime.year ++;
  37. cnt_temp -= 31536000;
  38. /*Leap year*/
  39. if(cnt_temp >= 31622400)
  40. {
  41. UtcTime.year ++;
  42. cnt_temp -= 31622400;
  43. /*Flat year*/
  44. if(cnt_temp >= 31536000)
  45. {
  46. UtcTime.year ++;
  47. cnt_temp -= 31536000;
  48. }
  49. }
  50. }
  51. }
  52. /*计算当前年份是平年还是闰年*/
  53. if((((UtcTime.year % 4) == 0) && ((UtcTime.year % 100) != 0)) || ((UtcTime.year % 400) == 0))
  54. {
  55. is_leap_or_flat_year = LEAP_YEAR;
  56. }
  57. else
  58. {
  59. is_leap_or_flat_year = FLAT_YEAR;
  60. }
  61. /*计算出不足一年剩余的天数*/
  62. day_temp = cnt_temp / 86400;
  63. /*剩余不足86400s的时间戳,计算出时间*/
  64. UtcTime.hour = (cnt_temp - day_temp * 86400) / 3600; //Calculate hours
  65. UtcTime.minute = (cnt_temp - day_temp * 86400 - UtcTime.hour * 3600) / 60; //Calculate minutes
  66. UtcTime.second = cnt_temp % 60;
  67. /*将天数结合平年还是闰年查表计算出当前的月份*/
  68. UtcTime.month = 1;
  69. for(i = 0; i < 12; i ++)
  70. {
  71. if(is_leap_or_flat_year == FLAT_YEAR)
  72. {
  73. if(day_temp >= flat_year_month_day[i + 1])
  74. {
  75. UtcTime.month ++;
  76. day_temp -= flat_year_month_day[i + 1];
  77. }
  78. }
  79. else if(is_leap_or_flat_year == LEAP_YEAR)
  80. {
  81. if(day_temp >= leap_year_month_day[i + 1])
  82. {
  83. UtcTime.month ++;
  84. day_temp -= leap_year_month_day[i + 1];
  85. }
  86. }
  87. }
  88. /*由于天数从1开始,因此需要加1*/
  89. UtcTime.date = day_temp + 1;
  90. printf("\nTime transform successfully\n");
  91. printf("++++++++++++++++++++++++++++++++++\n");
  92. printf("\nUTC time is : %dy - %dm - %dd\n", UtcTime.year, UtcTime.month, UtcTime.date);
  93. printf("\nUTC time is : %dh - %dm : %ds\n", UtcTime.hour, UtcTime.minute, UtcTime.second);
  94. printf("++++++++++++++++++++++++++++++++++\n");
  95. printf("\n");
  96. printf("\n");
  97. return 0;
  98. }
  1. #ifndef __TIMEX_H_
  2. #define __TIMEX_H_
  3. /*定义UTC时间结构体类型*/
  4. typedef struct
  5. {
  6. int year;
  7. int month;
  8. int date;
  9. int hour;
  10. int minute;
  11. int second;
  12. } utc_t;
  13. /*定义UNIX时间戳的起始UNIX时间*/
  14. #define UNIX_TIME_YEAR 1970
  15. #define UNIX_TIME_MONTH 1
  16. #define UNIX_TIME_DATE 1
  17. #define UNIX_TIME_HOUR 0
  18. #define UNIX_TIME_MINIUTE 0
  19. #define UNIX_TIME_SECOND 0
  20. #define LEAP_YEAR 1
  21. #define FLAT_YEAR 0
  22. #endif
最后,我们可以使用在线工具来验证转换结果是否正确,可以参考下列链接的UNIX时间戳在线转换工具,链接:https://tool.lu/timestamp/
  
假设我们同时输入UNIX时间戳1615906780,可以得出转换的北京时间为2021-03-16 22:59:40,而我们程序转换得出的时间为2021-03-16 14:59:40,这两个结果看似有出入,其实是一致的,我们知道北京时间是东8区的时间,而UTC时间是0时区的时间,因此相差8小时。如图6所示。
7RZJ7n.png
图6 转换结果

本文源自微信公众号:乐创客,不代表用户或本站观点,如有侵权,请联系nick.zong@aspencore.com 删除!