日历相关的算法

由于黄赤交角的存在,导致太阳直射点在南北回归线内交替变化,而不是始终直射赤道,从而引起四季的更替。公历(GregorianCalendar)的四季变化点:春分秋分(Equinox),冬至夏至(Solstice)。二分点昼夜时长相等,二至点昼夜分别达到最长。

地球绕太阳的轨道叫黄道,不过在地心说的古代,黄道就是太阳绕地球的轨道。黄道和赤道相交的日子,也就是春分或者秋分,被叫做黄道吉日,民间传说这天适合婚嫁,不过一年才两天太少不够用,所以老黄历有一套计算黄道吉日的方法。

除了4个季节变化点,中国古代把360°的地球轨迹又分成24节气,每个节气运动15°,这对指导农业生产至关重要。另外中气对于农历的历法计算也非常重要。二十四节气中,偶数的节气叫”中气”,比如”雨水”和”春分”就是”中气”。我们小时候背过的二十四节气歌:春雨惊春清谷天,夏满芒夏暑相连,秋处露秋寒霜降,冬雪雪冬小大寒。

如果大家仔细观察UTC的缩写,可能会觉得奇怪,协调世界时的三个英文单词的缩写应该是CUT,怎么是UTC呢?这里还有个小故事。它的法文是TempsUniverselCordonné,缩写是TUC,当时制定游戏规则的人都想用自己母语的缩写,争来争去达成妥协,谁的都不用,就用UTC了。

公历又叫格列历、阳历和太阳历,中国在辛亥革命后的民国元年开始采用。它的前身是儒略历(Juliancalendar),这是西方十六世纪前采用的历法,由儒略·凯撒颁发。它一年12个月,月的天数就是我们现在公历的方法,除了2月受闰年的影响,其余各月天数固定,并分大小月。4年一闰年。这样平均一年365.25天。比实际公转周期的365.2422日长11分14秒,即每400年约长3日,所以为了矫正这个误差,公历的闰年又加了一条规则:如果能被100整除,那么必须被400整除。以前觉得这个规则很诡异,看到这个后就非常自然了——相对于每400年去掉3个闰年。这样每年平均长365.2425日,与公转周期的365.2422日十分接近,可基本保证到公元5000年之前的误差不超过1天。

过去中国人说年龄,一般说多少岁,指的是虚岁。虚岁的精确定义是某个人从出生到现在为止经历过的农历的岁的数量。比如某人出生在农历一九九九年九月九日,那么到了两千年的一月一日(大年初一)他就两岁(虚岁),也就是他在这个世界时经历了一九九九年(岁)和二零零零年(岁),因此通常虚岁要比周岁大1.5岁左右。古代男子二十弱冠,表示成年,换成现在也就是18.5周岁左右,和我们现在的成年标准相差不大。虚岁中经历的岁的定义是有分歧的,有的地方认为过了冬至(阳历)就长了一岁,有的地方是过春节(阴历)或者立春(阳历)。

前面说了中国的农历有24节气,其中对于历法来说,冬至是最为重要的节气。周朝的时候以冬至所在的月为一年的开始。汉代以后,这个月变成十一月,它之后的第二个月成为正月。正月初一为春节。由于1岁=12.37月,如果这一个岁包含了完整的12个月(朔望月,也就是大约29.5日),那么这一个岁就叫闰岁。由于一岁包含至少13个月,而只有12个中气,那么至少有一个月没有中气,因此农历规定没有中气的月为闰月,闰月的天数和上一个月一样多,包含闰月的年叫闰年。

这一年的11月是闰月,所以这年是闰年,但它不包含12个完整的月,所以不是闰岁。2034年是闰岁,但不是闰年。

如果立春在春节前,那么这年就叫聋(子)年,比如2010年就是,如果这个聋子年下半年还不包含另一个立春,那么就叫双聋年。如果这年包含两个立春,那么就叫双春年。

由于需要的测量月亮的变化来确定,所以测量点非常重要。1929年前的测量点都是在北京,经度为东经116°25′,使用的时区却是120°的东八区,1949年后搬到了南京紫金山天文台,这个地方的经度是东经118°46′。这个变化看似微小,却引发了1978年”连续两天中秋节”的故事。

