日历算法 [ 解读算法的乐趣 ]

  •    判断闰年

  •    星期几

  •    儒略日计算

  •    蔡勒公式  

  •    输出12个月份

  •    二十四节气的天文学计算

历法,是一种推算年、月、 日的时间长度和日历编排规则之间的关系,是指定时间序列的法则。

我国的官方历法是目前全球各国通用的 公历,也就是公元纪年。

公历实际上是从 1582 年 10 月 15 日开始实行的格里历(Gregorian Calendar)。

格里历:格里高利历法就是我们通常所说的公历,它是一种源自于西方社会的历法,先在儒略历的基础上加以改革,后由罗马教皇格里高利十三世颁布(当时的法国被罗马统治)。

一年分为 12月,其中一、三、五、七、八、十和十二 是大月,大月的一个月有 31 天。四、六、九和十一是小月,小月的一个月有 30 天。2 月天数有规定,为避免以后再发生春分飘离的现象,改闰年方法为: 凡公元年数能被4整除的是闰年,但当公元年数后边是带两个“0”的“世纪年”时,必须能被400整除的年才是闰年。这就是通常所说的:四年一闰,百年不闰,四百年再闰。根据是否是闰年来定,如果是闰年,2月是 29 天,如果是平常年(不是闰年),2月是 28 天。平常 年一年是 365 天,闰年一年是 366 天。

p.s.      地球绕太阳运转的周期是365.2422天,即一个回归年(Tropical Year),而公历的一年是365天,这样一年就比回归年短了0.2422日,四年积累下来就多出0.9688天(约1天),于是设置一个闰年,这一年多一天。

如此,四个公历年又比四个回归年多了0.0312天,平均每年多0.0078天,这样经过四百年就会多出3.12天,也就是说每四百年要减少3个闰年才行,于是就设置了百年不闰,四百年再闰的俗话。


判断闰年

     判断闰年的规则: 不满足(1)、(2)条件的就是平常年

  1.  如果年份是 4 的倍数,且不是 100 的倍数,则是闰年
  2.  如果年份是 400 的倍数,则是闰年

     按照逻辑直接就可以写出如: 

日历算法 [ 解读算法的乐趣 ]_第1张图片

     事实上,可以利用条件运算符简化,这很优美以至于会让小白看不出这是判断闰年所以一定函数名一定要见名知意~

bool is_leap_year(int year)
{
    return year%(year%100?4:400)?false:true;
}

// 使用 stdbool.h 可以让 _Bool 和 bool 互换,C里面也可以使用 bool, false、true 可以宏定义

     您只需要记得,条件运算符在 epx1 ? exp2 : exp3,epx1为真时,就不是整除。


今天是星期几

     星期 的命名最早起源于古巴比伦。公元前 7~6 世纪,巴比伦人就使用了星期制,一个星期中的每一 天都有一个天神掌管。这一制度后来传到古罗马,并逐渐演变成现在的星期制度。

     星期 是固定的 7 天周期(星期本质就是模运算),其排列顺序固定,不受闰年、平常年以及大小月的天数变化影响。因此,只要确切地知道某一天是星期几,就可以推算出其他日期是星期几。

     推算方法 : 计算两个日期之间相差多少天,用相差的天数对 7 取余数,这个余数就是两个日期的星期数的差值。

     举个例子,今天是 2019年2月26日 星期二 ,那么 2020年2月26日 是星期几 ?? 

     按照推演,计算 2020年2月26日 和 2019年2月26日 的差是 365天,365 除以 7 余 1(星期差值),所以 2020年2月26日 是星期三。如果大于 2 月那需要 + 1天,因为 2020 是闰年,就是星期四因为 366 % 7 = 2 。

     如果计算 2018年2月26日是星期几,那么把差值 1 按减法运算即 -1,有如,星期二 - 1 = 星期一。

 

     此算法的核心 : 求俩个日期之间相隔的天数。我为您安利 4 种,高效准确的。

     1. 以 公历的 年 、月份 规则 计算   

            【 显然... 略 】

     2. 以 儒略日 计算

            【 一种不记年、月、只记日的历法,一秒钟对应的儒略日差值是 0.0000115740 个儒略日 】

     3. 蔡勒(Zeller)公式 

            【 一种简化闰年规律的公式,适合口算 】

     4.  say , sir   

            【  -- xxx年xx月x日  到  yyy年yy月y日相差多少天 ,另外替我谢谢 饼干大佬 这么好的方法 】

       日历算法 [ 解读算法的乐趣 ]_第2张图片


  儒略日

        儒略日(Julian Day,JD) :儒略日是一种不记年、不记月、只记日的历法,是由法国学者 Joseph Justus Scaliger (1540—1609)在 1583 年提出来的一种以天数为计量单位的流水日历。儒略日和儒略历(Julian Calendar)没有任何关系,命名为儒略日仅仅是因为他本人为了纪念他的父亲——意大利学者 JuliusCaesarScalige(1484—1558)。

