我现在想要输入一个日期,然后返回对应的日期是周几,如果是你,你会怎么解决,当然这应该难不倒你,但是如果限制了代码只能有两行,阁下又该如何应对?
今天我们学习一个非常优雅的星期几计算算法,大名叫做:
Tomohiko Sakamoto 算法
直接上代码,看得懂的同学就不需要继续往下看了:
int dow(int y, int m, int d) {
y -= m<3;
return(y + y/4 - y/100 + y/400 + "-bed=pen+mad."[m] + d) % 7;
}
WTF???
让我们一步步揭开这个算法的神秘面纱,我们首先从简单场景开始,逐步解决以下的问题:
Q1:已知某月1号是星期a,那该月x号是星期几?
A1:((x-1)+a)%7。因为每7天一轮回,只需要算出日期和1号的差值与7取余后从星期a递推即可。
int dow(int d){
return (d - 1 + a) % 7;
}
Q2:不考虑闰年,已知某年1月1号是星期a,那该年的x月1号是星期几?
A2:由于每个月的天数是一定的,那么每个月的起始日的星期一定与上一个月的星期存在固定的递推关系,即:
因此,考虑到Q1、Q2两个问题后,继续修改我们的解决方案:
int dow(int m, int d){
static int t[] = {0, 3, 3, 6, 1, 4, 6, 2, 5, 0, 3, 5};
return (t[m-1] + d - 1 + a) % 7;
}
Q3:不考虑闰年,已知去年1月1号是星期a,那今年的1月1号是星期几?
A3:一年固定为365天,365=52*7+1,因此,下一年一定为(a+1)%7,每过1年,则向后推移一天。那么我们又可以修改我们的解决方案,使得它满足年关系。设从start_year开始向后推移,start_year可以任取。
int dow(int y, int m, int d){
static int t[] = {0, 3, 3, 6, 1, 4, 6, 2, 5, 0, 3, 5};
return ((y - start_year) + t[m-1] + d - 1 + a) % 7;
}
由于上面的start_year,-1,a都是常量,我们将其合并为c:
int dow(int y, int m, int d){
static int t[] = {0, 3, 3, 6, 1, 4, 6, 2, 5, 0, 3, 5};
return (y + t[m-1] + d + c) % 7;
}
Q4:考虑闰年,如何完善方案?
A4:我们首先需要知道闰年的规则,能除尽4,不能除尽100或能除尽400时为闰年(也不知道是哪个天才的发明),则会使得2月多一天。
那么,在推算某一年的第一天是星期几时可能需要加以讨论:
由于start_year可以任取,取为0。
第y年第一天相较于第0年第一天的推移:
解释:由于每个闰年会导致多偏移一天,因此,每4年由于闰年应该多偏移一天,每100年因为不闰又应该把多的一天减去,由于每400年也会有一个特殊闰年,则又该加回,按照以前的分析,每年会向后偏移一天,这得到了上述公式。
于是方案修改为:
int dow(int y, int m, int d){
static int t[] = {0, 3, 3, 6, 1, 4, 6, 2, 5, 0, 3, 5};
return (y + y/4 - y/100 + y/400 + t[m-1] + d + c) % 7;
}
我们令start_year等于0在上面的公式中会影响c,但是因为c我们还未求解,实际上也并不影响他只是一个常数这件事。
闰年除了会影响当年首日的推断,还会影响月的推断,因为在闰的当年,实际上1、2月份并不会受闰月影响,而从3月份起,推断月份首日就应该向后推一天,我们可以判断如果是闰年,这也好办,我们“假设把一年的起点移动到3月1日”就好了。
int dow(int y, int m, int d){
// t[]数组改为从3月份开始推
static int t[] = {5, 1, 0, 3, 5, 1, 3, 6, 2, 4, 0, 2};
y -= m < 3; // 1,2月 算去年
return (y + y/4 - y/100 + y/400 + t[m-1] + d + c) % 7;
}
于是接下来,我们就只剩最后一个悬而未决的问题——c的值为多少。
这个问题我们只需要利用发文当日2024年5月7日是已知的星期二就可以带入求解,解得c=2 (限制c属于0~7)。
int dow(int y, int m, int d){
// t[]数组改为从3月份开始推
static int t[] = {5, 1, 0, 3, 5, 1, 3, 6, 2, 4, 0, 2};
y -= m < 3; // 1,2月 算去年
return (y + y/4 - y/100 + y/400 + t[m-1] + d + 2) % 7;
}
可以把c加到t数组中:
int dow(int y, int m, int d){
// t[]数组改为从3月份开始推
static int t[] = {0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4};
y -= m < 3; // 1,2月 算去年
return (y + y/4 - y/100 + y/400 + t[m-1] + d) % 7;
}
于是,就得到了一个完备的计算今天是星期几的 Tomohiko Sakamoto 算法。
根据最终的公式,我们将t数组加上一个7的倍数不会对结果产生影响,于是,某些比较闲的人士就去这么做了,找到了那么一句话“-bed=pen+mad.”。将他们按照ASCII码表规则取余7之后,即为{x, 0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4},同时让出了第0个位置。
于是就得到了我们最上面表述的形式:
int dow(int y, int m, int d) {
y -= m<3;
return(y + y/4 - y/100 + y/400 + "-bed=pen+mad."[m] + d) % 7;
}
不得不说真是优雅。
------
脚注:由于儒略历在过去的几个世纪里已造成了落后十天的事实,格里高利十三世指定1582年10月4日之后,第二天正式改为1582年10月15日,中间的10天就这么无可奈何地消失了,因此我们刚刚学会的算法对于1582年10月4日之前就无效了,但我想也应该没有谁需要用上吧。