这篇文章将会夹带慢慢的干货,详细地讨论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 计算一天之内时分秒的代码
#include <stdio.h>
  • #include "timex_test1.h"
  • utc_t UtcTime;
  • int main(void)
  • {
  • int retVal;
  • int u32UnixTimeStamp = 0;
  • int hour, minute, sec;
  • printf("input the UNIX time stamp:");
  • scanf("%d", &u32UnixTimeStamp);
  • if(u32UnixTimeStamp >= 86400)
  • {
  • printf("input out of range!\n");
  • retVal = 1;
  • }
  • else
  • {
  • hour = u32UnixTimeStamp / 3600; //计算小时
  • minute = (u32UnixTimeStamp - hour * 3600) / 60; //计算分
  • sec = (u32UnixTimeStamp - hour * 3600 - minute * 60); //计算秒
  • UtcTime.year = 1970;
  • UtcTime.month = 1;
  • UtcTime.date = 1;
  • UtcTime.hour = 0 + hour;
  • UtcTime.minute = 0 + minute;
  • UtcTime.second = 0 + sec;
  • printf("\nUTC time is : %dy - %dm - %dd\n", UtcTime.year, UtcTime.month, UtcTime.date);
  • printf("\nUTC time is : %dh - %dm : %ds\n", UtcTime.hour, UtcTime.minute, UtcTime.second);
  • retVal = 0;
  • }
  • return retVal;
  • }
  • 复制代码
    #ifndef __TIMEX_H_
  • #define __TIMEX_H_
  • /*定义UTC时间结构体类型*/
  • typedef struct
  • {
  • int year;
  • int month;
  • int date;
  • int hour;
  • int minute;
  • int second;
  • } utc_t;
  • #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时间代码
    #include <stdio.h>
  • #include "timex_test2.h"
  • utc_t UtcTime;
  • int main(void)
  • {
  • int retVal;
  • int u32UnixTimeStamp = 0;
  • int hour, minute, sec;
  • int flat_year_month_day[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
  • int leap_year_month_day[13] = {0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
  • int year_temp = 0;
  • int day_temp = 0;
  • unsigned int cnt_temp = 0;
  • int is_leap_or_flat_year;
  • int i;
  • printf("input the UNIX time stamp:");
  • scanf("%d", &u32UnixTimeStamp);
  • cnt_temp = u32UnixTimeStamp;
  • /*判断当前UNIX时间戳经过了多少个4年*/
  • while(cnt_temp >= 126230400)
  • {
  • year_temp ++;
  • cnt_temp = cnt_temp - 126230400;
  • }
  • /*计算出当前的4年周期起始年份*/
  • UtcTime.year = UNIX_TIME_YEAR + (4 * year_temp);
  • /*计算出当前的年份*/
  •   /*这部分代码可使用循环做精简,为了直观,我将其写开*/
  • if(cnt_temp >= 31536000)
  • {
  • UtcTime.year ++;
  • cnt_temp -= 31536000;
  • /*Flat year*/
  • if(cnt_temp >= 31536000)
  • {
  • UtcTime.year ++;
  • cnt_temp -= 31536000;
  • /*Leap year*/
  • if(cnt_temp >= 31622400)
  • {
  • UtcTime.year ++;
  • cnt_temp -= 31622400;
  • /*Flat year*/
  • if(cnt_temp >= 31536000)
  • {
  • UtcTime.year ++;
  • cnt_temp -= 31536000;
  • }
  • }
  • }
  • }
  • /*计算当前年份是平年还是闰年*/
  • if((((UtcTime.year % 4) == 0) && ((UtcTime.year % 100) != 0)) || ((UtcTime.year % 400) == 0))
  • {
  • is_leap_or_flat_year = LEAP_YEAR;
  • }
  • else
  • {
  • is_leap_or_flat_year = FLAT_YEAR;
  • }
  • /*计算出不足一年剩余的天数*/
  • day_temp = cnt_temp / 86400;
  • /*剩余不足86400s的时间戳,计算出时间*/
  • UtcTime.hour = (cnt_temp - day_temp * 86400) / 3600; //Calculate hours
  • UtcTime.minute = (cnt_temp - day_temp * 86400 - UtcTime.hour * 3600) / 60; //Calculate minutes
  • UtcTime.second = cnt_temp % 60;
  • /*将天数结合平年还是闰年查表计算出当前的月份*/
  • UtcTime.month = 1;
  • for(i = 0; i < 12; i ++)
  • {
  • if(is_leap_or_flat_year == FLAT_YEAR)
  • {
  • if(day_temp >= flat_year_month_day[i + 1])
  • {
  • UtcTime.month ++;
  • day_temp -= flat_year_month_day[i + 1];
  • }
  • }
  • else if(is_leap_or_flat_year == LEAP_YEAR)
  • {
  • if(day_temp >= leap_year_month_day[i + 1])
  • {
  • UtcTime.month ++;
  • day_temp -= leap_year_month_day[i + 1];
  • }
  • }
  • }
  • /*由于天数从1开始,因此需要加1*/
  • UtcTime.date = day_temp + 1;
  • printf("\nTime transform successfully\n");
  • printf("++++++++++++++++++++++++++++++++++\n");
  • printf("\nUTC time is : %dy - %dm - %dd\n", UtcTime.year, UtcTime.month, UtcTime.date);
  • printf("\nUTC time is : %dh - %dm : %ds\n", UtcTime.hour, UtcTime.minute, UtcTime.second);
  • printf("++++++++++++++++++++++++++++++++++\n");
  • printf("\n");
  • printf("\n");
  • return 0;
  • }
  • 复制代码
    #ifndef __TIMEX_H_
  • #define __TIMEX_H_
  • /*定义UTC时间结构体类型*/
  • typedef struct
  • {
  • int year;
  • int month;
  • int date;
  • int hour;
  • int minute;
  • int second;
  • } utc_t;
  • /*定义UNIX时间戳的起始UNIX时间*/
  • #define UNIX_TIME_YEAR 1970
  • #define UNIX_TIME_MONTH 1
  • #define UNIX_TIME_DATE 1
  • #define UNIX_TIME_HOUR 0
  • #define UNIX_TIME_MINIUTE 0
  • #define UNIX_TIME_SECOND 0
  • #define LEAP_YEAR 1
  • #define FLAT_YEAR 0
  • #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 删除!