详尽分享算法系列:日历算法

简介: 详尽分享算法系列:日历算法

Copyright ? 1900-2016, NORYES, All Rights Reserved.

欢迎转载,请保留此版权声明。

---

转载自

1、概述

日历在我们的生活中扮演着十分重要的角色,上班、上学、约会都离不开日历。每年新年开始,人们都要更换新的日历,你想知道未来一年的这么多天是怎么被确定下来的吗?为什么去年的国庆节是星期五而今年的国庆节是星期三?那就来研究一下日历算法吧。本文将介绍日历的编排规则,确定某日是星期几的计算方法,以及如何在计算机上打印某一年的年历。

要研究日历算法,首先要知道日历的编排规则,也就是历法。所谓历法,指的就是推算年、月、日的时间长度和它们之间的关系,指定时间序列的法则。我国的官方历法是中国公历,也就是世界通用的格里历(Gregorian Calendar),中国公历的年分为平常年和闰年,平常年一年是365天,闰年一年是366天。判定一年是平常年还是闰年的规则如下:

1、如果年份是 4 的倍数,且不是 100 的倍数,则是闰年;

2、如果年份是 400 的倍数,则是闰年;

3、不满足 1、2 条件的就是平常年。

总结成一句话就是:四年一闰,百年不闰,四百年再闰。

中国公历关于月的规则是这样的,一年分为十二个月,其中一月、三月、五月、七月、八月、十月和十二月是大月,一个月有 31 天。四月、六月、九月和十一月是小月,一个月有 30 天。二月天数要根据是否是闰年来定,如果是闰年,二月是 29 天,如果是平常年,二月是 28 天。

2、计算星期

除了年月日,人们日常生活中还对日期定义了另一个属性,就是星期几。星期并不是公历范畴内的东西,但是人们已经习惯用星期来管理和规划时间,比如一个星期工作五天,休息两天等等,星期的规则彻底改变了人们的生活习惯,因此星期已经成为历法中的一部分了。星期的命名最早起源于古巴比伦文化。公元前 7-6 世纪,巴比伦人就使用了星期制,一个星期中的每一天都有一个天神掌管。这一规则后来传到古罗马,并逐渐演变成现在的星期制度。

如何知道某一天到底是星期几?除了查日历之外,是否有办法推算出来某一天是星期几呢?答案是肯定的,星期不象年和月那样有固定的历法规则,但是星期的计算也有自己的规律。星期是固定的 7 天周期,其排列顺序固定,不随闰年、平常年以及大小月的天数变化影响。因此,只要确切地知道某一天是星期几,就可以推算出其它日期是星期几。推算的方法很简单,就是计算两个日期之间相差多少天,用相差的天数对 7 取余数,这个余数就是两个日期的星期数的差值。举个例子,假设已经知道 1977 年 3 月 27 日是星期日,如何得知 1978 年 3 月 27 日是星期几?按照前面的方法,计算出 1977 年 3 月 27 日到 1978 年 3 月 27 日之间相差 365 天,365 除以 7 余数是 1,所以 1978 年 3 月 27 日就是星期一。

上述方法计算星期几的关键是求出两个日期之间相隔的天数。有两种常用的方法计算两个日期之间相隔的天数,一种是利用公历的月和年的规则直接计算,另一种是利用儒略日计算。利用公历规则直接计算两个日期之间相差的天数,简单地讲就是将两个日期之间相隔的天数分成三个部分:前一个日期所在年份还剩下的天数、两个日期之间相隔的整数年所包含的天数和后一个日期所在的年过去的天数。如果两个日期是相邻两个年份的日期,则第二部分整年的天数就是 0。以 1977 年 3 月 27 日到 2005 年 5 月 31 日为例,1977 年还剩下的天数是 279 天,中间整数年是从 1978 年到 2005 年(不包括2005 年),共 26 年,包括 7 个闰年和 20 个平常年,总计 9862 天,最后是 2005 年从 1 月 1 日到 5 月 31 日经过的天数 151 天。三者总和 10292 天。直接利用公历规则计算日期相差天数的算法实现如下(为了简化算法复杂度,这个实现假设用于定位星期的那个日期总是在需要计算星期几的那个日期之前):

