背景:以前学习C语言也没做什么东西出来,之前看哈佛的CS50课程倒跟着课程进度写了一些C语言作业题。在看CS50的过程中,写了一些python和html以及css,对比发现其他语言的反馈效果要比C以及C++要好,更容易做出好看的成果出来,相比之下更容易得到激励并形成正反馈。后来看北京大学程序与设计实习课程,这么久写代码倒一直在做题,以前做作业题,后来刷OJ题,学数据结构。最近感到遇到瓶颈了,光做题恐怕很难坚持下去了,需要给自己些看得到的成果,我打算把自己写的一些有趣的小东西或算法写成一个系列,就当给自己一个纪念以及方便自己回看。
这几天跟着实验楼写了一个简单的C语言小项目—万年历(https://www.shiyanlou.com/courses/126),虽然很简单,但算法很精妙,值得分析并记录一下,在写的过程中对部分代码加入自己的注释以方便理解。
以下是源代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/*
设计思路
问题来了。设计一个万年历的思路是什么呢,其实很简单,
万年历的主要功能是提供给使用者查询日期时间。那么万年历具有什么特点呢?
(1)平年365天(52周+1天),闰年366天(52周+2天),平年2月28天,闰年2月29天。
由于公元1月1日设为星期六,故3月1日为星期三。为使算法达到最简,故本算法以“星期”为计算单位,且选3月1日为基月。
(2)每400年整一闰,或每4年且不为百年的一闰,即凡能被400整除,或不能被100整除但能被4整除的年份为润年。
(3)每 4年(3个平年+1个闰年)共208周+5天 每百年共100(208周+5天)-1天=5217周+5天
每400年共4(5217周+5天)+1天(整400年闰)=20871周+0天,即每400年一个轮回。
搜索到的解释:非整百的年份除以4,或者整百的年份除以400,若不能整除的,即为平年。
*/
int main(int a, char **date)
{
int year = 0, month = 0, day = 0, week;
int d, i, dm, dy, m2;
char WEEK[9];
if (a == 1)
{
printf("\n ERROR! you forget to enter the date you want to view\n");
exit(0);
}
i = 0;
d = -1;
while (date[1][i]) // 遍历传入的参数日期,计算出year,month,day
{
if ((date[1][i]=='/' || date[1][i]=='.') && d==-1)
{
d = 0;
i++;
continue;
}
if ((date[1][i]=='/' || date[1][i]=='.') && d == 0)
{
d = 1;
i++;
continue;
}
if (d == -1)
{
year = year*10 + (date[1][i] - '0');
}
if (d == 0)
{
month = month*10 + (date[1][i] - '0');
}
if (d == 1)
{
day = day*10 + (date[1][i] - '0');
}
i++;
}
if (month<1 || month>12) // 若月份传入错误数字
{
printf("\n ERROR! the entered MONTH is invalid\n");
exit(0);
}
if (year == 2000)
{
dy = 0; // 年引起的星期差为0个
m2 = 1; // 2月引起的星期差为1个,2000年闰年则2月有29天=4*8+1
goto la_100;
}
if (year > 2000)
{// d统计目标年与2000之间闰年的个数
d = (year-1-2000)/4 - (year-1-2000)/100 + (year-1-2000)/400 + 1;
}
else
{ // 不为百年且能被4整除 // 能被400整除
d = (year-2000)/4 - (year-2000)/100 + (year-2000)/400;
}
dy = (year-2000) + d; // 该年1月1号到2000年1月1号的“星期差”
if ((year%4==0 && year%100!=0) || (year%100==0 && year%400==0)) // 是闰年
{
m2 = 1; // 二月引起的星期差
}
else
{
m2 = 0;
}
la_100:
// 该月以前的月所引起的“星期差”
switch(month)
{
case 1: dm = 0; month = 31; break; // month在此存放月天数
case 2: dm = 3; month = d==1?29:28; break;
case 3: dm = 3 + m2; month = 31; break;
case 4: dm = 6 + m2; month = 30; break;
case 5: dm = 1 + m2; month = 31; break;
case 6: dm = 4 + m2; month = 30; break;
case 7: dm = 6 + m2; month = 31; break;
case 8: dm = 2 + m2; month = 31; break;
case 9: dm = 5 + m2; month = 30; break;
case 10: dm = m2; month = 31; break;
case 11: dm = 3 + m2; month = 30; break;
case 12: dm = 5 + m2; month = 31; break;
}
if (day<0 || day>month)
{
printf("\n ERROR! the entered DAY is invalid\n");
exit(0);
}
week = (dy + dm + day - 1 + 6) % 7;
if (week < 0)
{
week += 7;
}
if (day < 0)
{
switch (week)
{
case 0: strcpy(WEEK, "SUNDAY"); break;
case 1: strcpy(WEEK, "MONDAY"); break;
case 2: strcpy(WEEK, "TUESDAY"); break;
case 3: strcpy(WEEK, "WEDNESDAY"); break;
case 4: strcpy(WEEK, "THURSDAY"); break;
case 5: strcpy(WEEK, "FRIDAY"); break;
case 6: strcpy(WEEK, "SATURDAY"); break;
default: break;
}
}
else
{
week = ++week%7;
printf("\n the calendar of this month as following\n");
printf("\n *********************************\n");
printf(" SUN MON TUE WEN THU FRI STA\n");
for (i = 0; i < week; i++)
{
printf(" ");
}
for (i = 1; i <= month; i++)
{
printf(" %2d ", i);
week++;
if (week%7==0 && i!=month)
{
printf("\n");
}
}
printf("\n**********************************\n");
printf("\n OK!\n");
}
return 0;
}
程序是以Linux命令行形式运行的,将日期以命令行参数传入,然后计算出年月日。
实验楼的步骤中对算法的解释较为简略,我将自己的理解的思路阐述一下:万年历需要解决的主要问题是,确定日期的星期。此算法以2000年1月1日(星期六)为基点,计算其他日期与基点相差的星期数,如果相差一整周则说明星期相同。