[大学程序课] C语言日历程序

Author Avatar
MonsterX 6月3日 发布 | 06月07日最后更新
  • 在其它设备中阅读本文章

原本是打算写一篇关于蔡勒公式等算法的文章,写着写着突然发现这篇内容可能并不能称之为“算法”,我以为,正规意义上的“算法”应该是研究数据结构二叉树之类的…但是《 百度百科. 词条. 算法 》这样说:

算法(Algorithm) 是指解题方案的准确而完整的描述,是一系列解决问题的清晰指令,算法代表着用 系统的方法 描述解决问题的策略机制。也就是说,能够对 一定规范的输入 ,在有限时间内获得 所要求的输出

看完这段话我感觉之前那篇《[萌新算法] 公约数 & 公倍数 》和本篇应该也符合这些描述,公式即为 系统的方法 ,输入的数字是 一定规范的输入 ,得到的结果是 所要求的输出 。那就姑且把它们叫做“算法”,以后再慢慢摸索那种高深的算法叭…

 

问题引入

C 语言课程的期末大作业,要求完成一个多种功能具有一定实用意义的 Project 。供选八道选题中的一题:

题目 1:日历问题
功能要求:
主函数提供功能菜单供用户选择,用户可以选择调用以下各个功能,也可以选择继续或退出程序。系统应提供以下功能:

  • 计算未来天数和星期:输入未来某天的日期,输出距今天还有多少天?是星期几?
  • 打印年历:输入一个年份,将该年的日历按下面图示格式输出到文件 cal.txt 中。要求每行同时打印 3 个月。每月 1 号是星期几可以利用 蔡勒公式 计算,百度下该公式。
    日历示例
  • 打印月历:输入年月,输出该月的日历,可以参考年历中月份的格式。

未来可能像 九首猫 同学一样在博客写作业了哈哈哈 滑稽

这篇文章就来 百度百度计算星期的那些公式 学习推导这些公式…

算法

在开始计算之前,我们可能还需要了解一点关于历法的知识 233 :

  • 公历对日期的删减 参考百度百科 公元 词条
    罗马教皇格里高利十三世在 1582 年组织了一批天文学家,根据哥白尼日心说计算出来的数据,对儒略历作了修改。将 1582 年 10 月 5 日到 14 日之间的 10 天宣布撤销 (到 1582 年时,儒略历的春分日(3 月 21 日)与地球公转到春分点的实际时间已相差 10 天。因此,格里历开始实行时,将儒略历 1582 年 10 月 4 日星期四的次日,为格里历 1582 年 10 月 15 日星期五,即有 10 天被删除,但原星期的周期保持不变),继 10 月 4 日之后为 10 月 15 日。后来人们将这一新的历法称为“格里高利历”,也就是今天世界上所通用的历法,简称格里历或公历。
  • 闰年与平年的产生 参考百度百科 闰年 (历法中的名词) 词条
    地球绕太阳运行周期为 365 天 5 时 48 分 46 秒(合 365.24219 天)即一回归年(tropical year)。公历的平年只有 365 日,比回归年短约 0.2422 日,所余下的时间约为每四年累计一天,故第四年于 2 月末加 1 天,使当年的历年长度为 366 日,这一年就为闰年。现行公历中每 400 年有 97 个闰年。按照每四年一个闰年计算,平均每年就要多算出 0.0078 天,这样经过四百年就会多算出大约 3 天来。因此每四百年中要减少三个闰年。所以公历规定:年份是整百数时,必须是 400 的倍数才是闰年;不是 400 的倍数的世纪年,即使是 4 的倍数也不是闰年。这就是通常所说的:四年一闰,百年不闰,四百年再闰 。 例如,2000 年是闰年,2100 年则是平年。

蔡勒公式