int CalculateDays(int ys, int ms, int ds, int ye, int me, int de)

{

int days = CalcYearRestDays(ys, ms, ds);

if(ys != ye) /不是同一年的日期/

{

if((ye - ys) >= 2) /间隔超过一年,要计算间隔的整年时间/

{

days += CalcYearsDays(ys + 1, ye);

}

days += CalcYearPassedDays(ye, me, de);

}

else

{

days = days - CalcYearRestDays(ye, me, de);

}

return days;

}

/计算一年中过去的天数,包括指定的这一天/

int CalcYearPassedDays(int year, int month, int day)

{

int passedDays = 0;

int i;

for(i = 0; i < month - 1; i++)

{

passedDays += daysOfMonth【i】;

}

passedDays += day;

if((month > 2) IsLeapYear(year))

passedDays++;

return passedDays;

}

/计算一年中还剩下的天数,不包括指定的这一天/

int CalcYearRestDays(int year, int month, int day)

{

int leftDays = daysOfMonth【month - 1】 - day;

int i;

for(i = month; i < MONTHES_FOR_YEAR; i++)

{

leftDays += daysOfMonth【i】;

}

if((month <= 2) IsLeapYear(year))

leftDays++;

return leftDays;

}

/

计算years年1月1日和yeare年1月1日之间的天数,

包括years年1月1日,但是不包括yeare年1月1日

/

int CalcYearsDays(int years, int yeare)

{

int days = 0;

int i;

for(i = years; i < yeare; i++)

{

if(IsLeapYear(i))

days += DAYS_OF_LEAP_YEAR;

else

days += DAYS_OF_NORMAL_YEAR;

}

return days;

}

另一种计算两个日期相差天数的方法是利用儒略日(Julian Day,JD)进行计算。首先介绍一下儒略日,儒略日是一种不记年,不记月,只记日的历法,是由法国学者 Joseph Justus Scaliger(1540-1609)在 1583 年提出来的一种以天数为计量单位的流水日历。儒略日和儒略历(Julian Calendar)没有任何关系,命名为儒略日也仅仅他本人为了纪念他的父亲--意大利学者 Julius Caesar Scaliger(1484-1558)。简单来讲,儒略日就是指从公元前 4713 年 1 月 1 日 UTC 12:00 开始所经过的天数,JD0 就被指定为公元前 4713 年 1 月 1 日 12:00 到公元前 4713 年 1 月 2 日 12:00 之间的 24 小时,依次顺推,每一天都被赋予一个唯一的数字。例如从 1996 年 1 月 1 日 12:00 开始的一天就是儒略日JD2450084。使用儒略日可以把不同历法的年表统一起来,很方便地在各种历法中追溯日期。如果计算两个日期之间的天数,利用儒略日计算也很方便,先计算出两个日期的儒略日数,然后直接相减就可以得到两个日期相隔的天数。

由公历的日期计算出儒略日数是一个很简单的事情,有多个公式可以计算儒略日,本文选择如下公式计算儒略日:

其中 y 是年份,m 是月份,d 是日期,如果 m 小于或等于 2,则 m 修正为 m+12,同时年份修正为 y-1。c 值由以下方法计算:

下面就是由公历日期计算儒略日的算法实现:

119 int CalculateJulianDay(int year, int month, int day)

120 {

121 int B = 0;

122

123 if(month <= 2)

124 {

125 month += 12;

126 year -= 1;

127 }

128 if(IsGregorianDays(year, month, day))

129 {

130 B = year / 100;

131 B = 2 - B + year / 400;

132 }

133

134 double dd = day + 0.5000115740; /本日12:00后才是儒略日的开始(过一秒钟)/

135 return int(365.25 (year + 4716) + 0.01) + int(30.60001 (month + 1)) + dd+ B - 1524.5;

136 }

儒略日的计算通常精确到秒,得到的 JD 数也是一个浮点数,本文仅仅是为了计算日期相隔的整数天数,因此都采用整数计算。由于儒略日的周期开始与每天中午12:00,而历法中的天数通常是从0:00开始的,因此儒略日计算上对日期的天数进行了修正。1977年3月27日的儒略日是2443230,2005年5月31日的儒略日是2453522,差值是10292,和前一种方法计算的结果一致。

