cyycoish 发表于 2017-4-14 18:39:00

【C】几月几号星期几

在Unix系统中有一条有趣的命令:cal,它是用来打印月历的(calendar)如图:

它还可以用来打印最近几个月的月历,甚至指定特定年份的全年月历。具体用法可以参见 man cal 或者 cal -help 命令执行结果。
那么问题来了,怎样使用程序打印一张月历?我们不妨观察一下月历的规律。

如上图所示:蓝圈内,2017年4月1号是星期六。2018年4月1号就是星期天。红圈内,2017年1月1号是星期天。2018年1月1号就是星期一。
以此规律类推,2019年1月1日是星期二,2020年1月1日是星期三。但是2016年1月1号是不是星期六呢?答案是否定的。
因为2016年是闰年,所以2016年2月有29天。这样从2016年2月28日后每个月的星期数都要向后推一天。这样导致了2017年1月1日变成星期日。
反过来说,2016年1月1日就是从2017年1月1日的星期日往前推两天。那么2016年1月1日就是星期五,而不是没有涉及闰月的星期六。
按照这个规律我们只要保存任意一个年份的任意一天是星期几,其他年份的日期星期数全部可以由该日期向前或者向后通过特定的迭代公式推得。
且慢,如果真要使用这种办法,假设我保存了2000年1月1日的星期数,我要推算2998年7月25日是星期几,运算量可想而知。
有没有知道“任意一个有效日期是星期几”的办法呢?通过搜索大量资料,这里我引入“蔡勒公式(Zeller's congruence )”
蔡勒公式如下:

图中:
h 是星期数(0=周六、1=周日、2=周一……6=周五)
q 是日。
m 是月份。
K 是当前年份的世纪。(年份 mod 100)
J 是当前年份除以100再向下取整后的值。
还需要注意的是,在蔡勒公式中一月和二月要分别看作上一年的13月和14月。比如:2017年1月等于2016年13月、2017年14月等于2018年2月。
用这个公式我们马上可以写出程序:

// Zeller's congruence
// m: month
// q:
// a:
// return 0: Saturday, 1: Sunday, 2: Monday ... 6: Friday.
//
int ZellerWeek(int m, int q, int a)
{
    int K = 0, J = 0;
    if (1 == m || 2 == m)
    {
      m += 12;
      --a;
    }
    K = a % 100;
    J = a / 100;
    return (q + (13 * (m + 1)) / 5 + K + (K / 4) + (J / 4) - 2 * J) % 7;
}

我们用以下代码测试之:

#include <stdio.h>

const char * szweek = { "Saturday",
                           "Sunday",
                           "Monday",
                           "Tuesday",
                           "Wednesday",
                           "Thursday",
                           "Friday"
                         };

int ZellerWeek(int m, int q, int a)
{
    int K = 0, J = 0;
    if (1 == m || 2 == m)
    {
      m += 12;
      --a;
    }
    K = a % 100;
    J = a / 100;
    return (q + (13 * (m + 1)) / 5 + K + (K / 4) + (J / 4) - 2 * J) % 7;
}

int main()
{
    int m, q, a;
    printf("Input date format in MM-DD-YYYY: ");
    scanf("%d-%d-%d", &m, &q, &a);
    printf(" = %s\n", szweek);
    return 0;
}


测试结果表明输入的合法公历年份均能被转换为星期。
关于蔡勒公式原理的相关说明,维基百科上有较为详细的解释:Zeller's congruence
接下来我们通过蔡勒公式打印出给定年份月份的月历:

#include <stdio.h>

const char * szwtitle = "Su Mo Tu We Th Fr Sa";

int ZellerWeek(int m, int q, int a) // 蔡勒公式
{
    int K = 0, J = 0;
    if (1 == m || 2 == m)
    {
      m += 12;
      --a;
    }
    K = a % 100;
    J = a / 100;
    return (q + (13 * (m + 1)) / 5 + K + (K / 4) + (J / 4) - 2 * J) % 7;
}

int main()
{
    int i;
    int f, e;
    int a, m;
   
    printf("MM-YYYY :"); // 输入格式: 月份-年
    scanf("%d-%d", &m, &a);
    f = ZellerWeek(m, 1, a); // 利用蔡勒公式得出该月1号是星期几。
    switch (m) // 判断输入的月份
    {
    case 1: case3: case5: case 7:
    case 8: case 10: case 12: e = 31; // 有口诀如下: 一三五七八十腊,三十一天总不差。
      break;
    case 2: // 二月就要判断是否是闰年闰月。
      if (0 == a % 4 || 0 == a % 400 || (0 == a % 3200 && 0 == a % 172800))
            e = 29; // 闰月29天。
      else
            e = 28; // 平月28天。
      break;
    default: // 剩下的月份除了小月就是非法输入,一并判断之。
      if (m < 1 || m > 12)
      {
            printf("Invalid month.\n");
            return 0;
      }
      e = 30;
    }
    printf("%s\n", szwtitle); // 先打印星期抬头。
    for (i = 0, f = f % 7 - 1; i < f; ++i)
    {
      printf("   "); // 补充周日到本月1号之间的空格。
    }
    for (i = 1; i <= e; ++i) // 从1号打印到月末。
    {
      printf("%2d ", i);
      if (0 == (i + f) % 7) // 以7的倍数为单位换行打印。
            printf("\n");
    }
    printf("\n"); // 打印最后的换行符。
    return 0;
}

Ayala 发表于 2017-4-14 22:57:29

    case 2: // 二月就要判断是否是闰年闰月。
      if (0 == a % 4 || 0 == a % 400 || 0 == a % 3200 || 0 == a % 172800)
            e = 29; // 闰月29天。
      else
            e = 28; // 平月28天。
      break;
这个有问题

cyycoish 发表于 2017-4-14 23:00:39

Ayala 发表于 2017-4-14 22:57
    case 2: // 二月就要判断是否是闰年闰月。
      if (0 == a % 4 || 0 == a % 4 ...

感谢提醒,果然有问题,逻辑处理错了。应该是同时满足0 == a % 3200 与 0 == a % 172800。
而且172800是3200的倍数,能被172800整除肯定能被3200整除。

Ayala 发表于 2017-4-14 23:31:36

cyycoish 发表于 2017-4-14 23:00
感谢提醒,果然有问题,逻辑处理错了。应该是同时满足0 == a % 3200 与 0 == a % 172800。
而且172800是3 ...

不仅如此
闰年历法在4000年之前有效 满足0 == a %400 ||(0 != a % 100 && 0 == a%4)

Ayala 发表于 2017-4-14 23:34:32

本帖最后由 Ayala 于 2017-4-14 23:38 编辑


注意原算法结果可能为负数 这里+77 在公元4000年之前有效
int ZellerWeek(int m, int d, int y)
{
    int K = 0, J = 0;
    if (1 == m || 2 == m)
    {
      m += 12;
      --y;
    }
    K = y % 100;
    J = y / 100;
    return (d + (13 * (m + 1)) / 5 + K + (K / 4) + (J / 4) - 2 * J+77) % 7;
}

if (ZellerWeek(2,29,a)==ZellerWeek(3,1,a)
...

谢谢楼主 发表于 2020-5-23 14:52:27

谢谢分析师潇洒潇洒潇洒造型
页: [1]
查看完整版本: 【C】几月几号星期几