C语言程序设计杂谈(一):星期计算问题

C语言程序设计杂谈(一):星期计算问题

问题描述

在星期计算问题中,我们将问题按照实现难度/复杂程度划分成若干等级:

  1. 已知2019年1月1日是星期二,求2019年内任意一天的星期数。你需要引导用户输入包含月和日的日期,并输出该日期是星期几。
  2. 在等级1的要求中,增加对输入日期的合法性检查功能。
  3. 在等级2的要求中,增加查询任意年份日期的功能。

等级1分析

一种比较直观的思路是求出1月1日到给定的日期之间经过的天数(包括1月1日与该日期当天,当然实现的是不包括的也可以,本文不讨论该种写法),并将该值对7取余数(即取模运算,下简称对7取模)。

由于1月1日是2019年的首日,这让问题的解决简化了许多,给定的日期一定在这一天之后,只需要统计之后了多少天即可。对于经过的整月,向统计变量加进该月的天数,非整月(即该天所在的月份)向统计变量加进日期中的天的数值即可。这一过程可以通过11个if语句实现,每个if语句用于判断对应的整月是否需要加入统计变量。

统计完成之后,直接将该值对7取模。由于1月1日是星期二,需要对该值加上1,即可得到最终的答案。此时该值一定在区间[1, 7]中。

等级2分析

这一部分仍然是通过if语句块实现的。首先判断月份是否在区间[1, 12]中,然后再根据每个月的天数对月份分类成28天、29天(闰年2月)、30天、31天四类,分别处理即可。

需要注意的问题是,闰年的判断方法较为复杂:能被4整除但不能被100整除的年份,以及能被400整除的年份都是闰年。

等级3分析

在等级3中,首先解决比较简单的问题:假设输入的日期在2019年1月1日之后,且不考虑闰年,只需要在等级1的实现之前加入计算经过的整年的天数的过程即可,即向统计变量中加入输入年份与2019的差值倍的365天。

接下来我们考虑日期在2019年1月1日之前的情况。事实上这种情况可以统一到以上计算过程中。当日期在2019年1月1日之前时,计算出的整年天数(输入年份与2019的差值乘以365)是一个负数,我们用这个负数代表“在2019年1月1日之前多少天”这一含义,直接将这个负数加入统计变量即可。接下来直接执行等级1的过程,得到的答案也是正确的,只是由于负数对7取模可能得到负数,需要对该变量加7转化为正数输出。

这里分析上述过程的正确性:假设输入的日期是$x$年$y$月$z$日,计算整年天数时用的式子是$365(x-2019)$,这里会将该天所在的一整年都计入进去,且数字是一个负数。之后经过了等级1的流程,得到的答案是1月1日到$y$月$z$日的天数,且数字是一个整数。负数与正数相加,绝对值会变小,相当于把该天所在的一年中不应计入答案的部分给减去了。此外,对于使用负数的正确性,由于这里利用的是7天一循环的原理,正向反向循环都会代表同一个含义,因此正负数的原理本质完全相同,可以直接套用上述方法。

接下来我们考虑闰年计算的问题。由于4的倍数每隔4个会有1个,这里很容易想到用$(x-2019)/4$(向下取整,以下除法皆为向下取整,省略向下取整标记)作为$x$年到2019年之间的闰年的数量,但这个方法可能存在错误。例如2012年到2016年一共5年时间,计算出的结果是1,但答案有2012和2016一共2年。修正这种错误的方法有判断是否两端都是闰年(由于问题的特殊性,即端点之一的2019年不是闰年,使用这种方法在本问题中并不会造成错误,但可能在更加一般化的问题中出错,因此这里有必要解释这个错误),或者使用以下更加通用的方法。

如果我们计算出1年到$x$年之间的闰年数和1年到2019年之间的,作差就可以得到正确答案。由于1年不是闰年,这里可以放心大胆用$x/4$和$2019/4$来计算,即正确答案是$x/4-2019/4$。这种方法可以保证正确性,同时形式也较为简单。

