给定年月日,计算星期几。

最流行的方法是Zeller公式吧:

虽然十几年来一直在用这个东西,知道这个问题一行就能算出来,但还真没有特别仔细想过这个公式是如何推导出来的。大致上我觉得这就是发现了个什么规律然后吻合了。刚才 到Wikipedia上扫了一眼,发现还真是如此。个人习惯于把这种方法叫做有限范围内的巧合。好比说你找一个有限长的数列的通项公式,总能找得到的。但是要说公式里 的magic number有什么道理,就不是一句话能说清的了。叫一个外人来看,也未必看得懂。再加上这种方法还要把1月和2月当作前一年的13月和14月来算,也 不够直接。它唯一的优势就是计算简单,computer-friendly。

历法本来就是人定的。如果为了满足天文现象,只需要定义一年有365天就够了。这是有道理的,因为地球公转一周大致相当于自转365周,客观上你不能改变这个物理规律 。为了修正误差再加上点闰年什么的调整到一个合适的精度就可以了。至于一年分成多少个月,每个月多少天,就完全没啥数学道理了。A皇帝上台了,说我是X月出生的,X月 就多了一天;B皇帝上台了,说我是Y月出生的,Y月就多了一天;C皇帝上台了,看A和B都不爽,X月和Y月各减一天……如是而已。真要把历法弄得规矩一些的话倒不如分 解一下365 = 73 * 5,每年73个月每个月5天多好(当然这样月和星期就合并了,你叫它星期也行)。

所以我们还是来看看别的方法吧。首先你看计算weekday这种事情肯定是先规定某天是星期X,然后算出要计算的日期和该天的日期差再模7.既然每个月多少天都是人定 的,那把这种先验列个表格再算就行了。不过等下,先做下下面的命令:

cal 2 1600
cal 2 1700
cal 9 1752

怎么1600和1700年的2月份都是29天呢?1700好像不能被400整除吧?还有,怎么1752年的9月2日之后是9月14日呢?少了11天?

再往前看看会发现x00和1x00年的2月份都是29天。这中间涉及到一个历法的改革:从Julian calendar到Gregorian calendar。粗略地说,原来用的Julian calendar不准,没有400年不闰这一说,因而慢于实际时间。这样到1582年的时候它的春分日和地球公转 到春分点的实际时间差了10天。于是教皇Gregory XIII在1582年颁布了Gregorian calendar来修正这一误差,同时砍掉了10天使得历法 的时间和实际时间match。问题是教皇的历法遭到了新教国家的抵制。直到1752年才在英国和美国(当时还没建国)实行。这样(从新教国家的角度来看)在1700年 之前(包括1700年)的能被100整除的年份的2月都是29天。既然1582年的时候已经慢了10天,1700年的2月29日又慢了一天,那么总共就慢了11天。所 以1752年的9月2日之后砍掉11天就追上来了。

嗯,然后这个cal命令就是按照历法改革发生在1752年来算的。

然后我们继续算weekday。为了简单起见我们把日期差拆开。设输入为y年m月d日。先算年差(按每年第一天),再算年内月差(按每月第一天),最后算月内日差。加 起来就是了。一年365天,365 = 1 mod 7,所以一个平年相当于一天,一个闰年就再加一天。cal一下知道2001年1月1日是星期一。那么令dy = (y - 2001),这样dy + dy/4 - dy/100 + dy/400就是年差。为了再简单点,我们不妨用一个假想的,一直使用Gregorian calendar的1年1月1日来算。这样1年1月1日也是星期一,就不用减2001而是减1就可以了。所以年差可以进一步化简为(y - 1) + (y - 1)/4 - (y - 1)/100 + (y - 1)/400。

月差可以查表。1月份第一天和年初第一天当然不差,2月份第一天差31 = 3 mod 7……依此类推,得到mtable[] = {0, 3, 3, 6, 1, 4, 6, 2, 5, 0, 3, 5}。当然如果m >= 3,要判断一下是不是闰年,是的话再加一天。

日差直接加就行了。

这样就算完了。但是有个问题是算月差是还得判断一下是不是闰年,虽然算年差的时候已经用了形似y/4 - y/100 + y/400的式子了。能不能合起来呢?当然可以,我们大可不必让2月份有没有29天影响到其他月份,只要把分隔点选在3月初而不是1月初就行了。如果m < 3,我们就算y年的年差,然后对月差用加法;如果m >= 3,我们就算(y + 1)年的年差,然后对月差用减法。比如算2003年12月8日是星期几,我们就先算2004年的年差,然后减去月差31 % 7 = 3(12月有31天),再加上日差。这样只需要修改一下mtable就行了。修改后的mtable = {0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4}。在程序执行时只需要先判断m是否小于3,如果是,就令y = y - 1。写成代码就是:

int dow(int y, int m, int d)
{
   static int t[] = {0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4};
   y -= m < 3;
   return (y + y/4 - y/100 + y/400 + t[m-1] + d) % 7;
}

这就是Sakamoto方法。

Links: