万年历程序:让时间在代码里可视化
说实话,万年历这个项目,是我当年学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重绘了一遍,你看一眼就能明白整个流程:
五、打印日历的细节
打印日历其实是个排版活。我习惯这样设计输出格式:
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