农历每天的开始是凌晨0:00,有的历法是把中午12点作为一天的开始。

农历里新月是一个月的第一天,有的历法把满月后的一天作为下月第一天。一个农历月是29.5天,具体到每个月,根据观察会有大月(30天)和小月(29)的区别。月的”计算”也会比较复杂,而且会出现连续多个”大月”的情况,比如1990年十月到1991年一月连续4个大月。

我们现在说2011年五月五日(端午节)其实是非常不伦不类的,农历的说法应该是辛卯年五月初五。公元纪年是公历的用法,我国过去是干支纪年法。十天干是:甲(jiǎ)、乙(yǐ)、丙(bǐng)、丁(dīng)、戊(wù)、己(jǐ)、庚(gēng)、辛(xīn)、壬(rén)、癸(guǐ)。十二地支是:子(zǐ)、丑(chǒu)、寅(yín)、卯(mǎo)、辰(chén)、巳(sì)、午(wǔ)、未(wèi)、申(shēn)、酉(yǒu)、戌(xū)、亥(hài)。如果两两组合,那么共有120种可能,不过天干地支用来纪年是并不是任意两个组合都可以的。它的方法是先天干和地支两两配对。甲子、乙丑、丙寅、丁卯、戊辰、己巳、庚午、辛未、壬申、癸酉,这时天干用完了,地支还剩两个,于是天干循环使用甲戌、乙亥,这时地支也用完了,那么也循环使用,最终得到60中可能的组合,仔细分析,其实就是天干和地支奇数和奇数能配对,偶数和偶数能配对。所以如果有人说甲丑年,那么别忙着算啦。这样的方法只能计数一甲子,也就是60年,然后循环使用,所以60年前和60年后的干支纪年没有差别。过了六十岁的人自称过了花甲之年,也就是过了一个甲子(60年)。

辛亥革命后到1949年前人们废除了干支纪年的方法,并且使用了公历,但是民间农历依然盛行,却没法说宣统xx年了,所以使用了民国xx年。这种纪年法1949年在大陆被废除,采用了公元纪年法,但是现在在台湾,人们仍然说民国xx年。

上面说了一大堆,可能工程师最关心的就是换算了。因为日常生活使用公历就足够了,但是有些传统节日的计算需要把农历转换成公历。也就是说把农历转成公历是最常见的用途,不过如果做个什么算命网站,公历转农历也是需要的。

下面是公历转农历的算法:

SimpleDateFormatdf=newSimpleDateFormat("yyyy-MM-dd");Datedate=df.parse("2011-6-6");ChineseCalendarcal=newChineseCalendar(date);System.out.println(cal.get(ChineseCalendar.YEAR));System.out.println(cal.get(ChineseCalendar.MONTH));System.out.println(cal.get(ChineseCalendar.DATE));System.out.println(cal.get(ChineseCalendar.IS_LEAP_MONTH));上面的代码输出说明2011年6月6日就是农历的五月初五,并且五月不是闰月(IS_LEAP_MONTH),因此可以判定是节日。

农历转公历的算法如下:

注:这部分内容也是之前博客的内容,目前除了遗留代码不再建议使用Date和Calendar了。

Calendar是一个接口,获取Calendar实例的方法一般是CalendarrightNow=Calendar.getInstance();Calendar的getInstance静态方法会根据本地的时区(TimeZone)和区域(Locale)选择合适的Calendar,比如我们这里,返回的其实是GregorianCalendar,因为中国官方的历法现在是日历。JDK1.6的getInstance()方法的会调用如下方法:

privatestaticCalendarcreateCalendar(TimeZonetimezone,Localelocale){if("th".equals(locale.getLanguage())&&"TH".equals(locale.getCountry()))returnnewBuddhistCalendar(timezone,locale);if("JP".equals(locale.getVariant())&&"JP".equals(locale.getCountry())&&"ja".equals(locale.getLanguage()))returnnewJapaneseImperialCalendar(timezone,locale);elsereturnnewGregorianCalendar(timezone,locale);}除了泰国和日本,其余都是GregorianCalendar。中国现在官方的历法也是公历。泰国使用的佛历,其实基本就是公历,只不过纪年的开始不是基督耶稣的诞辰年,而是佛祖的诞辰年。日本的历法也是公历,不过纪年采用天皇的年号,这个比较麻烦,等现任天皇驾崩后又得维护数据了。

Calendar的set方法会修改Fields(比如修改年月日等),但不会离开触发计算,必须调用getgetTime等方法后才会现算。Calendar默认是”宽容”的,比如设置1月32日,不过出错,它会计算成2月1日。一个月的第一周,很可能一个周分布在两个月中,这样可能导致一个月有5周,那么怎么样这个周算这个月呢?getMinimalDaysInFirstWeek告诉我们,包含此周的天数如果大于等于这个值,那么这周就是这个月,默认是1,也就是说如果一个周分布在两个月,那么这个周同时属于两个月。每周的第一天是哪天?getFirstDayOfWeek()默认是Sunday,如果计算时想要符合中国人的习惯,那么需要设置这个值为Monday。

ChineseCalendarnewyear=ChineseCalendar.ofNewYear(2020);ChineseCalendarqingming=newyear.with(ChineseCalendar.SOLAR_TERM,SolarTerm.MINOR_03_QINGMING_015);System.out.println(newyear.transform(PlainDate.axis()));//2020-01-25System.out.println(qingming.transform(PlainDate.axis()));//2020-04-04计算冬至冬至(WinterSolstice)是阳历的一个节气,我们可以计算某年冬至对于的阴历日期:

然后把这个日期转换成农历(ChineseCalender),最后输出农历日期。ChronoFormatter用于格式化的输出,主要使用了addPatten。其中EEE代表周几,d代表日期(农历),MMMM代表月,r代表年,U代表干支纪年,最后通过addText(ChineseCalendar.SOLAR_TERM)输出节气。

下面的代码计算2019年的端午节对应的公历日期:

ChineseCalendarduanwu=ChineseCalendar.of(EastAsianYear.forGregorian(2019),EastAsianMonth.valueOf(5),5);System.out.println(duanwu.transform(PlainDate.axis()));输出:2019-06-07ChineseCalendar.of有三个参数,分别代表农历年、月和日。在大陆,我们使用公历年来定义农历年,也就是和公历2019年重合天数最多的那个农历年就是农历二零一九年。如果是台湾,我们可以使用:

ChineseCalendarduanwu=ChineseCalendar.of(EastAsianYear.forMinguo(108),EastAsianMonth.valueOf(5),5);System.out.println(duanwu.transform(PlainDate.axis()));民国108年也就是公元2019年。

农历2020年闰四月,下面的代码把闰四月五日转换成公历:

ChineseCalendardate=ChineseCalendar.of(EastAsianYear.forGregorian(2020),EastAsianMonth.valueOf(4).withLeap(),5);System.out.println(date.transform(PlainDate.axis()));输出:2020-05-27上面代码的关键是EastAsianMonth.valueOf(4).withLeap(),表示我们要的是闰四月。

如果这个月不是闰月,我们要求闰月就会抛出异常,比如:

ChineseCalendardate=ChineseCalendar.of(EastAsianYear.forGregorian(2020),EastAsianMonth.valueOf(3).withLeap(),5);System.out.println(date.transform(PlainDate.axis()));2020年三月不是闰月,因此上面的代码会抛出异常指明Invaliddate。

我们通常想知道某年是否闰年(有闰月),并且具体哪个月是闰月。time4j没有直接的API,但是我们可以使用上面的异常来判断是否闰月,我们可以封装这个函数:

publicstaticbooleanisLeapMonth(intyear,intmonth){try{ChineseCalendar.of(EastAsianYear.forGregorian(year),EastAsianMonth.valueOf(month).withLeap(),1);returntrue;}catch(Exceptione){returnfalse;}}有了上面的函数,我们可以来判断某年是否有闰月,并且哪个月是闰月。