我们用两种方法计算出两个日期之间的天数都是10292,现在用10292除以7得到余数是2,也就是说2005年5月31日与1977年3月27日星期数差两天,所以2005年5月31日就是是星期二。

上述计算星期的方法虽然步骤简单,但是每次都要计算两个日期的时间差,不是非常方便。如果能够有一个公式可以直接根据日期计算出对应的星期岂不是更好?幸运的是,这样的公式是存在的。此类公式的推导原理仍然是通过两个日期的时间差来计算星期,只是通过选择一个特殊的日期来简化公式的推导。这个所谓的特殊日期指的是某一年的 12 月 31 日这天刚好是星期日这种情况。选择这样的日子有两个好处,一个是计算上可以省去计算标准日期这一年的剩余天数,另一个是计算出来的日期差余数是几就是星期几,不需要再计算星期的差值。人们知道公元元年的 1 月 1 日是星期一,那么公元前 1 年的 12 月 31 日就是星期日,用这一天作为标准日期,就可以只计算整数年的时间和日期所在的年积累的天数,这个星期公式就是:

w = (L 366 + N 365 + D) % 7 (公式 2)

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

w = ( (y - 1) 365 + L + D) % 7 (公式 3)

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

将L带入公式2,得到星期w的最终计算公式:

还以2005年5月31日为例,利用公式5计算w的值为:

得到 2005 年 5 月 31 日是星期二,和前面的计算方法得到的结果一致。根据上述分析,可得写出使用公式 5 计算星期的算法实现:

146 int TotalWeek(int year, int month, int day)

147 {

148 int d = CalcYearPassedDays(year, month, day);

149 int y = year - 1;

150 int w = y DAYS_OF_NORMAL_YEAR + y / 4 - y / 100 + y / 400 + d;

151

152 return w % 7;

153 }

公式 5 的问题在于计算量大,不利于口算星期结果。于是人们就在公式 5 的基础上继续推导更简单的公式。德国数学家克里斯蒂安·蔡勒(Christian Zeller, 1822- 1899)在 1886 年推导出了著名的蔡勒(Zeller)公式:

对计算出的 w 值除以7,得到的余数就是星期几,如果余数是0,则为星期日。蔡勒公式中各符号的含义如下:

w :星期;

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

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

y :年份,取公元纪念的后两位,如1998年, = 98,2001年, = 1;

d :某月内的日数

为了方便口算,人们通常将公式6中的一项改成。

目前人们普遍认为蔡勒公式是计算某一天是星期几的最好的公式。但是蔡勒公式有时候可能计算出的结果是负数,需要对结果+7进行修正。比如2006年7月1日,用蔡勒公式计算出的结果是 -1,实际上这天是星期六。根据前面分析的结果整理出的蔡勒公式算法实现如下:

155 int ZellerWeek(int year, int month, int day)

156 {

157 int m = month;

158 int d = day;

159

160 if(month <= 2) /对小于2的月份进行修正/

161 {

162 year--;

163 m = month + 12;

164 }

165

166 int y = year % 100;

167 int c = year / 100;

168

169 int w = (y + y / 4 + c / 4 - 2 c + (13 (m + 1) / 5) + d - 1) % 7;

170 if(w < 0) /修正计算结果是负数的情况/

171 w += 7;

172

173 return w;

174 }

蔡勒公式(公式6)和前面提到的公式 5 都只适用于格里历法。罗马教皇在 1582 年修改历法,将 10 月 5 日指定为 10 月 15 日,从而正式废止儒略历法,开始启用格里历法。因此,上述求星期几的公式只适用于 1582 年 10 月 15 日之后的日期,对于 1582 年将 10 月 4 日之前的日期,蔡勒也推导出了适用与儒略历法的星期计算公式:

公式 7 适用于对 1582 年 10 月 4 日之前的日期计算星期,1582 年 10 月 5 日与 1582 年 10 月 15 日之间的日期是不存在的,因为它们都是同一天。

