聊聊时间(2)硬核算法篇——UNIX时间戳转UTC时间

文/Edward


前一篇文章《聊聊时间(1)UNIX时间戳和UTC时间》中,我们讲述了计算机中UNIX时间戳和UTC时间的基本概念,以及简要地阐述了诸如STM32F103之类芯片简单功能RTC的时间转换问题,这篇文章将会夹带慢慢的干货,详细地讨论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这种处理器来讲述的。

图1 溢出

  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所示。

图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所示。

图3 1970年开始的年份闰年平年分布

由于闰年每四年出现一次,因此我们由图3中可以得出一个简单方法,即可以从1790年开始,每四年组成一个集合,每一个集合的都是由1年闰年加上3年平年组成的,它们的时间都是相等的,即126230400秒。

因此这个月数的求解步骤就可以变成:

(1)计算从1970年开始到当前的UNIX时间戳为止,一共过了多少个“集合年(平年+闰年)”;

(2)计算出当前的UNIX时间戳位于本“集合年“的哪一年,这样就可以判断当年年份是平年还是闰年;

(3)判断了当前年份是平年还是闰年之后,就可以推算出2月份有多少天,然后可以根据上述的递归法,求解出当前位于某一月,某一天。

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

图4 计算当前年份代码

得出当前年份之后,我们就可以很容易使用“能被4整除且不能被100整除,或者能被400整除的年份是闰年”这一条规则算出当年年份是闰年还是平年。

接着,我们可以直接将当前年份剩余的时间戳结合平年还是闰年,查表计算出当前的月份。

最后代码如图5所示。

图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所示。

图6 转换结果

下一期,我们将会讲述UTC时间如何转换成UNIX时间戳,以及如何进行时区转换。

(0)

相关推荐

  • Python|判断闰年与平年

    问题描述从键盘输入一个年份,若该年是闰年,则输出"闰年",否则输出"平年".解决方案首先我们要知道闰年与平年的判断区别,这里可用if语句来进行假设,下面我们来编 ...

  • 什么是闰年 怎么判断

    闰年是什么,该如何判断一个年份是否是闰年呢? 闰年是历法中的一种年份计算方式,是为了平衡地球的公转时间与我们所使用年份中的关系,下面用excel判断一个年份是否是闰年. 打开一个excel" ...

  • 三年级数学第七单元知识点小结(口诀)附单元练习

    年月日,时分秒的学习非常贴近生活,结合日常生活的了解能更快掌握知识.这个单元的知识以识记内容为主,多了解多记忆,少量的计算也要仔细哦. 年月日 一.认识大小月 一年有12个月 大月每月31天,分别是1 ...

  • 【天文历法】平年、闰年怎么区分?平年、闰年怎么算?

    让知识回家 一站式收藏您的阅读与创作 [天文历法] 平年.闰年怎么区分?平年.闰年怎么算? 什么是平年,什么是闰年? 平年,在历法上指阳历没有闰日或农历没有闰月的年份.     闰年,公历名词.闰年分 ...

  • ​【博文精选】至简设计法_fpga万年历设计

    ​【博文精选】至简设计法_fpga万年历设计

  • 聊聊时间(1)——UNIX时间戳和UTC时间

    文/Edward 各位小伙伴们,大家好,本文是农历辛丑年本公众号所发表的第一篇文章.时光如梭,短短的一年又过去了,不禁时时感叹时间之匆匆.就好像朱自清说的"我的日子滴在时间的流里,没有声音, ...

  • 《羿知半解》第88篇时间是宇宙的法

    <羿知半解>第88篇时间是宇宙的法 时间是宇宙的法 文/生子 时间是一个神秘的概念,既有浮现在眼前的一刹那,又有宇宙斑斓的时空谜团.人们对于时间的认识是逐渐深化的.从授时到历法:从日出而作 ...

  • 聊聊时间(3)UTC时间转UNIX时间戳

    前一篇文章<聊聊时间(2)UNIX时间戳转UTC时间>中,我们详细地解释了如何将一个32位无符号的UNIX时间戳利用软件算法转换成我们可以看得懂的UTC时间.我们说,在STM32F10x系 ...

  • 现代灸法篇—— 内科疾病 43.肝 硬 化

    四十二 肝 硬 化 [概述] 肝硬化是肝脏损害为主的慢性全身性疾病.根据临床表现一般分为早期肝硬化和晚期肝硬化(肝腹水)两大类.早期表现为腹胀,乏力,食欲不振,恶心呕吐,上腹部不适或隐痛,面色微黄,面 ...

  • 【三味淑屋景淑反思日记第384篇】认识你的时间

    关注 这是三味淑屋原创文章 - 第804篇 - 星期四 天气晴 2021年5月6日 01. 今日小确幸 (1)感恩每天和一群有能量.积极正向.努力突破自我的星宝宝们一起开早会,一起分享日记,一起讨论问 ...

  • 第十章 八字风水择日初级修天嗣求子法篇

    第十章 修天嗣求子法 凡人家人丁不旺,必因本命天嗣男女星方失陷,又值龙气克泻之故也.宜择本年天嗣方.金匮方.红鸾天喜方,并取青龙生气,火星胜光,神后.功曹.传送,及太阳.太阴,而带本命天嗣男女主禄马贵 ...

  • 4、学会控制心态提升短线交易胜率——法篇(1)

    ----------------------------------------- 五一节前,我会陆续整理分享一些关于短线交易的方方面面的心得体会,便于自己整理回顾自己的知识体系,做一次提升.这是我每 ...

  • 第一篇SCI用了一个月,第二篇用了两个星期,第三篇用了3天时间

    本人是一名临床专业研究生(移植科),白天上班(周一到周五门诊),晚上熬夜搞科研,移植科天天忙都要命,完全都是挤时间做科研的,不信的话可以问一下移植科的朋友.我们毕业的要求就发一篇SCI即可,对期刊没有 ...

  • 5、学会控制心态提升短线交易胜率——法篇(2)

    ----------------------------------------- 五一节前,我会陆续整理分享一些关于短线交易的方方面面的心得体会,便于自己整理回顾自己的知识体系,做一次提升.这是我每 ...