for(inty=2010;y<=2020;y++){for(intm=1;m<=12;m++){if(isLeapMonth(y,m)){System.out.printf("%d/%disleapmonth.%n",y,m);}}}判断一个月是大月还是小月下面的代码判断2019年五月的天数,30天就是大月,29是小月。

上面所有的类都提供很多类型的构造函数,同时都接受通用的Object作为构造函数的参数,这样的目的是便于把各种类型转换成Joda-Time的对象。比如DateTime的构造函数可以使用如下类型的参数来构造:

比如下面的代码把java.util.Date对象转换成DateTime:

Joda-Time的对象都是不可修改的,但是我们可以基于已有对象产生新的对象,比如:

除了基本的get方法(比如getMonthOfYear),Joda-Time也提供属性方法(比如monthOfYear),它返回一个属性对象,这个对象可以提供更加多的功能,比如:

DateTimedt=newDateTime();StringmonthName=dt.monthOfYear().getAsText();StringfrenchShortName=dt.monthOfYear().getAsShortText(Locale.FRENCH);booleanisLeapYear=dt.year().isLeap();DateTimerounded=dt.dayOfMonth().roundFloorCopy();dt.getMonthOfYear()和dt.monthOfYear().get()是一样的,但是dt.monthOfYear()可以更多的方法,比如根据不同的Locale生成不同的文本。对于year()我们还可以调用isLeap判断是否闰年。

Calendar使用继承的方式来扩展,比如使用GregorianCalendar来实现抽象类Calendar。但是不同的历法差别很大,很难有太多的共性,所以Joda-Time使用插件的方式来扩展。

比如我们要使用埃及历法,那么可以使用下面的代码:

另外大家看文档时会发现和我们学的英语不同,Joda-Time使用CE和BCE代表公元和公元前,而不是BC和AD,其实它们是相同的意思。BC是BeforeChrist的缩写,也就是圣人诞生之前;而AD是AnnoDomini的缩写,它是一个拉丁语,意思就是圣人诞生之年。因为BC和AD基督教气味太浓(凭什么圣人是耶稣而不是孔子或者老子?),所以有人使用CE和BCE。CE是CommonEra的缩写,也就是公元的意思;而BCE是BeforeCommonEra的缩写,也就是公元前的意思。

下面是简单的示例代码:

DateTimedt=newDateTime(2005,3,26,12,0,0,0);DateTimeplusPeriod=dt.plus(Period.days(1));DateTimeplusDuration=dt.plus(newDuration(24L*60L*60L*1000L));当然,大部分情况下Period.days(1)和newDuration(24L60L60L*1000L)是相同的。但是对于很多欧洲国家实行夏令时,那么在切换的那天可能就要多出一个小时来。

注:理论上还有闰秒的影响,但是因为闰秒是人为规定的,无法用程序预先计算,因此大部分日历库包括Joda-Time多不支持闰秒。

每个Field都对应一个Property。Property里有更详细的信息。比如getYear()返回当前年,而year()函数返回一个Property,我们可以用这个Property来查询是否闰年。

两个时刻,和大部分Java的区间概念类似,这是半开半闭的。

两个时刻的差值,其实就是毫秒数,可以是负数,表示前者比后者晚。

Joda-Time使用DateTimeZone来表示时区,我们可以用洲/城市来说明时区,比如:

另外对于UTC时区,因为要经常使用,所以有一个特殊的静态变量:

DateTimeZonezoneUTC=DateTimeZone.UTC;如果我们知道某个时区和UTC的时差(-12到12),也可以这样:

DateTimeZonedefaultZone=DateTimeZone.getDefault();使用接口在Java里,我们习惯使用接口,比如:

Listlist=newArrayList<>();但是前面介绍过了,Joda-Time的设计不是通过基础而是使用插件,因此基类里没有太多有用的东西,我们通常直接使用对应的类,比如:

DateTimedt=newDateTime();而不是使用:

和Date的转换:

//fromJodatoJDKDateTimedt=newDateTime();DatejdkDate=dt.toDate();//fromJDKtoJodadt=newDateTime(jdkDate);和Calendar的转换:

//fromJodatoJDKDateTimedt=newDateTime();CalendarjdkCal=dt.toCalendar(Locale.CHINESE);//fromJDKtoJodadt=newDateTime(jdkCal);因为Calendar需要Locale来显示不同的字符串,因此通常需要Locale,所以toCalendar有一个Locale的参数。如果传入null,则使用系统默认的Locale。

和GregorianCalendar的转换:

intiDoW=dt.getDayOfWeek();这里1表示周一,7表示周日。当然如果怕记错,也可以使用DateTimeConstants.SUNDAY。而之前的Calendar.SUNDAY是1,这是和很多人(至少中国人)的常识是不符合的,这是按照西方把周日当成一周的开始。但是Joda-Time这么搞某些西方人会不会抗议违反了他们的常识?

因此最好的办法还是不要这样:

if(dt.getDayOfWeek()==7){//gotosleep}应该写成:

if(dt.getDayOfWeek()==DateTimeConstants.SUNDAY){//gotosleep}访问属性属性里有更加详细的信息:

DateTime.PropertypDoW=dt.dayOfWeek();StringstrST=pDoW.getAsShortText();//returns"Mon","Tue",etc.StringstrT=pDoW.getAsText();//returns"Monday","Tuesday",etc.注:上面的输出依赖于默认的Locale。如果在作者的电脑上运行的话输出是”星期六”。我们也可以明确的指定Locale:

StringstrTF=pDoW.getAsText(Locale.FRENCH);//returns"Lundi",etc.修改属性我们可以把日期改到星期一:

DateTimeresult=dt.dayOfWeek().setCopy(DateTimeConstants.MONDAY);上面会产生一个新的DateTime对象。把日期修改为星期一的含义是什么呢?如果今天是星期二,那么相当于减一天;今天是星期天,那么减6天。

下面的代码求三天后的日期:

DateTimeresult=dt.plusDays(3);修改时区DateTimedt=newDateTime();DateTimedtLondon=dt.withZone(DateTimeZone.forID("Europe/London"));它的输出是:

2019-03-30T21:47:40.755+08:002019-03-30T13:47:40.755Zdt和dtLondon表示的时刻都是相同的,但是时区相差8小时。

新的API的行为更加明确,比如调用是传入null通常会触发异常。

新的API使用起来更加流畅。因为不能返回null,因此通常可以链式的调用,比如:

LocalDatetoday=LocalDate.now();LocalDatepayday=today.with(TemporalAdjusters.lastDayOfMonth()).minusDays(2);看不懂没关系,后面我们会介绍,但是也许我们可以猜测出来它是找到这个月的倒数第三天。

之前的Calendar包括SimpleDateFormat等由于可变导致不是线程安全,带来很多并发的问题。如果自己做线程的同步,又很费劲。新的API的大部分(常见)对象都是不可变的,这样可以放心大胆的随便传递。我们每次的”修改”都不会改变原来的对象,只是会产生新的对象,比如:

LocalDatedateOfBirth=LocalDate.of(2012,Month.MAY,14);LocalDatefirstBirthday=dateOfBirth.plusYears(1);这些”修改”的函数都是以of、from和with开头,并且所有的对象没有set方法。

新的API容易扩展,比如你可以写自己的TemporalAdjusters,这个在后面会介绍。

ISO-8061之外的其它历法,你也可以自己实现(估计没人会有兴趣)。

扩展API,主要是给库的开发者用到。

时区,处理时区我们通常需要ZonedDateTime、ZoneId和ZoneOffset这些类。

THE END
1.干支纪年法简便算法初中历史所涉及的四种纪年法公元纪年,也称公历纪年,或基督纪年。它以相传的耶稣基督诞生年即公元元年作为历史算起,在中国这一年正好是西汉平帝元始元年。以这一年为界,在此以前的时间称公元前多少年,在此以后的时间和公元多少年,或直接称XX年(注意,不能写成公元后XX年)。这就是公元纪年法。 https://blog.csdn.net/weixin_39581318/article/details/111040757
2.探究历史新知:揭开未来十年序幕——从第一个20年代的年代及其开启具体的计算方法很简单:世纪的计算是在年份的前两位数基础上加一,如20世纪是前两位数为“二十”,而百年后则是“二十一”。年代的划分则是以十位数的数字为标准,例如,年份的十位数是几,那就是几零年代。从公元纪年开始计算,我们可以知道每一年所处的世纪和年代。 http://www.qaszl.com/Bb9AAE330740.html
3.路程和时间的计算物理教案13篇(全文)2. 干支纪年法的简便算法 传统的计算法, 必须知道一个已知年, 然后往前后推算, 有一定的难度, 下面我给大家介绍一种干支纪年法的简便算法, 每个字都有对应的一个符号。如下表: 年份的最后一个数字就对应天干的相应的字, 然后用年份除以12所得的余数, 用这个余数去找相对应的地支字和十二生肖字, 于是天干地https://www.99xueshu.com/w/filesr7pz31h.html
4.公元纪年法1公元纪年法 编辑 【释义】现在通行的公元纪年,就是所谓“耶稣出生”之年算起。耶稣出生之年就是公元元年,以前的年份叫公元前某年,从这年起叫公元某年,例如,陈胜吴广起义于公元前209年,淝水之战于公元383年。这种算法以及所谓的“耶稣出生”之年,是6世纪的一个基督修道士狄安尼西提出的。虽然耶稣只是宗教传说中https://baike.sogou.com/v7595587.htm
5.世纪年代公元的算法.要告诉为什么要这样算.在通行的公元纪年,就是所谓“耶稣出生”之年算起.耶稣出生之年就是公元元年,以前的年份叫公元前某年,从这年起叫公元某年.这种算法以及所谓的“耶稣出生”之年,是6世纪的一个基督修道士狄安尼西提出的.虽然耶稣只是宗教传说中的人物,但是这个纪年标志逐渐在全世界通用.根据公元纪年和中国历史纪年对照换算,公元元年是https://www.zybang.com/question/787331e3903b50839ad2b77cf82739e6.html
6.我们学习了公元纪年法的换算,请你算一算今年(公元2010年)是多少刷刷题APP(shuashuati.com)是专业的大学生刷题搜题拍题答疑工具,刷刷题提供我们学习了公元纪年法的换算,请你算一算今年(公元2010年)是多少世纪的什么时期A.20世纪早期B.20世纪晚期C.21世纪早期D.21世纪晚期的答案解析,刷刷题为用户提供专业的考试题库练习。一分钟将考试https://www.shuashuati.com/ti/afaf7c4593f84e93964353cd85dae30a.html
7.《竹书纪年》(精选十篇)这样公元前任何一年的干支纪年都可以在公元1年到公元60年之间找到对应的等值年。那么我们能不能用数学的周期和平移知识将公元前纪年平移到公元1年到公元60年之间进行干支纪年的等值对应呢?只要找到了对应规律, 就可以用公元后的尾数余数法。 要对应成功首先必须解决公元前纪年的数学问题。 (1) 把历史表述转换为数学https://www.360wenmi.com/f/cnkey7rc3kt9.html
8.纪年转换和年代计算方法及试题试题按照公元纪年法,这一年属于( ) A.9世纪60年代 B.10世纪60年代 C.9世纪50年 D.10世纪50年代 3.汉武帝在位时,首创了用年号纪年的方式,于公元前140年定年号为“建元”,这一年就是“建元元年”。请算一算,“建元三年(公元前138年)”应属于( ) A.公元前2世纪 B.公元前1世纪 C.公元1世纪 D.公元2https://wbblishi.com/post/324.html
9.闰年怎么判断闰年的计算方法闰年2月有多少天闰年计算方法3闰年的计算方法:公历纪年法中:能被4整除的大多是闰年;能被100整除而不能被400整除的年份不是闰年;能被3200整除的也不是闰年;如1900年是平年,2000年是闰年,3200年不是闰年。闰年共有366天(1-12月分别为31天,29天,31天,30天,31天,30天,31天,31天,30天,31天,30天,31天)。凡阳历中有闰日http://sx.ychedu.com/SXJA/ELJJA/598444.html