格里历历法简单,除二月外每月天数固定,二月则根据是否是闰年确定是 28 天还是 29 天,每天的星期数可以通过蔡勒公式(公式 6)计算,有了这些信息,就可以按照一定的排版格式将某一年的日历打印出来。排版打印的算法非常简单,就是按照顺序打印 12 个月的月历,因此,打印月历的函数就是输出算法的重点。代码没什么特别之处,就是用一些小技巧确定每个月的第一天的开始位置,打印月历的核心代码如下:

229 void PrintMonthCalendar(int year, int month)

230 {

231 int days = GetDaysOfMonth(year, month); /确定这个月的天数/

232 if(days <= 0)

233 return;

234

235 PrintMonthBanner(nameOfMonth【month - 1】);

236 PrintWeekBanner();

237 int firstDayWeek = ZellerWeek(year, month, 1);

238 InsertRowSpace(firstDayWeek);

239 int week = firstDayWeek;

240 int i = 1;

241 while(i <= days)

242 {

243 printf("%-10d", i);

244 if(week == 6) /到一周结束,切换到下一行输出/

245 {

246 SetNextRowStart();

247 }

248 i++;

249 week = (week + 1) % 7;

250 }

251 }

GetDaysOfMonth()函数其实就是从daysOfMonth表中查一下每月的天数,如果是闰年,则对二月的天数修正(+1),daysOfMonth表定义如下:

int daysOfMonth【MONTHES_FOR_YEAR】 = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};

计算星期不必对每一天都计算一次,只要对每个月的第一天计算一次就可以了,以后的日期可以用 week = (week + 1) % 7 直接推算出星期几。下面就是我们的算法打印输出的效果:

**

Calendar of 2012

**

--January--

Sunday Monday Tuesday Wednesday Thursday Friday Saturday

1 2 3 4 5 6 7

8 9 10 11 12 13 14

15 16 17 18 19 20 21

22 23 24 25 26 27 28

29 30 31

--February--

Sunday Monday Tuesday Wednesday Thursday Friday Saturday

1 2 3 4

5 6 7 8 9 10 11

12 13 14 15 16 17 18

19 20 21 22 23 24 25

26 27 28 29

--March--

Sunday Monday Tuesday Wednesday Thursday Friday Saturday

1 2 3

4 5 6 7 8 9 10

11 12 13 14 15 16 17

18 19 20 21 22 23 24

25 26 27 28 29 30 31

……

2、二十四节气

二十四节气在中国古代历法中扮演着非常重要的角色,本文将介绍二十四节气的基本知识,以及如何使用 VSOP82/87 行星运行理论计算二十四节气发生的准确时间。

中国古代历法都是以月亮运行规律为主,严格按照朔望月长度定义月,但是由于朔望月长度和地球回归年长度无法协调,会导致农历季节和天气的实际冷暖无法对应,因此聪明的古人将月亮运行规律和太阳运行规律相结合制定了中国农历的历法规则。在这种特殊的阴阳结合的历法规则中,二十四节气就扮演着非常重要的作用,它是联系月亮运行规律和太阳运行规律的纽带。正是由于二十四节气结合置闰规则,使得农历的春夏秋冬四季和地球绕太阳运动引起的天气冷暖变化相一致,成为中国几千年来生产、生活的依据。

二十四节气起源于中国黄河流域。远在春秋时代,古人就开始使用仲春、仲夏、仲秋和仲冬四个节气指导农耕种植。后来经过不断地改进与完善,到秦汉年间,二十四节气已经基本确立。公元前 104年,汉武帝颁布由邓平等人制定的《太初历》,正式把二十四节气订于历法,明确了二十四节气的天文位置。二十四节气天文位置的定义,就是从太阳黄经零度开始,沿黄经每运行15度所经历的时日称为“一个节气”。太阳一个回归年运行360度,共经历24个节气,每个公历月对应2个节气。其中,每月第一个节气为“节令”,即:立春、惊蛰、清明、立夏、芒种、小暑、立秋、白露、寒露、立冬、大雪和小寒等12个节令;每月的第二个节气为“中气”,即:雨水、春分、谷雨、小满、夏至、大暑、处暑、秋分、霜降、小雪、冬至和大寒等12个中气。“节令”和“中气”交替出现,各历时15天,人们习惯上把“节//代码效果参考:http://www.zidongmutanji.com/bxxx/164806.html