公元前4713(公元 -4712)年1月1日UTC12:00 开始所经过的天数,通常记为 JD (***),JD0 就被指定为公元前 4713 年 1 月 1 日 12:00(世界时12点开始的)到公元前 4713 年 1 月 2 日 12:00 之间的 24 小时,以此顺推,每一天都被赋予一个唯一的数字

e.g.     2018年 2月 26 日 UTC12:00时 0分 0秒  =  JD 2458176.00  记为 A

           2019年 2月 26 日 UTC12:00时 0分 0秒  =  JD 2458541.00   记为 B

           2020年 2月 26 日 UTC12:00时 0分 0秒  =  JD 2458906.00     记为 C

           第一种方法, 从现在出发的日期? 用计算器算出, B(已知星期二) - A = 365,  365%7=1(因为是已知的B大所以星期二-1=星期一),C - B同理。

           第二种方法是,您还可以从公元前 4713 年 1 月 1 日开始的计算,这天规定为星期一,值:> JD  0。那么拿 JD 2458176.00 - JD 0 = JD 2458176.00 ,接着拿 JD 2458176.00 % 7 = 0,因为结果为0即不变而这天规定是星期一,SO A 也是星期一啦~ 

 

而后,儒略日的计算

     计算两个日期之间的天数,利用儒略日计算也很方便,先计算出两个日期的儒略日数, 然后直接相减就可以得到两个日期相隔的天数。

    儒略日计算公式1(多用于格里历): JD = \left \lfloor \frac{153m+2}{5} \right \rfloor+365y+\left \lfloor \frac{y}{4} \right \rfloor-\left \lfloor \frac{y}{100}\right \rfloor+\left \lfloor \frac{y}{400} \right \rfloor+day-32045

    儒略日计算公式2(多用于儒略历):JD = \left \lfloor \frac{153m+2}{5} \right \rfloor+365y+\left \lfloor \frac{y}{4} \right \rfloor+day-32083

    其中,m、y 的公式为 :      m = month +12a- 3,   y = year + 4800 - a ,

    其中,a 的推导公式为:   a = \left \lfloor \frac{14-month}{12} \right \rfloor

p.s. 

    儒略历是 Julius Caesar发明的。一年有12个月,大月31日,小月30日,平年2月有28,日闰年2月则有29日,平均每年有365.25日。到1582年10月由格勒哥里十三世(Gregory XIII)改革成为格里历(Gregorian calendar),取消1582年10月5日至1582年10月14日这10日及取消400年内00年尾的3个闰年,使一年的平均日数变成365.2425日,更接近于准确的回归年365.2422日。

     在公元1582年10月15日之前,人们使用的历法是源自古罗马的儒略历,儒略历的置闰规则就是四年一闰,但是没有计算每年多出来的0.0078天,这样从公元前46年到公元1582年一共累积多出了10天,为此,当时的教皇格里十三世将1582年10月5日人为指定为10月15日,并开始启用新的置闰规则,这就是后来沿用至今的格里历

     如果您用的操作系统是 Unix 或 Linux,在控制台输入以下命令:

