万年历程序:让时间在代码里可视化

说实话,万年历这个项目,是我当年学C++时做的第一个“有成就感”的东西。你想想看,一个黑乎乎的终端窗口,能打印出整年的日历,月份、星期、日期整整齐齐——那种感觉,比写一百道算法题都爽。

但别小看它。万年历背后藏着日期计算、闰年判断、蔡勒公式这些硬核知识点。我后来在开发日志系统时,还专门回来翻过这段代码。嗯,今天我们就把它彻底讲透。

核心目标:输入年份,输出该年12个月的日历。支持公历,正确处理闰年,星期对齐。

一、先搞清楚日期怎么算

万年历的核心就两个问题:这个月有多少天?这个月的1号是星期几?

第一个问题简单。我习惯用一个数组把每个月的天数存起来:

int daysInMonth[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};

但二月特殊。闰年29天,平年28天。判断闰年的规则其实就一句话:

  • 能被400整除,是闰年
  • 能被4整除但不能被100整除,也是闰年
  • 其他情况都是平年

写成代码就是:

bool isLeapYear(int year) {
    return (year % 400 == 0) || (year % 4 == 0 && year % 100 != 0);
}

个人经验:我曾经在项目里把闰年判断写反了,导致2月29日的日志全部丢失。从那以后,我每次写日期相关代码,都会单独写个单元测试测闰年。

二、蔡勒公式:算星期几的利器

第二个问题——某天是星期几?这里我要隆重介绍蔡勒公式。说白了,它就是一个数学公式,输入年月日,输出星期几的数字(0=星期六,1=星期日,2=星期一……)。

公式长这样(我习惯用C++实现版):

int zeller(int year, int month, int day) {
    if (month <= 2) {
        month += 12;
        year--;
    }
    int c = year / 100;
    int y = year % 100;
    int w = (y + y/4 + c/4 - 2*c + 26*(month+1)/10 + day - 1) % 7;
    return (w + 7) % 7;  // 保证非负
}

注意看,我把1月、2月当作上一年的13月、14月处理。这是公式的要求,别问为什么,数学就是这么定义的。

有了这个函数,我们就能算出任意年份1月1日是星期几。然后整个年的日历就都能推出来了。

避坑指南:我曾经在移植代码到嵌入式平台时,发现蔡勒公式算出来的星期总是差一天。排查了半天,原来是公式里对负数取模的处理方式不同。记住:C++里负数取模结果可能是负数,一定要加7再取模。

三、整体架构:怎么组织代码

我不喜欢把所有代码塞进main函数。万年历程序虽然小,但也要有结构。我建议这样分:

  • isLeapYear():判断闰年
  • getDaysInMonth():获取某月天数
  • zeller():计算星期几
  • printMonth():打印单月日历
  • printYear():打印全年日历

你看,每个函数只做一件事。这就是我反复强调的单一职责原则。哪怕是一个小项目,养成好习惯,以后写大项目才不会乱。

四、核心流程图

下面这张图,是我当年手绘万年历逻辑时画的。现在用SVG重绘了一遍,你看一眼就能明白整个流程:

输入年份 isLeapYear() 判断闰年 zeller() 计算1月1日星期几 循环12个月:打印日历 输出全年日历

五、打印日历的细节

打印日历其实是个排版活。我习惯这样设计输出格式:

void printMonth(int year, int month, int firstDay) {
    // 打印月份标题
    const char* monthNames[12] = {
        "一月", "二月", "三月", "四月", "五月", "六月",
        "七月", "八月", "九月", "十月", "十一月", "十二月"
    };
    printf("\n  %s %d\n", monthNames[month-1], year);
    printf("  日 一 二 三 四 五 六\n");
    
    // 打印空格对齐
    for (int i = 0; i < firstDay; i++) {
        printf("   ");
    }
    
    // 打印日期
    int days = getDaysInMonth(year, month);
    for (int d = 1; d <= days; d++) {
        printf("%3d", d);
        if ((d + firstDay) % 7 == 0) {
            printf("\n");
        }
    }
    printf("\n");
}

这里有个小技巧:firstDay是当月1号的星期几(0=星期日)。打印时先输出对应数量的空格,然后依次打印日期,每7个换行。嗯,就这么简单。

我的习惯:我会在打印完一个月后加一个空行,让月份之间分开。视觉上清晰很多。另外,我建议用%3d格式化,保证个位数和十位数对齐。

六、完整代码骨架

把上面这些拼起来,main函数就非常清爽了:

int main() {
    int year;
    printf("请输入年份:");
    scanf("%d", &year);
    
    // 计算1月1日是星期几
    int firstDay = zeller(year, 1, 1);
    
    // 逐月打印
    for (int month = 1; month <= 12; month++) {
        printMonth(year, month, firstDay);
        // 更新下个月1号的星期
        int days = getDaysInMonth(year, month);
        firstDay = (firstDay + days) % 7;
    }
    
    return 0;
}

注意看,我没有每个月都调用zeller公式。而是用了一个递推:下个月1号的星期 = (本月1号的星期 + 本月天数) % 7。这样效率更高,代码也更优雅。

七、测试一下

我拿2024年(闰年)测试了一下,输出结果应该像这样:

月份 1号星期 天数 备注
1月星期一31
2月星期四29闰年
3月星期五31
4月星期一30
5月星期三31
6月星期六30
7月星期一31
8月星期四31
9月星期日30
10月星期二31
11月星期五30
12月星期日31

注意:如果你发现输出结果和系统日历对不上,先检查闰年判断。我曾经犯过一个低级错误——把year % 400 == 0写成了year % 400 = 0,编译没报错,但逻辑全错了。赋值和相等,C++新手最容易踩的坑。

八、扩展思路

万年历程序虽然基础,但可以玩出很多花样:

  • 支持农历:需要查表法,比较复杂
  • 支持节日标注:比如元旦、春节、国庆
  • 图形界面:用Qt或Win32 API做个窗口版
  • 日期计算器:计算两个日期相差多少天

我个人觉得,把万年历做成控制台版只是第一步。如果你能把它扩展成一个带界面的工具,那才是真正吃透了日期处理。不过那是后话了。

好了,万年历的核心逻辑就这些。代码量不大,但知识点很密集。你写的时候如果遇到问题,欢迎随时交流。


公众号:蓝海资料掘金营,微信deep3321