Linux内核mktime64实现简洁性:巧合还是刻意设计?
说明:此前曾发布此内容但未符合平台规范。
偶然发现Linux内核中用于将公历日期转换为Unix时间戳(1970/01/01 00:00:00 UTC以来的秒数)的mktime64实现,惊讶于其代码行数极少,因此希望理解该公式的工作原理及设计逻辑。主要有以下疑问:
- 魔法数367和719499的来源是什么?
- 月、日参数为何采用1索引?例如2012年3月3日表示为(2012, 3, 3)
以下是代码片段:
time64_t mktime64(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 ((((time64_t) (year/4 - year/100 + year/400 + 367*mon/12 + day) + year*365 - 719499 )*24 + hour /* now have hours - midnight tomorrow handled here */ )*60 + min /* now have minutes */ )*60 + sec; /* finally seconds */ }
该实现可表示为以下公式:
其中日历日期偏移了-2个月。
本文将尝试解释公式中各术语的含义,说明该函数的假设与限制,并推导这些魔法常量的来源。为简化表述与提升可读性,后续代码片段均采用Python编写。
直观部分:已知时间及1970年1月1日以来天数求时间戳
非直观部分:已知日历日期求1970年1月1日以来天数
代码中的条件块将计算基准年的起始日调整为3月1日,因为这一天是2月闰日的次日。这会将闰日计算的基准日期从0000/01/01偏移到0000/03/01,而这一偏移已被魔法数719499抵消。
这使得我们可以通过简单的整数除法计算自公元0年(公元前1年)以来公历的闰日数:
这与代码中的year/4 - year/100 + year/400等价,因为C语言中的整数除法为向下取整。
我们先定义一个计算自公历0000/03/01以来天数的公式,其中年份和月份已通过上述条件代码调整:
若我们将Unix时间戳基准日期(1970, 1, 1)通过上述条件代码调整为(1969, 11, 1)并代入公式,即可得到第一个魔法数的来源:
因此:
其中年份和月份已偏移-2个月。
“简化”原函数
我们用Python重写原函数,并将公式中直观与复杂的部分拆分为独立函数。
def days_since_gregorian_y0(year, month, day): if (0 >= (month := month-2)): month += 12 year -= 1 return ( (year//4 - year//100 + year//400) + # 自0000/03/01以来的闰日数 (367 * month) // 12 + day + # 以03-01为基准的年内天数(?) year * 365 # 每年的天数 ) def mktime64(year, month, day, hour, minute, second): return ( ( ( days_since_gregorian_y0(year, month, day) - days_since_gregorian_y0(1970, 1, 1) ) * 24 + hour ) * 60 + minute ) * 60 + second
日、月采用1索引的原因
我们已确定1970年1月1日以来的天数是两个自0000/03/01以来天数的差值。只要被减的两个值(目标日期与基准日期)的索引保持一致,结果就合理。由于魔法数719499是通过1索引的月、日计算得出的,因此调用mktime64时也需使用1索引的月、日。
魔法数367的来源
若我们对比各月天数数组与1索引(1-12)的月份对应的(367 * m) // 12项的连续差值数组,会发现两者相似,仅存在一位偏移及2月天数的差异。
month_days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] unknown_term_values = [(367 * (m+1)) // 12 - (367 * m) // 12 for m in range(12)] print(month_days[1:] + month_days[:1]) [28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 31] print(unknown_term_values) [30, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 31]
幸运的是,由于月份采用1索引,且始终计算的是自公历0000/03/01以来天数的差值,367*m//12公式在m=1(对应2月)时的误差会被完全抵消。
总结
该简洁公式的实现得益于以下几点的契合:
- 计算时将年起始日调整为3月1日,因为该日期的前一天是当年的闰日(若存在)。
367*m1//12 - 367*m0//12的差值可准确计算任意两个月份之间的天数。- 日、月采用1索引不仅提升易用性,还能抵消
367*m//12公式在2月的误差。
mktime64的文档字符串提到高斯是该公式部分内容的来源,但尚未找到相关资料。
这种简洁性要么纯属巧合,要么是罗马历法制定者利用367*m//12的商来确定各月天数。
若有人知晓该算法的来源,以及367*m//12项是否确实用于推导各月天数,欢迎提供相关资料!
感谢!:-)
内容的提问来源于stack exchange,提问作者Campbell McDiarmid