​#cal 9 1752

   日历算法 [ 解读算法的乐趣 ]_第3张图片 

     1752 年的 9 月缺了 11 天,因为从儒略历到格里历的转换造成的。在欧洲研究历史,你会发现很多事件都是有多个时间版本的,比如大科学家 牛顿 的生日就有两个时间版本,一个是按照儒略历历法的 1642 年 12 月 25 日,另一个是格里历历法的 1643 年 1 月 4 日。对于英国人来说,1752 年之前都是按照儒略历计算的,所以英国的史书 可能会记载牛顿出生在圣诞节,这也没什么可奇怪的。

#include 

bool is_calendar(int year, int month, int day)
{
    if(year < 1582)
        return false;        // 采用儒略历

    if(year == 1582)
    {
        if( (month < 10) || ((month == 10) && (day < 15)) )
            return false;    // 采用儒略历
    }

    return true;             // 采用格里历
}

double JD(int year, int month, int day, int hour, int minute, double second)
{
	int a = (14 - month) / 12;
	int y = year + 4800 - a;
	int m = month + 12 * a - 3;
	double jdn = day + (153 * m + 2) / 5 + 365 * y + y / 4;
	if ( is_calendar(year, month, day) )
	{
		    jdn = jdn - y / 100 + y / 400 - 32045.5;      // 采用格里历
	}
	else
	{
		jdn -= 32083.5;                                   // 采用儒略历
	}
	// 由于是从中午12:00开始, 计算日期是0:00开始所以要加或减0.5个儒略日
	return (jdn + hour / 24.0 + minute / 1440.0 + second / 86400.0);
}

int main( int argc, char *argv[] )
{
	int year = 2019;
	int month = 2;
	int day = 26;
	int hour = 12;
	int minute = 0;
	int secode = 0;
//	scanf("%d%*c%d%*c%d%*c%d%*c%d%*c%d%*c", &year, &month, &day, &hour, &minute, &secode);
	printf("%.2f\n",JD(year, month, day, hour, minute, secode));
//  因为像儒略历的时间位数较长,想实现一个每 3 位(西方习惯)、或者 每 4 位(东方习惯)就分隔的函数,有想法吗 ??
	return 0;
}
#include 

bool is_calendar(int year, int month, int day)
{
    if(year < 1582)
        return false;        // 采用儒略历

    if(year == 1582)
    {
        if( (month < 10) || ((month == 10) && (day < 15)) )
            return false;    // 采用儒略历
    }

    return true;             // 采用格里历
}

double JD(int year, int month, int day, int hour, int minute, double second)
{
	int a = (14 - month) / 12;
	int y = year + 4800 - a;
	int m = month + 12 * a - 3;
	double jdn = day + (153 * m + 2) / 5 + 365 * y + y / 4;
	if ( is_calendar(year, month, day) )
	{
		    jdn = jdn - y / 100 + y / 400 - 32045.5;      // 采用格里历
	}
	else
	{
		jdn -= 32083.5;                                   // 采用儒略历
	}
	// 由于是从中午12:00开始, 计算日期是0:00开始所以要加或减0.5个儒略日
	return (jdn + hour / 24.0 + minute / 1440.0 + second / 86400.0);
}