接下来,我们需要应对比较复杂的闰年判断方法,这里应用容斥原理的想法:首先计算出4的倍数,从中减去100的倍数(100可以被4整除),再加进400的倍数(400可以被100整除)即可。这里的修正仅限闰年时每年的天数会多1,因此直接将$x$年到2019年间闰年的数量加入统计变量即可。

到这里为止,如果你对内容的理解有问题,不妨指定一个较小的日期,手动计算一遍上述过程,体会其中的原理。

实现

由于个人癖好以及思维方式等原因,这里只给出一种比较有本人实现风格的实现方法,较难懂的部分会在代码注释中给出部分解释。你也可以按照上述原理,实现出自己的程序。

// Code by KSkun, 2019/10
#include <stdio.h>

int main() {
    int year, month, day;
    printf("Please input year:\n");
    scanf("%d", &year);
    printf("Please input month (1-12):\n");
    scanf("%d", &month);
    printf("Please input day (1-31):\n");
    scanf("%d", &day);

    // 合法性检查
    if (year <= 0) {
        printf("Invalid year!\n");
        // 不在最后写return 0;的效果是程序提前终止,即程序只要执行至return 0;的位置就会终止,后面的代码并不会继续运行
        return 0;
    }
    if (month < 1 || month > 12) {
        printf("Invalid month!\n");
        return 0;
    }
    if ((month == 1 || month == 3 || month == 5 || month == 7 || month == 8 || month == 10 || month == 12) && (day < 1 || day > 31)) { // 由于&&优先级比||高,在这里需要增加括号以保证逻辑的正确性
        printf("Invalid day!\n");
        return 0;
    }
    if ((month == 4 || month == 6 || month == 9 || month == 11) && (day < 1 || day > 30)) {
        printf("Invalid day!\n");
        return 0;
    }
    if (month == 2 && (year % 400 == 0 || year % 100 != 0 && year % 4 == 0) && (day < 1 || day > 29)) {
        printf("Invalid day!\n");
        return 0;
    }
    if (month == 2 && !(year % 400 == 0 || year % 100 != 0 && year % 4 == 0) && (day < 1 || day > 28)) { // 这里的!是逻辑非运算符
        printf("Invalid day!\n");
        return 0;
    }

    // 计算整年的天数
    int result = 1; // 当result值为0时代表result是7的倍数,此时代表该日期是星期日
    result += (year - 2019) * 365;
    // 下面应用容斥原理计算闰年
    result += year / 4 - 2019 / 4;
    result -= year / 100 - 2019 / 100;
    result += year / 400 - 2019 / 400;
    // 这里将负数数值转换成整数,需要注意的是,边做加法边取模不会改变最终结果,这里是由模运算的性质决定的,感兴趣的同学可以百度学习模运算的性质(数论内容,不要求掌握)
    result = (result % 7 + 7) % 7;
    
    // 同样是边加边取模
    if (month > 1) result = (result + 31) % 7;
    if (month > 2) result = (result + 28) % 7;
    if (month > 3) result = (result + 31) % 7;
    if (month > 4) result = (result + 30) % 7;
    if (month > 5) result = (result + 31) % 7;
    if (month > 6) result = (result + 30) % 7;
    if (month > 7) result = (result + 31) % 7;
    if (month > 8) result = (result + 31) % 7;
    if (month > 9) result = (result + 30) % 7;
    if (month > 10) result = (result + 31) % 7;
    if (month > 11) result = (result + 30) % 7;
    result = (result + day) % 7;
    
    // 由于result值可能为0,此时应当输出7,因此特判一下
    if (result == 0) result = 7;
    printf("%d-%d-%d is the No.%d day of that week.\n", year, month, day, result); 

    return 0;
}

扩展阅读

  • 蔡勒公式(Zeller’s Formula)


发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据