Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1494277
  • 博文数量: 226
  • 博客积分: 3997
  • 博客等级: 少校
  • 技术积分: 2369
  • 用 户 组: 普通用户
  • 注册时间: 2010-06-19 17:26
个人简介

Never save something for a special occasion. Every day in your life is a special occasion.

文章分类

全部博文(226)

文章存档

2018年(5)

2017年(11)

2016年(1)

2015年(17)

2014年(14)

2013年(30)

2012年(5)

2011年(52)

2010年(107)

分类:

2010-10-26 08:33:02

Tomohiko Sakamoto 星期计算算法:
static int t[]={0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4};
y-=m<3;
w = (y+y/4-y/100+y/400+t[m-1]+d)%7;


在《你必须知道的495个C语言问题》中看到这个算法,乍看,容易,再看,没线索了~ 于是因此困惑了一些日子。下面一步步推导出它。

算法推导:
设y年m月d日是星期w。不同年份同一日期,星期号不同。
因为相邻年之间这种差异可以推算出来,所以y/m/d的w存在规律,可计算。

以日期 1年1月1日 为参考点,假设这天是星期w0。现在计算y年1月1日的星期号w1


一年中日期d递增1,星期号w递增1;


平年365天=52*7 + 1
润年366天=52*7 + 2
每经过一个平年,对应日期的星期号递增1;
每经过一个润年,3月前对应日期的星期号递增1,3月开始对应日期的星期号递增2。
y年相对参考点,经过(含年y)润年数为(y/4-y/100+y/400)

if(isLeap(y))
    w1 = (y + y/4-y/100+y/400 - 1 + H)%7; // H 是修正常数。
else
    w1 = (y + y/4-y/100+y/400 + H)%7;

这里"-1"是因为表达式(y/4-y/100+y/400)对于润年y 已经计算了偏移1,而现在(1月1日)还没有到达这个特别的日子 2日29日。

年y的第cd天( cd = ∑dayOfMon + d )是星期w2

if(isLeap(y))
    w2 = (y + y/4-y/100+y/400 - 1 + cd + H)%7;
else
    w2 = (y + y/4-y/100+y/400 + cd + H)%7;

如果将每月第1天的星期号建立一个表,取名叫 月初日星期表,则可用查表的方法代替计算 ∑dayOfMon。
假设1月1日是星期0(星期天),则平年和润年的月初日星期表分别为
wFirstDayOfMonth_noLeap[]={0, 3, 3, 6, 1, 4, 6, 2, 5, 0, 3, 5};
wFirstDayOfMonth_Leap[] ={0, 3, 4, 0, 2, 5, 0, 4, 6, 1, 4, 6};

于是 y/m/d 的星期号w3计算式如下

if(isLeap(y))
    w3 = (y + y/4-y/100+y/400 - 1 + wFirstDayOfMonth_Leap[m] + d + H)%7;
else
    w3 = (y + y/4-y/100+y/400 + wFirstDayOfMonth_noLeap[m] + d + H)%7;

对比平年和润年的月初日星期表,差异产生在 2月的第29日,再考虑以上两个计算式的差异"-1",可以将两个月初表合二为一。

如果取平年的月初日星期表,则 y/m/d 的星期号w的计算式如下:

    static int wFirstDayOfMonth_noLeap[]={0, 3, 3, 6, 1, 4, 6, 2, 5, 0, 3, 5};
    if(leap(y))
    {
        if(m<3)
            w = (y + y/4-y/100+y/400 -1 + wFirstDayOfMonth_noLeap[m-1] + d + 6) % 7; // 6 即为前面推导式中的H。查日历或运行一次程序即可知道它的值。
        else
            w = (y + y/4-y/100+y/400 + wFirstDayOfMonth_noLeap[m-1] + d + 6) % 7; // 其实 "-1" 已经包含在wFirstDayOfMonth中。
    }
    else
    {
        w = (y + y/4-y/100+y/400 + wFirstDayOfMonth_noLeap[m-1] + d + 6) % 7;
    }

