这里选择从另外一个角度再次解析这部分代码,建议先阅读上面的博客内容:
-
/* converts gregorian date to seconds since 1970-01-01 00:00:00.
-
* assumes input in normal date format, i.e. 1980-12-31 23:59:59
-
* => year=1980, mon=12, day=31, hour=23, min=59, sec=59.
-
*
-
* [for the julian calendar (which was used in russia before 1917,
-
* britain & colonies before 1752, anywhere else before 1582,
-
* and is still in use by some communities) leave out the
-
* -year/100 year/400 terms, and add 10.]
-
*
-
* this algorithm was first published by gauss (i think).
-
*
-
* warning: this function will overflow on 2106-02-07 06:28:16 on
-
* machines where long is 32-bit! (however, as time_t is signed, we
-
* will already get problems at other places on 2038-01-19 03:14:08)
-
*/
-
unsigned long
-
mktime(const unsigned int year0, const unsigned int mon0,
-
const unsigned int day, const unsigned int hour,
-
const unsigned int min, const unsigned int sec)
-
{
-
unsigned int mon = mon0, year = year0;
-
/* 1..12 -> 11,12,1..10 */
-
if (0 >= (int) (mon -= 2)) {
-
mon = 12; /* puts feb last since it has leap day */
-
year -= 1;
-
}
-
return ((((unsigned long)
-
(year/4 - year/100 year/400 367*mon/12 day)
-
year*365 - 719499
-
)*24 hour /* now have hours */
-
)*60 min /* now have minutes */
-
)*60 sec; /* finally seconds */
-
}
这个函数的功能是获得1970年1月1日至今的秒数,我设这个数为totol_sec
首先需要获得公元元年1月1日至今的天数,我们设这个数为days
显然:
totol_sec = ((days * 24 hour) * 60 min) * 60 sec
后面的部分比较简单,难点在于获取days
设leapdays为公元0年到今天之前(不包含今天)的闰天数
设ydays为今年1月1日至今的天数
另: 公元0年到1970年1月1日的天数为719162天
因此:
days = leapdays (year0 - 1) * 365 ydays - 719162 (1)
为了更好的理解上面的源代码,我们引入变量mon, year, magic,其中mon和year都是源码中用到的变量
-
当mon0 > 2 时, mon = mon0 - 2, year = year0 (2a)
-
当mon0 <= 2 时, mon = mon0 10, year = year0 - 1 (2b)
-
magic = 367 * mon / 12
上面的(2a) (2b)等价于源码中的
-
/* 1..12 -> 11,12,1..10 */
-
if (0 >= (int) (mon -= 2)) {
-
mon = 12; /* puts feb last since it has leap day */
-
year -= 1;
-
}
遍历mon0(参见上面的博客内容)我们可以发现magic有如下的性质
-
当mon0 > 2 时, ydays = magic day 28 (3a)
-
当mon0 <= 2 时, ydays = magic day - 365 28 (3b)
同时,无论mon0为何值,year是否等于year0都有
leapdays = year/4 - year/100 year/400 (4)
结合(1)(2a)(3a)(4), 当mon0 > 2 时
-
days = year/4 - year/100 year/400 (year - 1) * 365
-
367 * mon / 12 day 28 - 719162
结合(1)(2b)(3b)(4), 当mon0 <= 2 时
-
days = year/4 - year/100 year/400 year * 365
-
367 * mon / 12 day - 365 28 - 719162
得到相同的等式:
-
days = (year/4 - year/100 year/400 367*mon/12 day)
-
year*365 - 719499
这也就是源码中获取经过天数的公式。
可以看到源码中最精彩最难以理解的部分就是
367*mon/12
我们再来看看这个算式的计算结果:
-
计算值
-
mon mon0 value
-
1 (3) 30
-
2 (4) 61
-
3 (5) 91
-
4 (6) 122
-
5 (7) 152
-
6 (8) 183
-
7 (9) 214
-
8 (10) 244
-
9 (11) 275
-
10 (12) 305
-
11 (1) 336
-
12 (2) 367
可以发现这个计算结果实际上隐含了一个信息就是,它恰好代表着之前月份天数的和month_day_in_year ,只是这个和是一个经过偏转的值即
value = month_day_in_year - 29
1月和2月比较特殊,因为他们被加上了12个月,也就是1年,他们的value实际上应该先减去365,也就是
value - 365 = month_day_in_year - 29
也就是说367 * mon / 12这个算式经过取整运算刚好能得出月份天数和的信息,这应该是一个数学上的巧合。
据说是高斯最先提出这种算法,实在是佩服这些天才。
我也参看了glibc相同功能的代码(参见glicb源码time/mktime.c),在glibc中是通过查表法来获得月份天数信息的
-
const unsigned short int __mon_yday[2][13] =
-
{
-
/* normal years. */
-
{ 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365 },
-
/* leap years. */
-
{ 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366 }
-
};
因为linux源码中已经处理了闰年的情况,所以我们只需要关注normal years的部分就行了。根据上面的分析倒算回去对比一下,其实是一样的。
从效率上来说,linux和glibc的mktime都是差不多的,但是linux版本的mktime不需要依靠全局变量。