前言
时间戳转日期是我们最常用的需求, 一般情况我们会采用系统提供的localtime或localtime_r来转换, 可是localtime是线程不安全的, 而且两个都是通过系统调用来实现的, 如果在大量调用的时候, 会导致整个系统性能低下, 这也就是为什么我们要通过数学的方法转换时间戳.
算法
现行的公历又称格里历, 分平年和闰年, 平年二月份28天, 闰年29年, 每四年闰一次, 百年不闰, 四百年再闰, 也就是说给出一个年份, 能被4整除, 就可能是闰年, 如果能被100整除, 同是不被400整除就是平年, 其余都是闰年(不能被4整除, 就一定不是闰年).
算法看上去很简单, 需要处理的就是百年的情况. 众所周知, 我们的unixtime是从1970年开始算, 那么到现在中间只有2000年这一个百年, 然而这个百年是可以被400整除的, 于是算法可以进一步简单的认为我们只需要考虑四年一个周期(2100年的事情就要后人去解决吧).
一些坑
unixtime 是从1970年开始算, 而1970、1971年是平年,1972年是闰年,也就是说我们按4整除求余之前, 应该先踢除前面三年(写之前网上搜了大量的资料, 基本上就没有对的, 这个地方完全就被漏掉了)
源码
// 时间戳转换
static unsigned short date_time_days[2][12] =
{
{ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},// 平年
{ 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},// 润年
};
struct date_time
{
unsigned int second; // 0-59
unsigned int minute; // 0-59
unsigned int hour; // 0-23
unsigned int day; // 1-31
unsigned int month; // 1-12
unsigned int year; // 1970 - 2100
unsigned long long timestamp;
date_time(unsigned long long unixtime)
{
timestamp = unixtime;
unixtime_to_date();
}
date_time(unsigned int syear, unsigned int smouth, unsigned int sday, unsigned int shour, unsigned int sminute, unsigned int ssecond)
{
year = syear;
month = smouth;
day = sday;
hour = shour;
minute = sminute;
second = ssecond;
date_to_unixtime();
}
void unixtime_to_date()
{
second = timestamp % 60; timestamp /= 60;
minute = timestamp % 60; timestamp /= 60;
hour = timestamp % 24; timestamp /= 24;
year = 1970;
// 1970 1971 是平年
for (unsigned int i = 0; i < 2; ++i)
{
if (timestamp >= 365)
{
++year;
timestamp -= 365;
}
else
{
for(unsigned int j = 0; j < 12; ++j)
{
month = j + 1;
if (timestamp >= date_time_days[0][j])
{
timestamp -= date_time_days[0][j];
}
else
{
day = timestamp + 1;
return;
}
}
}
}
// 1972 闰年
if (timestamp >= 366)
{
++year;
timestamp -= 366;
}
else
{
for(unsigned int j = 0; j < 12; ++j)
{
month = j + 1;
if (timestamp >= date_time_days[1][j])
{
timestamp -= date_time_days[1][j];
}
else
{
day = timestamp + 1;
return;
}
}
}
// 中间四年一个阶段
unsigned int years = timestamp/(365*4+1)*4; timestamp %= 365*4+1;
year += years;
for (unsigned int i = 0; i < 3; ++i)
{
if (timestamp >= 365)
{
++year;
timestamp -= 365;
}
else
{
for(unsigned int j = 0; j < 12; ++j)
{
month = j + 1;
if (timestamp >= date_time_days[0][j])
{
timestamp -= date_time_days[0][j];
}
else
{
day = timestamp + 1;
return;
}
}
}
}
for(unsigned int j = 0; j < 12; ++j)
{
month = j + 1;
if (timestamp >= date_time_days[1][j])
{
timestamp -= date_time_days[1][j];
}
else
{
day = timestamp + 1;
return;
}
}
}
void date_to_unixtime()
{
timestamp = second; // 0-59
unsigned int sminute = minute; // 0-59
unsigned int shour = hour; // 0-23
unsigned int sday = day-1; // 0-30
unsigned int smonth = month-1; // 0-11
unsigned int syear = year - 1;
timestamp += (sminute * 60 + shour * (60 * 60) + sday * (24 * 60 * 60));
int isfour = ((syear + 1)% 4 == 0 ? 1 : 0);
for( unsigned int i = 0; i < smonth; ++i )
{
timestamp += (date_time_days[isfour][i] * (24 * 60 * 60));
}
if (syear >= 1970)
{
timestamp += (365 * 24 * 60 * 60);
}
else
{
return ;
}
if (syear >= 1971)
{
timestamp += (365 * 24 * 60 * 60);
}
else
{
return ;
}
if (syear >= 1972)
{
timestamp += (366 * 24 * 60 * 60);
}
else
{
return ;
}
timestamp += (((syear - 1972) / 4)*(365 * 4 + 1) + ((syear - 1972) % 4) * 365) * 24 * 60 * 60;
}
};
后语
一个会写代码的人, 基本功也是很结实的, 这些小算法特别考验基本功. 业务代码写多了, 对这种基本功的考验往往不屑一顾, 我们应该不忘初心.
补充一个问题
上面的代码用的是标准时区进行计算的, 但实际上我们更多的时候用的是东八区(准确的说应该叫北京时间, 跟国家相关的), 所以我们会先 + 8小时, 然后进行计算。实际这样是不准的, 会有夏时令的问题。如果把这个直接弄成东八区的, 那就得特殊处理夏时令问题.
欢迎大家订阅雀观代码, 我将给你讲述, 中小企业程序员, 淘金路上的故事.
日期/时区 处理是个超级大坑,能用系统的,还是尽量用系统的。