int main( int argc, char *argv[] )
{
	int year = 2019;
	int month = 2;
	int day = 26;
	int hour = 12;
	int minute = 0;
	int secode = 0;
//	scanf("%d%*c%d%*c%d%*c%d%*c%d%*c%d%*c", &year, &month, &day, &hour, &minute, &secode);
	printf("%.2f\n",JD(year, month, day, hour, minute, secode));
//  因为像儒略历的时间位数较长,想实现一个每 3 位(西方习惯)、或者 每 4 位(东方习惯)就分隔的函数,有想法吗 ??
	return 0;
}

 蔡勒(Zeller)公式  

       蔡勒是一个公式可以直接根据日期计算出对应的星期,很NB吧 ! 

       推导原理仍然是通过两个日期的时间差来计算星期,只是通过选择一个 特殊的日期 来简化公式的推导。这个特殊日期指的是某一年的 12 月 31 日这天刚好是星期日这种情况。 选择这样的日子有两个好处,

        一是:> 计算上可以省去计算标准日期这一年的剩余天数,

        二是:> 计算出来的日期差余数是几就是星期几,不需要再计算星期的差值。

        公元元年的 1 月 1 日是星期一,那么公元 1 年的 12 月 31 日就是星期日,用这一天作为标准日期,就可以只计算整数年的时间和日期所在的年积累的天数。这个星期公式如下:

  •          w = (L * 366 +N * 365+D)~ mod ~7 

         D 是一年的第几天,代码实现计算D值:

// 判断某日是一年的第几天
#include 

bool is_leap_year(int year)
{
    return year%(year%100?4:400)?false:true;
}

int main(int argc, char *argv[])
{

	int day, month, year, sum, leap;

	printf("%s", "请输入年,月,日: >  ");
	scanf("%d%*c%d%*c%d", &year, &month, &day);
	switch (month)
	{
	case 1:
		sum = 0;
		break;
	case 2:
		sum = 31;
		break;
	case 3:
		sum = 59;
		break;
	case 4:
		sum = 90;
		break;
	case 5:
		sum = 120;
		break;
	case 6:
		sum = 151;
		break;
	case 7:
		sum = 181;
		break;
	case 8:
		sum = 212;
		break;
	case 9:
		sum = 243;
		break;
	case 10:
		sum = 273;
		break;
	case 11:
		sum = 304;
		break;
	case 12:
		sum = 334;
		break;
	default:
		printf("%s", "data error!");
		break;
	}

	sum += day;
	if ( is_leap_year(year) )
		leap = 1;
	else
		leap = 0;
	if (leap = 1 && month > 2)
		sum++;
	printf("D = %d\n", sum);

	return 0;
}

        公式中的 L 是从公元元年到 year 年 month 月 day 日所在的年之间发生闰年的次数,N 是平常年的次数,D 是 year 年内的积累天数。将整年数 year - 1 = L + N 带入上式,可得:

  •          w = ((year-1)*365+L+D)~mod~7

        根据闰年规律,从公元元年到 y 年之间的闰年次数是可以计算出来的,即:

  •           L = \left \lfloor \frac{year-1}{4}\right \rfloor-\left \lfloor \frac{year-1}{100} \right \rfloor+\left \lfloor \frac{year-1}{400} \right \rfloor

        将 L = \left \lfloor \frac{year-1}{4}\right \rfloor-\left \lfloor \frac{year-1}{100} \right \rfloor+\left \lfloor \frac{year-1}{400} \right \rfloor 代入 w = ((year-1)*365+L+D)~mod~7,可得:

  •           w = ((year-1)*365+ \left \lfloor \frac{year-1}{4} \right \rfloor-\left \lfloor \frac{year-1}{100} \right \rfloor+\left \lfloor \frac{year-1}{400} \right \rfloor+D)~mod~7

        以 2019年2月26日计算,就是:

  •           w = ((2019-1)*365+ \left \lfloor \frac{2019-1}{4} \right \rfloor-\left \lfloor \frac{2019-1}{100} \right \rfloor+\left \lfloor \frac{2019-1}{400} \right \rfloor+57)~mod~7          
  •                =        ( 736 570     +       504         -          20         +         5           +  57)   %  7
  •                =                                                737 116    %     7
  •                =                                                                 2

         结果为 2 即这天是 星期 2。不过这个公式并不适合口算~

         德国数学家克里斯蒂安·蔡勒(Christian Zeller, 1822—1899)在 1886 年推导出了著名的为蔡勒 公式:

  •           w = (y+\left \lfloor \frac{y}{4} \right \rfloor+\left \lfloor \frac{c}{4} \right \rfloor-2c+\left \lfloor \frac{13(m+1)}{5} \right \rfloor+d-1)~mod~7

        y:  年份,取公元纪念的后两位,如 2016 年,y = 16,2003 年,y = 3;

        c:  世纪数 - 1 的值,如 21 世纪,则 c = 20;

       m: 月数,m 的取值是大于等于 3,小于等于 14。在蔡勒公式中,某年的 1 月和 2 月看作上一 年的 13月和 14月,比如 2001年 2月 1日要当成 2000年的 14月 1日计算。

       d:  某月内的日数

            蔡勒公式有时候计算出的结果可能是负 数,需要对结果+7 进行修正。比如 2006 年 7 月 1 日,用蔡勒公式计算出的结果是 -1,实际上这 天是星期六,这是用于格里历法)

           用于儒略历的星期计算机公式:

  •             w = (5-c+y+\left \lfloor \frac{y}{4} \right \rfloor+\left \lfloor \frac{13(m+1)}{5} \right \rfloor+d - 1)~mod~7