令”和“中气”统称为“节气”。

为了更好地理解二十四节气的天文位置,首先要解释几个天文学概念。“天球”是人们为了研究天体的位置和运动规律而引入的一个假象的球体,根据观察点(也就是球心)的位置不同,可分为“日心天球”、“地心天球”等等。图(1)就是天球概念的一个简单示意图:

图(1)天球概念示意图

天文学中常用的一个坐标体系就是“地心天球”,它与地球同心且有相同的自传轴,理论上具有无限大的半径。地球的赤道和南北极点延伸到天球上,对应着天赤道和南北天极点。和地球上用经纬度定为位置一样,天球也划分了经纬度,分别命名为“赤经”和“赤纬”,地球上的经度用的是度(分秒)为单位,赤经以时(分秒)为单位。天空中的所有天体都可以投射到天球上,用赤经和赤纬定为天体在天球上的位置。“黄道(Ecliptic)”是地球绕太阳公转轨道的轨道平面与天球(地心天球)相交的大圆,由于地球公转受月球和其它行星的摄动,地球的公转轨道并不是严格的平面,因此黄道的严格定义是:地月系质心绕太阳公转的瞬时平均轨道平面与天球相交的大圆。黄道和天赤道所在的两个平面并不是重叠的,它们之间存在一个23度26分的交角,称为“黄赤交角”。由于黄赤交角的存在,黄道和天赤道就在天球上有两个交点,这两个交点就是春分点和秋分点。在天球上以黄道为基圈可以形成黄道坐标系,在黄道坐标系中,也使用了经纬度的概念,分别称为“黄经”和“黄纬”。天体的黄经从春分点起沿黄道向东计量,春分点是黄经//代码效果参考:http://www.zidongmutanji.com/bxxx/171794.html

0度,沿黄道一周是360度,使用的单位是度、分和秒。黄纬以黄道测量平面为准,向北记为0度到90度,向南记为0度到-90度。

黄道平面可以近似理解为地球绕太阳公转的平面,以黄道为基圈的黄道坐标系根据观测中心是太阳还是地球还可以区分为日心坐标系和地心坐标系,对应天体的黄道坐标分别被称为“日心黄经、日心黄纬”和“地心黄经、地心黄纬”。日心黄经和日心黄纬比较容易理解,因为太阳系的行星都是绕太阳公转,以太阳为中心将这些行星向天球上投影是最简单的确定行星位置关系的做法。但是人类自古观察太阳的周年运动,都是以地球为参照,以太阳的周年视运动位置来计算太阳的运行轨迹,使用的其实都是地心黄经和地心黄纬,要了解古代历法,理解这一点非常重要。图(2)就解释了造成这种视觉错觉的原因:

图(2)太阳黄道视觉位置原理图

古人定义二十四节气的位置,是太阳沿着黄道运行时的视觉位置,每个节气对应的黄道经度其实是地心黄经。从图(2)可以看出日心黄经和地心黄经存在180度的转换关系,同样可以理解,日心黄纬和地心黄纬在方向上是反的,因此可以很方便地将两类坐标相互转换,转换公式是:

太阳地心黄