如果取润年的月初日星期表,则 y/m/d 的星期号w的计算式如下:

    static int wFirstDayOfMonth_Leap[]={0, 3, 4, 0, 2, 5, 0, 3, 6, 1, 4, 6};
    if(leap(y))
    {
        w = (y + y/4-y/100+y/400 -1 + wFirstDayOfMonth_Leap[m-1] + d + 6) % 7;
    }
    else
    {
        if(m>2)
            w = (y + y/4-y/100+y/400 + wFirstDayOfMonth_Leap[m-1] + d + 6 -1) % 7; // 使用了润年的月初日星期表,2月后要 -1
        else
            w = (y + y/4-y/100+y/400 + wFirstDayOfMonth_Leap[m-1] + d + 6) % 7;
    }

算法还可以优化吗?

算法中有2个计算式,它们之间关系密切
润年判断式:isLeap(y) = ( (y%4==0)&&(y%100!=0) ) || (y%400==0)
润年数计算式: cntLeap(y) = y/4-y/100+y/400;
如果 y 是润年,则 cntLeap(y-1) = cntLeap(y) - 1 ;
如果 y 是平年,则 cntLeap(y-1) = cntLeap(y) ;

static int wFirstDayOfMonth_noLeap[]={0, 3, 3, 6, 1, 4, 6, 2, 5, 0, 3, 5};
if(m<3)
{
 y--; // 期望通过y的变形来统一公式... 通过尝试,结果是可喜的! 消除了润年判断。
 if(leap(y))
 {
  w = (y+1 + y/4-y/100+y/400+1 + wFirstDayOfMonth_noLeap[m-1] + d + 5) % 7;
 }
 else
 {
  w = (y+1 + y/4-y/100+y/400 + wFirstDayOfMonth_noLeap[m-1] + d + 6) % 7;
 }
 
}
else
 w = (y + y/4-y/100+y/400 + wFirstDayOfMonth_noLeap[m-1] + d + 6) % 7;

以上程序等价于

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

通过修改3~12月的月初日星期号,得到下面的程序:

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

#include "stdio.h"

 /*验证数据*/
typedef struct DATE_tag
{
    int y, m, d;
    int w;
}DATE;

int numItem;
DATE aCheck[]={
/* 年,月,日,星期 */
{0000, 3, 1, 3},
{1600, 3, 1, 3},
{2000, 3, 1, 3},
{2001, 3, 1, 4},
{2004, 3, 1, 1},
{2008, 3, 1, 6},
{2042, 3, 1, 6},

/* 平年3月1日 */
{1700, 3, 1, 1}, /* 注意:1700年是平年 */
{1800, 3, 1, 6}, /* 注意:1800年是平年 */
{1900, 3, 1, 4}, /* 注意:1900年是平年 */
{1901, 3, 1, 5},
{1918, 3, 1, 5},
{1958, 3, 1, 6},
{1988, 3, 1, 2},
{1999, 3, 1, 1},
{2100, 3, 1, 1}, /* 注意:2100年是平年 */
{2101, 3, 1, 2},
{2102, 3, 1, 3},
{2103, 3, 1, 4},
{2104, 3, 1, 6}, /* 注意:2104年是闰年 */ /* 不是周2 */
{9999, 3, 1, 1},

/* 2004年的重大节日 */
{2004, 1, 1, 4}, /* 元旦 */
{2004, 1, 22, 4}, /*春节*/
{2004, 2, 5, 4}, /*宵节*/
{2004, 3, 8, 1}, /*妇女节*/
{2004, 3, 12, 5}, /*植树节*/
{2004, 3, 15, 1}, /*投诉节*/
{2004, 4, 4, 0}, /*清明节*/
{2004, 5, 1, 6}, /*五一节*/
{2004, 5, 4, 2}, /*青年节*/
{2004, 5, 9, 0}, /*母亲节*/
{2004, 6, 1, 2}, /*儿童节*/
{2004, 6, 22, 2}, /*端午节*/
{2004, 7, 1, 4}, /*建党节*/
{2004, 8, 1, 0}, /*建军节*/
{2004, 8, 8, 0}, /*父亲节*/
{2004, 9, 10, 5}, /*教师节*/
{2004, 9, 28, 2}, /*中秋节*/
{2004, 10, 1, 5}, /*国庆节*/
{2004, 10, 6, 3}, /*老人节*/
{2004, 10, 22, 5}, /*重阳节*/
{2004, 11, 7, 0}, /*立冬节*/
{2004, 12, 21, 2}, /*冬至节*/

/* 二十世纪 */
{1900, 1, 1, 1}, /*元旦*/

{1900, 3, 1, 4}, /* 公式测试关键点 */

{1918, 1, 21,1}, /*大寒*/
{1918, 2, 11, 1}, /*春节*/
{1927, 1, 1, 6}, /*元旦 */
{1948, 1, 18, 0}, /*腊八节*/
{1949, 10, 1, 6}, /*国庆*/
{1962, 5, 1, 2}, /*劳动节*/
{1999, 12, 31, 5},

/* 二十一世纪 */
{2000, 1, 1, 6}, /*元旦*/
{2018, 1, 20, 6}, /*大寒*/
{2018, 2, 16, 5}, /*春节*/
{2027, 1, 1, 5}, /*元旦*/
{2048, 1, 22, 3}, /*腊八节*/
{2099, 12, 31, 4},

/* 二十二世纪 */
{2100, 1, 1, 5},
{2100, 1, 31, 0},
{2100, 2, 1, 1},
{2100, 2, 28, 0},
{2100, 3, 1, 1},
{2100, 3, 31, 3},
{2100, 12, 31, 5},
{2101, 1, 1, 6},
{2101, 1, 31, 1},
{2101, 2, 1, 2},
{2101, 2, 28, 1},
{2101, 3, 1, 2},
{2101, 3, 31, 4},
{2101, 12, 31, 6},
{2104, 2, 29, 5}, /* 不是周1 */
{2104, 3, 1, 6}, /* 不是周2 */
{2199, 12, 31, 2}
};