p.s. 1582 年 10 月 4 日之前才是儒略历,1582 年 10 月 5 日与 1582 年 10 月15 日之间的日期是不存在的,因为TA们都是同一天。具体原因可以可上面的p.s故事。另外为了方便口算,人们通常将公式中的 [ 13(m +1) / 5 ] 一项改成 [ 26(m +1) /10 ][向下取整]

  日历算法 [ 解读算法的乐趣 ]_第4张图片

           改进版代码实现: 

   // 写法略有不同,因为这样更简洁。您可以自己实现描述版的。 
    int zeller_week(int y, int m, int d)  /* 0 = Sunday */
    {
    static int t[] = {0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4};
    y -= m < 3;
    return (y + y/4 - y/100 + y/400 + t[m-1] + d) % 7;
    }

 输出12个月份表

日历算法 [ 解读算法的乐趣 ]_第5张图片

            简洁与高效并重因此我们采用 查表 就好。先上代码,逐布分解的讲理理思路~

日历算法 [ 解读算法的乐趣 ]_第6张图片

#include 

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

bool is_leap_year(int year)
{
	return year % (year % 100 ? 4 : 400) ? false : true;
}

int zeller_week(int y, int m, int d)	/* 0 = Sunday */
{
	static int t[] = { 0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4 };
	y -= m < 3;
	return (y + y / 4 - y / 100 + y / 400 + t[m - 1] + d) % 7;
}

void print_space(int fweek)
{
	int cnt = fweek;

	for (int i = 0; i < cnt; i++)
	{
		printf("%s", "        ");     // 需要自己调节宽度
	}
}


// 确定每月的天数
int get_m_days(int year, int month)
{
	if (0 > month || 12 < month)
		return 0;
	static int days = m_days[month - 1];
	if (2 == month && is_leap_year(year))
		days++;

	return days;
}

void print_calendar(int year, int month)
{
	int days = get_m_days(year, month);	// 调用函数,得到指定月天数
	int one_day = zeller_week(year, month, 1);
	print_space(one_day);
	int week = one_day;
	int i = 1;
	while (i <= days)
	{
		printf("%8d", i);               // 需要自己调节宽度
		if (week == 6)			/* 到一周结束,切换到下一行输出 */
		{
			putchar(10);
		}
		i++;
		week = (week + 1) % 7;
	}
}

  // 每个月第一天的星期数可以用蔡勒公式计算,之后每天不必重复使用蔡勒公式,用 
  // week = (week + 1) % 7 直接推算就可以了

int main(int argc, char *argv[])
{
	int y, m;
	printf("%s", "请输入年、月:>  ");
	scanf("%d%*c%d", &y, &m);
	printf(" \n  %s  %s  %s  %s  %s  %s  %s\n", "星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六");
	print_calendar(y, m);    // 产生 12 个月的日历循环 12 次即可

	return 0;
}

 

   首先呢,先把目标定位为打印一个月份,之前说过我们决定使用 查表 指代每月的天数,用一个数组存储。

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

   而后,因为还有平年和闰年之分。所以,需要额外 add 一个函数,这个函数要返回某个月准备的天数。这样函数声明原型您应该清楚了,只需要俩个参数,一个是年份、另一个是具体的月份。

// 确定每月的天数
int get_m_days(int year, int month)
{
	if (0 > month || 12 < month)
		return 0;
	static int days = m_days[month - 1];
	if ( 2 == month && is_leap_year(year) )
		days++;

	return days;
}

    接着是核心算法啦,确定每个月的天数之后,就可以依次排列输出这个月的所有日期。星期的位置是固定的,每个月的第一天是星期几就从星期几对应的位置开始排列数字,遇到星期六 (位置从星期日到星期六排列) 就换行继续输出。  

void print_calendar(int year, int month)
{
	int days = get_m_days(year, month);	        // 调用函数,得到指定月天数
	int one_day = zeller_week(year, month, 1);  // 得到每月的第一天是星期几
	print_space(one_day);                       // 格式化输出保证准备的格式

	int week = one_day;                         // 知道第一天就可不重复计算
	int i = 1;
	while (i <= days)
	{
		printf("%8d", i);                       // 需要自己调节宽度,不通用
		if (week == 6)			                // 一周结束,换到下一行输出 
		{
			putchar(10);
		}
		i++;
		week = (week + 1) % 7;                  // 星期几的机制本质就是取模
	}
}

       最后写测试代码,可以把 星期几 换成英文 , 如 星期天 -> Su 、星期一 -> Mo。     


       格里历其实并不精确,但是,尽管如此却是最适合人类的。

       精确就要细度化,不要用天计时了,用秒,比如“7,987,654,321“ 宇宙秒时,我们复习功课”。你想过这样的生活吗?这对于以后的计算机或需非常适应,因为对机器来说,一个编号就是一个对象。比如我们说,能帮我拿一下钥匙吗,TA在办公室往前数第3张桌子上,如果是机器说那么是这样,能帮我拿一下钥匙吗,TA在 0000 0001 号 0000 0003 桌,果然还是用格里历吧。


    二十四节气      

       中国古代历法都是以月亮运行规律为主,严格按照朔望月长度定义月,但是由于朔望月长度 和地球回归年长度无法协调,会导致农历季节和天气的实际冷暖无法对应,因此聪明的古人将月 亮运行规律和太阳运行规律相结合制定了中国农历的历法规则。

       在这种特殊的阴阳结合的历法规 则中,二十四节气就扮演着非常重要的作用,它是联系月亮运行规律和太阳运行规律的纽带。正 是由于二十四节气结合置闰规则,使得农历的春夏秋冬四季和地球绕太阳运动引起的天气冷暖变 化相一致,成为中国几千年来生产和生活的依据。

      二十四节气起源于中国黄河流域。远在春秋时代,古人就开始使用仲春、仲夏、仲秋和仲冬 四个节气指导农耕种植。后来经过不断地改进与完善,到秦汉年间,二十四节气已经基本确立。

      公元前 104 年(汉武帝太初元年),汉武帝颁布由邓平等人制定的《太初历》,正式把二十四节气 订于历法,明确了二十四节气的天文位置。二十四节气天文位置的定义,就是从太阳黄经零度开 始,沿黄经每运行 15 度所经历的时日称为一个节气。太阳一个回归年运行 360 度,共经历二十 四个节气,每个公历月对应两个节气。其中,每月第一个节气为“节令”,即立春、惊蛰、清明、 立夏、芒种、小暑、立秋、白露、寒露、立冬、大雪和小寒十二个节令;每月的第二个节气为“中 气”,即雨水、春分、谷雨、小满、夏至、大暑、处暑、秋分、霜降、小雪、冬至和大寒十二个 中气。“节令” 和“中气”交替出现,各历时 15 天,人们习惯上把“节令”和“中气”统称为 “节气”。

 

[ 更新ing ... ]

你可能感兴趣的:(算法导论)