蔡勒公式(Zeller's Fulcrum)最早由德国牧师 / 数学家克里斯蒂安·蔡勒(Christian Zeller, 1822-1899)在 1886 年推导出。

// 1582 年 10 月 4 日前:
Week = ([Century/4] - 2*Century + Year + [Year/4] + [13*(Month+1)/5] + Day - 1)%7
// 1582 年 10 月 4 日后:
Week = ([Century/4] - 2*Century + Year + [Year/4] + [13*(Month+1)/5] + Day + 2

推导过程

占位待编辑...

拓展:基姆拉尔森公式

Week = (Day + 2*Month + 3*(Month+1)/5 + Year + Year/4 - Year/100 + Year/400) % 7

或者使用这个直接返回 1~7 的星期数:

Week = (Day + 2*Month + 3*(Month+1)/5 + Year + Year/4 - Year/100 + Year/400) % 7 + 1

代码

用刚学的热乎的 C 语言实现,数组和结构体都没有用到,指针也只用到了很基础的用法。但是能用就行,管他那么多呢…

#include <stdio.h>

// 定义全局变量供自定义函数处理 
int year0,month0,day0,year,month,day;
 
// 将部分功能提取作为 API 使用 
int dateAPI (int togetY, int togetM, int togetD, int type) {
    int week;
    switch (type) {
        case 1:
            // 判断是否闰年,返回指定年份总天数 
            return (togetY % 400 == 0 || (togetY % 4 == 0 && togetY % 100 != 0))?366:365;
        case 2:
            // 判断月份,返回指定月份总天数 
            switch (togetM) {
                case 1: case 3: case 5: case 7: case 8: case 10: case 12:
                    return 31;
                case 4: case 6: case 9: case 11:
                    return 30;
                case 2:
                    return (dateAPI(togetY, togetM, 1, 1) == 366)?29:28;
            }
        case 3:
            // 基于 基姆拉尔森公式 修改,返回指定日期星期数
            // 这里周一至周日依次返回 1 ~ 7 ...
            if (togetM < 3) {
                togetM += 12;
                togetY -= 1;
            }
            week = (togetD + 2*togetM + 3*(togetM + 1)/5 + togetY + togetY/4 - togetY/100 + togetY/400) % 7 + 1;
            if (togetM > 12) {
                togetM -= 12;
                togetY += 1;
            }
            return week;
    }
}
 
void calDate () {
    int week,midY,midM,sum=0;
    printf("\n    请输入未来年月日(格式如:2019 6 1):\n");
    scanf("%d%d%d", &year, &month, &day);
    
    // 获取日期对应星期
    week = dateAPI(year, month, day, 3);
    
    // 开始计算日期间隔
    // 相同年份时直接累加月末天数、月初天数与中间月份天数 
    if (year == year0) {
        for (midM = month0 + 1; midM < month; midM ++)
            sum += dateAPI(year0, midM, 1, 2);
        if (month == month0)
            sum = sum - day0 + day;
        else
            sum = sum - day0 + day + dateAPI(year0, month0, 1, 2);
    }
    // 不同年份时累加年末天数、年初天数与中间年份天数 
    else {
        // 加上日期起点到年末的日期
        for (midM = month0 + 1; midM < 13; midM ++)
            sum += dateAPI(year0, midM, 1, 2);
        sum = sum + dateAPI(year0, month0, 1, 2) - day0;
        // 加上日期终点到年初的日期
        for (midM = 1; midM < month; midM ++)
            sum += dateAPI(year, midM, 1, 2);
        sum = sum + day;
        // 中间年份日期
        for (midY = year0 + 1; midY < year; midY ++)
            sum += dateAPI(midY, 1, 1, 1);
    }
    
    printf("\n    %d年%d月%d日是星期%d(这里偷懒了直接输出数字,7为周日),距离今天%d年%d月%d日还有%d天!\n", year, month, day, week, year0, month0, day0, sum);
}

void printY () {
    int line,i,j,k,getweek,week[3][8];
    FILE * fp;
    fp = fopen("E://Ycal.txt", "w");
    printf("\n    请输入需要打印日历的年份(格式如:2019):\n");
    scanf("%d", &year);
    
    // 输出年份标记
    fprintf(fp, "**********************************************     %d     **********************************************\n\n", year);
    
    // 最外层 for 控制打印 4 次,一次打印 3 个月 
    for (line = 0; line < 4; line ++ ) {
        // 输出月份标记 
        switch (line) {
            case 0:
                fprintf(fp, "Jan\t\t\t\t\t\t\t\tFeb\t\t\t\t\t\t\t\tMar\t\t\t\t\t\t\t\t\n");
                break;
            case 1:
                fprintf(fp, "Apr\t\t\t\t\t\t\t\tMay\t\t\t\t\t\t\t\tJun\t\t\t\t\t\t\t\t\n");
                break;
            case 2:
                fprintf(fp, "Jul\t\t\t\t\t\t\t\tAug\t\t\t\t\t\t\t\tSet\t\t\t\t\t\t\t\t\n");
                break;
            case 3:
                fprintf(fp, "Oct\t\t\t\t\t\t\t\tNov\t\t\t\t\t\t\t\tDec\t\t\t\t\t\t\t\t\n");
                break;
        }
        // 输出三个月的星期标记 
        fprintf(fp, "Mon\tTue\tWed\tThr\tFri\tSat\tSun\t\t");
        fprintf(fp, "Mon\tTue\tWed\tThr\tFri\tSat\tSun\t\t");
        fprintf(fp, "Mon\tTue\tWed\tThr\tFri\tSat\tSun\t\t\n");
        // 这里用 for 循环为二维数组 week[3][8] 赋值
        // week[0][0] 为第一个月 1 日星期数,用于日历首行输出空位 
        // week[0][1] 为第一个月第零周结束日(第一周开始日的前一天,为 0)
        // week[0][2] 为第一个月第一周结束日 
        // week[0][3] 为第一个月第二周结束日 
        // week[0][4] 为第一个月第三周结束日 
        // week[0][5] 为第一个月第四周结束日 
        // week[0][6] 为第一个月第五周结束日(当月份不足六周时失效)
        // week[0][7] 为第一个月第六周结束日(当月份不足六周时失效) 
        // week[0][] week[1][] week[2][] 分别保存每一行三个月份的数据 
        for (i = 0; i < 3; i ++) {
            for (j = 0; j < 8; j ++) {
                month = 3*line + i + 1;
                getweek = dateAPI(year, month, 1, 3);
                switch (j) {
                    case 0:
                        week[i][j] = getweek;
                        break;
                    case 1:
                        week[i][j] = 0;
                        break;
                    default:
                        week[i][j] = 7*(j-1) - getweek + 1;
                }
            }
        }
        
        // 外层 for 按行打印日期 
        for (j = 0; j < 7; j ++) {
            // 内层 for 控制一行打印三个月 
            for (i = 0; i < 3; i ++) {
                // 如果是每月日历的第一周,开头打印空格直到 1 日所在星期 
                if (j == 0) {
                    for (k = 1; k <= week[i][j]; k ++)
                        fprintf(fp, "\t");
                }
                // 打印月份每周日期,从 week[i][j+1]+1 到 week[i][j+2] 
                for (k = week[i][j+1]+1; k <= week[i][j+2]; k ++) {
                    // 当 k 不超过当月总天数时打印 k ,否则打印制表符空出位置 
                    if (k <= dateAPI(year, 3*line+i+1, 1, 2) )
                        fprintf(fp, "%2d\t", k);
                    else
                        fprintf(fp, "\t");
                }
                fprintf(fp, "\t");
                // 如果是每一行的第三个月份则日期打印完毕后回车进入下一行 
                if (i == 2)
                    fprintf(fp, "\n");
            }
            // 日历的一行打印完毕 
        }
        // 三个月打印完毕
    }
    fclose(fp);
    
    printf("\n    %d年的日历写入成功!快打开E盘 Ycal.txt 文件看看吧~\n", year);
}

void printM () {
    int i,j,week;
    FILE * fp;
    fp = fopen("E://Mcal.txt", "w");
    printf("\n    请输入需要打印日历的年份月份(如:2019 6):\n");
    scanf("%d%d", &year, &month);
    
    // 开始打印月历 
    fprintf(fp, "****************************     %d     ****************************\n", year);
    // 输出月份标记 
    switch (month) {
        case 1:    fprintf(fp, "Jan\n");    break;
        case 2:    fprintf(fp, "Feb\n");    break;
        case 3:    fprintf(fp, "Mar\n");    break;
        case 4:    fprintf(fp, "Apr\n");    break;
        case 5:    fprintf(fp, "May\n");    break;
        case 6:    fprintf(fp, "Jun\n");    break;
        case 7:    fprintf(fp, "Jul\n");    break;
        case 8:    fprintf(fp, "Aug\n");    break;
        case 9:    fprintf(fp, "Set\n");    break;
        case 10:    fprintf(fp, "Oct\n");    break;
        case 11:    fprintf(fp, "Nov\n");    break;
        case 12:    fprintf(fp, "Dec\n");    break;
    }
    // 输出星期标记 
    fprintf(fp, "Mon\tTue\tWed\tThr\tFri\tSat\tSun\n");
    // 获取指定月份 1 日星期 
    week = dateAPI(year, month, 1, 3);
    // 打印制表符空出位置直至该月 1 日所在星期 
    for (i = 1; i < week; i ++)
        fprintf(fp, "\t");
    // 开始打印该月所有天数 
    for (i = 1, j = week; i <= dateAPI(year,month,1,2) ; i ++, j ++ ) {
        if (j % 7 == 0) { 
            fprintf(fp, "%3d", i);
            fprintf(fp, "\n");
        }
        else
            fprintf(fp, "%3d\t", i); 
    }
    fclose(fp);
    
    printf("\n    %d年%d月的月历写入成功!快打开E盘 Mcal.txt 文件看看吧~\n",year,month);
}

int main () {
    char act;
    printf("\n\n    * 使用本程序前请先输入今天的日期!格式:2019 1 1\n\n");
    scanf("%d%d%d",&year0,&month0,&day0);
    printf("\n    今天是%d年%d月%d日,欢迎使用日历服务。\n    * 请输入对应序号选择您需要的服务\n        1.计算未来天数和星期\n        2.打印年历\n        3.打印月历\n    * 按 q(uit) 可退出程序。\n\n",year0,month0,day0);
    while (scanf("%c",&act) == 1) {
        // switch 选择服务进入下一步函数,执行完毕后仍可继续选择 
        switch (act) {
            case '1':
                calDate();
                printf("\n    * 功能1执行完毕!请继续选择下一步操作:\n\n");
                break;
            case '2':
                printY();
                printf("\n    * 功能2执行完毕!请继续选择下一步操作:\n\n");
                break;
            case '3':
                printM();
                printf("\n    * 功能3执行完毕!请继续选择下一步操作:\n\n");
                break;
            case 'q':
                printf("\n    * 感谢您的使用~\n");
                return 0;
        }
    }
}

看看效果:

编译程序运行效果
打印年历效果打印月历效果

更多

关于当前日期

毕竟是个懒人,题目中要求的“今日”我采用了用户手动输入的方式定义,而通过 C 语言的 <time.h> 库可以实现对当前系统时间的直接获取。当时赶着交作业没仔细看,现在看了下后发现引用也很简单,如果当时写进作业代码里可能会有一点点加分吧。不想学习,请看后面压轴资料讲的比较详细,虽然我也没大看懂指针结构体这块,但是下面这个例子能用来获取当前年月日…能用就行……

#include <time.h>

int main() {
    time_t timep;               // 定义 time_t 类型的变量 timep 用于执行 time() 函数
    struct tm *p;               // 以 tm 结构返回的时间信息指针 p
    time(&timep);               // 获取机器日历时间
    p = gmtime(&timep);         // 返回 GMT 世界标准时间

    int yn = 1900 + p->tm_year; // 实际调试并不是用 1970 加,暂时不清楚是怎么用的
    int mn = 1 + p->tm_mon;
    int dn = 1 + p->tm_yday;
    
    printf("系统时间为:%d年%d月%d日\n",yn,mn,dn);
}

// 关于 tm 结构,从标准计时点 1970 年 1 月 1 日起累计
struct tm{
    int tm_sec;
    int tm_min;
    int tm_hour;
    int tm_mday;
    int tm_mon;
    int tm_year;
    int tm_wday;
    int tm_yday;
    int tm_isdst;
};

学习资料

感谢以下资料的详细讲解,本文表达模糊或错误的地方请参考以下资料学习:

 

新开了一个博客交流群,希望认识更多喜欢博客、网站、计算机、单片机等大佬们,各位朋友们来凑一凑热闹啊吼吼,群文件有一些我收藏的资料。戳 这里 加入 Blog 主 の 圈(791792693) 吧!

本文链接:https://monsterx.cn/Algorithm/Zeller-and-more.html
本站文章除特殊说明外全部由 MonsterX 原创发布,未经允许禁止以任何形式转载。
如果您发现以上内容含有任何引起不适/侵犯权利/违反法律的内容,请立即 联系站长 进行处理。

选择表情


    Zero
    Zero  2019-06-19, 19:52

    是大佬 wsl 15.png

      MonsterX
      MonsterX  2019-06-20, 09:39

      40.png (臭弟弟不敢吱声儿...

    xzymoe
    xzymoe  2019-06-12, 16:31

    膜拜!!!想起了被指针支配的恐惧。。。

    格子老师
    格子老师  2019-06-09, 19:06

    真是牛 比我上学时候 牛多了

      MonsterX
      MonsterX  2019-06-09, 19:07

      谢谢老师夸奖!不过我这程序其实写的还是挺烂的2333

        格子老师
        格子老师  2019-06-09, 19:23

        实现功能就好 然后就可以在这个功能的基础上继续修改了

    Nroy
    Nroy  2019-06-09, 15:07

    其实很羡慕你们这种还在上学又有时间折腾这些自己想折腾了,上起班来了就只有做项目做项目,自己想折腾的很少有时间能做。虽然我刚刚用上班时间发完了一篇文章,现在又用上班时间给你发了这条评论 17.png

      MonsterX
      MonsterX  2019-06-09, 15:49

      52.png 趁年轻多玩一点想玩的,上班的确…感觉工作比上学累多了哎…

    Seale
    Seale  2019-06-07, 00:47

    这难道就是别人家的算法大佬吗?

      MonsterX
      MonsterX  2019-06-07, 00:51

      见笑了2333,刚才才把之前写的代码里的错误改了一下…不然生成的年历还是错的,配图都没注意(逃…