相关文章
|
5月前
|
算法 C++
详尽分享算法生成青天白日满地红旗
详尽分享算法生成青天白日满地红旗
74 1
|
24天前
|
算法 安全 数据安全/隐私保护
基于game-based算法的动态频谱访问matlab仿真
本算法展示了在认知无线电网络中,通过游戏理论优化动态频谱访问,提高频谱利用率和物理层安全性。程序运行效果包括负载因子、传输功率、信噪比对用户效用和保密率的影响分析。软件版本:Matlab 2022a。完整代码包含详细中文注释和操作视频。
|
1月前
|
机器学习/深度学习 算法 数据安全/隐私保护
基于MSER和HOG特征提取的SVM交通标志检测和识别算法matlab仿真
### 算法简介 1. **算法运行效果图预览**:展示算法效果,完整程序运行后无水印。 2. **算法运行软件版本**:Matlab 2017b。 3. **部分核心程序**:完整版代码包含中文注释及操作步骤视频。 4. **算法理论概述**: - **MSER**:用于检测显著区域,提取图像中稳定区域,适用于光照变化下的交通标志检测。 - **HOG特征提取**:通过计算图像小区域的梯度直方图捕捉局部纹理信息,用于物体检测。 - **SVM**:寻找最大化间隔的超平面以分类样本。 整个算法流程图见下图。
|
9天前
|
算法 数据挖掘 数据安全/隐私保护
基于FCM模糊聚类算法的图像分割matlab仿真
本项目展示了基于模糊C均值(FCM)算法的图像分割技术。算法运行效果良好,无水印。使用MATLAB 2022a开发,提供完整代码及中文注释,附带操作步骤视频。FCM算法通过隶属度矩阵和聚类中心矩阵实现图像分割,适用于灰度和彩色图像,广泛应用于医学影像、遥感图像等领域。
|
10天前
|
算法 调度
基于遗传模拟退火混合优化算法的车间作业最优调度matlab仿真,输出甘特图
车间作业调度问题(JSSP)通过遗传算法(GA)和模拟退火算法(SA)优化多个作业在并行工作中心上的加工顺序和时间,以最小化总完成时间和机器闲置时间。MATLAB2022a版本运行测试,展示了有效性和可行性。核心程序采用作业列表表示法,结合遗传操作和模拟退火过程,提高算法性能。
|
11天前
|
存储 算法 决策智能
基于免疫算法的TSP问题求解matlab仿真
旅行商问题(TSP)是一个经典的组合优化问题,目标是寻找经过每个城市恰好一次并返回起点的最短回路。本文介绍了一种基于免疫算法(IA)的解决方案,该算法模拟生物免疫系统的运作机制,通过克隆选择、变异和免疫记忆等步骤,有效解决了TSP问题。程序使用MATLAB 2022a版本运行,展示了良好的优化效果。
|
10天前
|
机器学习/深度学习 算法 芯片
基于GSP工具箱的NILM算法matlab仿真
基于GSP工具箱的NILM算法Matlab仿真,利用图信号处理技术解析家庭或建筑内各电器的独立功耗。GSPBox通过图的节点、边和权重矩阵表示电气系统,实现对未知数据的有效分类。系统使用MATLAB2022a版本,通过滤波或分解技术从全局能耗信号中提取子设备的功耗信息。
|
10天前
|
机器学习/深度学习 算法 5G
基于MIMO系统的SDR-AltMin混合预编码算法matlab性能仿真
基于MIMO系统的SDR-AltMin混合预编码算法通过结合半定松弛和交替最小化技术,优化大规模MIMO系统的预编码矩阵,提高信号质量。Matlab 2022a仿真结果显示,该算法能有效提升系统性能并降低计算复杂度。核心程序包括预编码和接收矩阵的设计,以及不同信噪比下的性能评估。
27 3
|
21天前
|
人工智能 算法 数据安全/隐私保护
基于遗传优化的SVD水印嵌入提取算法matlab仿真
该算法基于遗传优化的SVD水印嵌入与提取技术,通过遗传算法优化水印嵌入参数,提高水印的鲁棒性和隐蔽性。在MATLAB2022a环境下测试,展示了优化前后的性能对比及不同干扰下的水印提取效果。核心程序实现了SVD分解、遗传算法流程及其参数优化,有效提升了水印技术的应用价值。
|
22天前
|
机器学习/深度学习 算法 数据安全/隐私保护
基于贝叶斯优化CNN-LSTM网络的数据分类识别算法matlab仿真
本项目展示了基于贝叶斯优化(BO)的CNN-LSTM网络在数据分类中的应用。通过MATLAB 2022a实现,优化前后效果对比明显。核心代码附带中文注释和操作视频,涵盖BO、CNN、LSTM理论,特别是BO优化CNN-LSTM网络的batchsize和学习率,显著提升模型性能。