int dayofweek(int y, int m, int d); /* 0 sunday */

int main()
{
    int i;
    int y, m, d, w;

    numItem = sizeof(aCheck)/sizeof(aCheck[0]);
    for(i=0; i<numItem; i++)
    {
        y = aCheck[i].y;
        m = aCheck[i].m;
        d = aCheck[i].d;
        w = aCheck[i].w;
        if(w != dayofweek(y, m, d))
        {
            printf("error on %dth item:(%d,%d,%d,%d), calc res is %d \n",
                i, y, m, d, w, dayofweek(y, m, d));
            break;
        }
    }
    if(i == numItem)
        puts("ok");
    else
        puts("no");

}

int leap(int y)
{
    return ( (y%4==0)&&(y%100!=0) ) || (y%400==0);
}

int dayofweek(int y, int m, int d) /* 0 sunday */
{
#if 1
    /* 算法原型——需要判断润年 */
    int w=0;

    static int wFirstDayOfMonth_noLeap[]={0, 3, 3, 6, 1, 4, 6, 2, 5, 0, 3, 5};
    #if 0
    if(leap(y))
    {
        if(m<3)
            w = (y + y/4-y/100+y/400 -1 + wFirstDayOfMonth_noLeap[m-1] + d + 6) % 7;
        else
            w = (y + y/4-y/100+y/400 + wFirstDayOfMonth_noLeap[m-1] + d + 6) % 7;

    }
    else
    {
        w = (y + y/4-y/100+y/400 + wFirstDayOfMonth_noLeap[m-1] + d + 6) % 7;
    }
    #else
    if(m<3)
    {
        if(leap(y))
        {
            w = (y + y/4-y/100+y/400 + wFirstDayOfMonth_noLeap[m-1] + d + 5) % 7;
        }
        else
        {
            w = (y + y/4-y/100+y/400 + wFirstDayOfMonth_noLeap[m-1] + d + 6) % 7;
        }
        
    }
    else
        w = (y + y/4-y/100+y/400 + wFirstDayOfMonth_noLeap[m-1] + d + 6) % 7;
    #endif

    return w;

#else
    /* 算法变形——不需要判断润年 */
    /* 此算法版本最初由 Tomohiko Sakamoto 提供 */
    int w=0;
    static int t[]={0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4};
    y-=m<3;
    w = (y+y/4-y/100+y/400+t[m-1]+d)%7;
    return w;

#endif
}




另一个星期计算公式:

Zeller's Rule 

http://blog.chinaunix.net/u3/116013/showart.php?id=2364750


阅读(2607) | 评论(0) | 转发(1) |
给主人留下些什么吧!~~