本文将从编程角度介绍一下日历,包括闰年、闰月的判断和计算,天干地支的表示,阳历、农历之间的相互转换,最后再介绍一种应用场景。不包含UI上的绘制,只提供数据上的支撑。
(1)日历基础知识
从阳历来讲,一年有365天或366天。平年2月28天,闰年2月29天。大月31天,小月30天。1、3、5、7、9、10、12月固定为大月,4、6、9、11月固定为小月。地球绕太阳转一周,就是一年。
从农历来讲,平年有354或355天。大月30天,小月29天,闰年多出一个月。大月、小月并不固定。月亮绕地球一周,就是一月,初一为“朔日”,十五为“望日”。
阳历闰年判断:能被400整除或能被4整除但不能被100整除的年。如2000、2012、2020是闰年,2010、2022、2023是平年。每4年一闰。
农历闰年判断:含闰月的年份即闰年。比如2020年、今年、2025、2028都是闰年。
闰月判断:闰月是农历的概念,并不固定,所以没有特定的算法。只能通过事先准备好的数据来获取。
(2)农历闰月信息及处理
本小节介绍一下农历的闰月、月份天数以及如何获取它们,时间从1900年到2100年。数据采用20位bit的十六进制表示,并遵循下面的规则:
1-4位:如果是闰年,且对应的闰月是大月的话,为1,否则为0;只有这两个取值;
5-16位:除了闰月外的正常月份是大月还是小月,1为30天,0为29天;
17-20位:非闰年为0,大于0表示闰月月份,仅当存在闰月的情况下有意义。
举例说明:今年(2023)在后面的数据中对应的数据是0x05b52,转换成二进制是:
0000 0101 1101 0101 0010
后四位不为0,十进制是2,表示今年农历会闰二月;前四位为0,说明闰二月只有29天。中间的十二位对应大小月,以5-8位的0101来说明,正月为0,说明它是小月,只有29天;二月为1,说明是大月,有30天;三月为0,小月,29天;······以此类推。
具体数据:
final static long[] lunarInfo = new long[]{
0x04bd8, 0x04ae0, 0x0a570, 0x054d5, 0x0d260, 0x0d950, 0x16554, 0x056a0, 0x09ad0, 0x055d2,//1900-1909
0x04ae0, 0x0a5b6, 0x0a4d0, 0x0d250, 0x1d255, 0x0b540, 0x0d6a0, 0x0ada2, 0x095b0, 0x14977,//1910-1919
0x04970, 0x0a4b0, 0x0b4b5, 0x06a50, 0x06d40, 0x1ab54, 0x02b60, 0x09570, 0x052f2, 0x04970,//1920-1929
0x06566, 0x0d4a0, 0x0ea50, 0x06e95, 0x05ad0, 0x02b60, 0x186e3, 0x092e0, 0x1c8d7, 0x0c950,//1930-1939
0x0d4a0, 0x1d8a6, 0x0b550, 0x056a0, 0x1a5b4, 0x025d0, 0x092d0, 0x0d2b2, 0x0a950, 0x0b557,//1940-1949
0x06ca0, 0x0b550, 0x15355, 0x04da0, 0x0a5b0, 0x14573, 0x052b0, 0x0a9a8, 0x0e950, 0x06aa0,//1950-1959
0x0aea6, 0x0ab50, 0x04b60, 0x0aae4, 0x0a570, 0x05260, 0x0f263, 0x0d950, 0x05b57, 0x056a0,//1960-1969
0x096d0, 0x04dd5, 0x04ad0, 0x0a4d0, 0x0d4d4, 0x0d250, 0x0d558, 0x0b540, 0x0b6a0, 0x195a6,//1970-1979
0x095b0, 0x049b0, 0x0a974, 0x0a4b0, 0x0b27a, 0x06a50, 0x06d40, 0x0af46, 0x0ab60, 0x09570,//1980-1989
0x04af5, 0x04970, 0x064b0, 0x074a3, 0x0ea50, 0x06b58, 0x055c0, 0x0ab60, 0x096d5, 0x092e0,//1990-1999
0x0c960, 0x0d954, 0x0d4a0, 0x0da50, 0x07552, 0x056a0, 0x0abb7, 0x025d0, 0x092d0, 0x0cab5,//2000-2009
0x0a950, 0x0b4a0, 0x0baa4, 0x0ad50, 0x055d9, 0x04ba0, 0x0a5b0, 0x15176, 0x052b0, 0x0a930,//2010-2019
0x07954, 0x06aa0, 0x0ad50, 0x05b52, 0x04b60, 0x0a6e6, 0x0a4e0, 0x0d260, 0x0ea65, 0x0d530,//2020-2029
0x05aa0, 0x076a3, 0x096d0, 0x04afb, 0x04ad0, 0x0a4d0, 0x1d0b6, 0x0d250, 0x0d520, 0x0dd45,//2030-2039
0x0b5a0, 0x056d0, 0x055b2, 0x049b0, 0x0a577, 0x0a4b0, 0x0aa50, 0x1b255, 0x06d20, 0x0ada0,//2040-2049
0x14b63, 0x09370, 0x049f8, 0x04970, 0x064b0, 0x168a6, 0x0ea50, 0x06b20, 0x1a6c4, 0x0aae0,//2050-2059
0x0a2e0, 0x0d2e3, 0x0c960, 0x0d557, 0x0d4a0, 0x0da50, 0x05d55, 0x056a0, 0x0a6d0, 0x055d4,//2060-2069
0x052d0, 0x0a9b8, 0x0a950, 0x0b4a0, 0x0b6a6, 0x0ad50, 0x055a0, 0x0aba4, 0x0a5b0, 0x052b0,//2070-2079
0x0b273, 0x06930, 0x07337, 0x06aa0, 0x0ad50, 0x14b55, 0x04b60, 0x0a570, 0x054e4, 0x0d160,//2080-2089
0x0e968, 0x0d520, 0x0daa0, 0x16aa6, 0x056d0, 0x04ae0, 0x0a9d4, 0x0a2d0, 0x0d150, 0x0f252,//2090-2099
0x0d520};//2100
下面是基于上述数据的方法:
获取农历闰月信息,没有返回0:
/**
* 获取闰月,没有返回0
* @param year 农历年份
*/
public static int leapMonth(int year) {
return (int) (lunarInfo[year - 1900] & 0xf);
}
获取农历某年某月的天数(不含闰月):
/**
* 农历某年某月的天数
*
* @param year 农历年
* @param m 月
* @return 天数
*/
public static int monthDays(int year, int m) {
if ((lunarInfo[year - 1900] & (0x10000 >> m)) == 0)
return 29;
else
return 30;
}
获取某年闰月的天数:
/**
* 获取农历某年闰月的天数
* @param y 农历年
*/
public static int leapDays(int y) {
if (leapMonth(y) != 0) {
if ((lunarInfo[y - 1900] & 0x10000) != 0)
return 30;
else
return 29;
} else
return 0;
}
获取某年的总天数,包括含闰月的情况:
/**
* 传回农历年的总天数
*
* @param y 农历年
*/
final private static int lYearDays(int y) {
int i, sum = 348;
for (i = 0x8000; i > 0x8; i >>= 1) {
if ((lunarInfo[y - 1900] & i) != 0)
sum += 1;
}
return (sum + leapDays(y));
}
(3)天干地支、生肖和时辰
天干地支是我国古代的一种历法表示方式。天干有:
甲(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)。
地支和生肖有对应关系,如下:
子-鼠,丑-牛,寅-虎,卯-兔,辰-龙,巳-蛇, 午-马,未-羊,申-猴,酉-鸡,戌-狗,亥-猪。
生肖的叫法现在很普遍,比如今年是兔年。换成地支的叫法是:卯年。加上天干的叫法是:癸卯年。这是怎么来的呢?先来了解一下天干地支纪年与农历纪年的对应关系和换算表:
对应关系:
天干 = year % 10
地支 = year % 12
换算表:
天干 | 甲 | 乙 | 丙 | 丁 | 戊 | 己 | 庚 | 辛 | 壬 | 癸 |
---|---|---|---|---|---|---|---|---|---|---|
“” | 4 | 5 | 6 | 7 | 8 | 9 | 0 | 1 | 2 | 3 |
地支 | 子 | 丑 | 寅 | 卯 | 辰 | 巳 | 午 | 未 | 申 | 酉 | 戌 | 亥 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
“” | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 0 | 1 | 2 | 3 |
year = 2023,根据上面的算式,天干=3,地支=7,再通过换算表即可得到“癸卯年”。一些历史事件的叫法都是通过这种换算得来,比如1911年的辛亥革命,1901年的《辛丑条约》等。
天干地支的组合共有60种,如下图所示:
上图中,是将甲子年作为第一年来计算的。在换算表中,甲和子对应的数值是4。所以这里存在一定的偏移。如果某一年是甲子年,则 (year - 4) 既能被10整除,也能被12整除。10和12的最小公倍数是60,因此year -4 = 60n,即year = 60n + 4 , n = 0、1、2 ······;符合这个等式的年份就是甲子年。以今年为基准,下一个甲子年是2044年,上一个甲子年是1984年。
数据准备:
private final static String[] Gan = new String[]{"甲", "乙", "丙", "丁", "戊",
"己", "庚", "辛", "壬", "癸"};
private final static String[] Zhi = new String[]{"子", "丑", "寅", "卯", "辰",
"巳", "午", "未", "申", "酉", "戌", "亥"};
private final static String[] Animals = new String[]{"鼠", "牛", "虎", "兔",
"龙", "蛇", "马", "羊", "猴", "鸡", "狗", "猪"};
根据农历年获取天干地支纪年:
/**
* 获取天干地支纪年
*
* @param y 农历年份
*/
final public static String cyclical(int y) {
int num = y - 4;
return (Gan[num % 10] + Zhi[num % 12] + "年");
}
这里为什么要减去4呢?是因为偏移量的缘故。数组中以甲和子作为第一项,第一个甲子年是第4年,所以偏移量为4。
生肖计算:
/**
* 生肖计算
* @param y 农历年
*/
final public static String animalsYear(int y) {
return Animals[(y - 4) % 12] + "年";
}
地支除了用来表示年以外,还用来表示时辰,这里稍做记录,如下:
时辰对应的时间都是固定的,很容易获取,这里就不贴代码了。
地支还可以用来表示月份信息,如下:
(4)阳历转农历
本小节介绍一下如何将阳历转换为农历。首先来看二组数据,第一组是关于阳历的,遵循下面的格式:
从1887年到2111年的阳历数据,每一项由年、月、日按照下面的规则组成:
item = (year << 9) | (month << 5) | day
其中year是从1887-2111依次递增,month和day对应的是农历正月初一的阳历月、日,比如今年正月初一是阳历1月22号,那么month = 1,day = 22,对应的数据项是(2023 << 9) | (1 << 5) | 22 = 0xfce36 。
具体数据:
private static int[] solar_1_1 = {1887, 0xec04c, 0xec23f, 0xec435, 0xec649,
0xec83e, 0xeca51, 0xecc46, 0xece3a, 0xed04d, 0xed242, 0xed436,
0xed64a, 0xed83f, 0xeda53, 0xedc48, 0xede3d, 0xee050, 0xee244,
0xee439, 0xee64d, 0xee842, 0xeea36, 0xeec4a, 0xeee3e, 0xef052,
0xef246, 0xef43a, 0xef64e, 0xef843, 0xefa37, 0xefc4b, 0xefe41,
0xf0054, 0xf0248, 0xf043c, 0xf0650, 0xf0845, 0xf0a38, 0xf0c4d,
0xf0e42, 0xf1037, 0xf124a, 0xf143e, 0xf1651, 0xf1846, 0xf1a3a,
0xf1c4e, 0xf1e44, 0xf2038, 0xf224b, 0xf243f, 0xf2653, 0xf2848,
0xf2a3b, 0xf2c4f, 0xf2e45, 0xf3039, 0xf324d, 0xf3442, 0xf3636,
0xf384a, 0xf3a3d, 0xf3c51, 0xf3e46, 0xf403b, 0xf424e, 0xf4443,
0xf4638, 0xf484c, 0xf4a3f, 0xf4c52, 0xf4e48, 0xf503c, 0xf524f,
0xf5445, 0xf5639, 0xf584d, 0xf5a42, 0xf5c35, 0xf5e49, 0xf603e,
0xf6251, 0xf6446, 0xf663b, 0xf684f, 0xf6a43, 0xf6c37, 0xf6e4b,
0xf703f, 0xf7252, 0xf7447, 0xf763c, 0xf7850, 0xf7a45, 0xf7c39,
0xf7e4d, 0xf8042, 0xf8254, 0xf8449, 0xf863d, 0xf8851, 0xf8a46,
0xf8c3b, 0xf8e4f, 0xf9044, 0xf9237, 0xf944a, 0xf963f, 0xf9853,
0xf9a47, 0xf9c3c, 0xf9e50, 0xfa045, 0xfa238, 0xfa44c, 0xfa641,
0xfa836, 0xfaa49, 0xfac3d, 0xfae52, 0xfb047, 0xfb23a, 0xfb44e,
0xfb643, 0xfb837, 0xfba4a, 0xfbc3f, 0xfbe53, 0xfc048, 0xfc23c,
0xfc450, 0xfc645, 0xfc839, 0xfca4c, 0xfcc41, 0xfce36, 0xfd04a,
0xfd23d, 0xfd451, 0xfd646, 0xfd83a, 0xfda4d, 0xfdc43, 0xfde37,
0xfe04b, 0xfe23f, 0xfe453, 0xfe648, 0xfe83c, 0xfea4f, 0xfec44,
0xfee38, 0xff04c, 0xff241, 0xff436, 0xff64a, 0xff83e, 0xffa51,
0xffc46, 0xffe3a, 0x10004e, 0x100242, 0x100437, 0x10064b, 0x100841,
0x100a53, 0x100c48, 0x100e3c, 0x10104f, 0x101244, 0x101438,
0x10164c, 0x101842, 0x101a35, 0x101c49, 0x101e3d, 0x102051,
0x102245, 0x10243a, 0x10264e, 0x102843, 0x102a37, 0x102c4b,
0x102e3f, 0x103053, 0x103247, 0x10343b, 0x10364f, 0x103845,
0x103a38, 0x103c4c, 0x103e42, 0x104036, 0x104249, 0x10443d,
0x104651, 0x104846, 0x104a3a, 0x104c4e, 0x104e43, 0x105038,
0x10524a, 0x10543e, 0x105652, 0x105847, 0x105a3b, 0x105c4f,
0x105e45, 0x106039, 0x10624c, 0x106441, 0x106635, 0x106849,
0x106a3d, 0x106c51, 0x106e47, 0x10703c, 0x10724f, 0x107444,
0x107638, 0x10784c, 0x107a3f, 0x107c53, 0x107e48};
第二组数据是关于农历的,遵循下面的规则:
从后往前计数,后13位,表示大小月信息。1表示大月,30天,0表示小月,29天。之所以有13位,是因为存在闰月的情况。平年第13位永远为0,闰年按照闰月信息依次后移一位。比如今年闰2月,那么在这13位中,第三位表示闰二月的大小月情况,第四位表示三月的大小月情况,依次类推。
前4位,表示闰哪个月,不足4位的前面补0。比如今年对应的数据项是0x49b5,除去后13位得到前四位是0010,十进制数是2,表示闰2月。
具体数据:
private static int[] lunar_month_days = {1887, 0x1694, 0x16aa, 0x4ad5,
0xab6, 0xc4b7, 0x4ae, 0xa56, 0xb52a, 0x1d2a, 0xd54, 0x75aa, 0x156a,
0x1096d, 0x95c, 0x14ae, 0xaa4d, 0x1a4c, 0x1b2a, 0x8d55, 0xad4,
0x135a, 0x495d, 0x95c, 0xd49b, 0x149a, 0x1a4a, 0xbaa5, 0x16a8,
0x1ad4, 0x52da, 0x12b6, 0xe937, 0x92e, 0x1496, 0xb64b, 0xd4a,
0xda8, 0x95b5, 0x56c, 0x12ae, 0x492f, 0x92e, 0xcc96, 0x1a94,
0x1d4a, 0xada9, 0xb5a, 0x56c, 0x726e, 0x125c, 0xf92d, 0x192a,
0x1a94, 0xdb4a, 0x16aa, 0xad4, 0x955b, 0x4ba, 0x125a, 0x592b,
0x152a, 0xf695, 0xd94, 0x16aa, 0xaab5, 0x9b4, 0x14b6, 0x6a57,
0xa56, 0x1152a, 0x1d2a, 0xd54, 0xd5aa, 0x156a, 0x96c, 0x94ae,
0x14ae, 0xa4c, 0x7d26, 0x1b2a, 0xeb55, 0xad4, 0x12da, 0xa95d,
0x95a, 0x149a, 0x9a4d, 0x1a4a, 0x11aa5, 0x16a8, 0x16d4, 0xd2da,
0x12b6, 0x936, 0x9497, 0x1496, 0x1564b, 0xd4a, 0xda8, 0xd5b4,
0x156c, 0x12ae, 0xa92f, 0x92e, 0xc96, 0x6d4a, 0x1d4a, 0x10d65,
0xb58, 0x156c, 0xb26d, 0x125c, 0x192c, 0x9a95, 0x1a94, 0x1b4a,
0x4b55, 0xad4, 0xf55b, 0x4ba, 0x125a, 0xb92b, 0x152a, 0x1694,
0x96aa, 0x15aa, 0x12ab5, 0x974, 0x14b6, 0xca57, 0xa56, 0x1526,
0x8e95, 0xd54, 0x15aa, 0x49b5, 0x96c, 0xd4ae, 0x149c, 0x1a4c, //49b5
0xbd26, 0x1aa6, 0xb54, 0x6d6a, 0x12da, 0x1695d, 0x95a, 0x149a,
0xda4b, 0x1a4a, 0x1aa4, 0xbb54, 0x16b4, 0xada, 0x495b, 0x936,
0xf497, 0x1496, 0x154a, 0xb6a5, 0xda4, 0x15b4, 0x6ab6, 0x126e,
0x1092f, 0x92e, 0xc96, 0xcd4a, 0x1d4a, 0xd64, 0x956c, 0x155c,
0x125c, 0x792e, 0x192c, 0xfa95, 0x1a94, 0x1b4a, 0xab55, 0xad4,
0x14da, 0x8a5d, 0xa5a, 0x1152b, 0x152a, 0x1694, 0xd6aa, 0x15aa,
0xab4, 0x94ba, 0x14b6, 0xa56, 0x7527, 0xd26, 0xee53, 0xd54, 0x15aa,
0xa9b5, 0x96c, 0x14ae, 0x8a4e, 0x1a4c, 0x11d26, 0x1aa4, 0x1b54,
0xcd6a, 0xada, 0x95c, 0x949d, 0x149a, 0x1a2a, 0x5b25, 0x1aa4,
0xfb52, 0x16b4, 0xaba, 0xa95b, 0x936, 0x1496, 0x9a4b, 0x154a,
0x136a5, 0xda4, 0x15ac};
根据范围获取位数信息:
static int getBitInt(int data, int length, int shift) {
return (data & (((1 << length) - 1) << shift)) >> shift;
}
它的含义是:
从后往前数,获取数据的第(shift + 1)到第(shift + length)位。例如:getBitInt(solar11, 5, 0),表示获取数据solar11的后5位信息;getBitInt(solar11, 4, 5)表示获取第6-9位的信息。
如果要计算两个日期之间的间隔天数,第一反应是创建两个Calendar对象,然后相减得到间隔的毫秒数,再除以每天的毫秒数,即可获得。但这里忽略了一个问题,默认创建的Calendar其实是GregorianCalendar,它是以1970年1月1日0点0分0秒开始计时的,即格林威治时间(GMT),又叫Unix时间戳。而这里考虑的时间跨度超出了这个范围,所以不能用它来计算。
下面提供了一种办法,个人也没有完全弄明白为什么这样写,但经测试及实际检验,并没有问题:
private static long solarToInt(int y, int m, int d) {
m = (m + 9) % 12;
y = y - m / 10;
return 365 * y + y / 4 - y / 100 + y / 400 + (m * 306 + 5) / 10
+ (d - 1);
}
拿今天(2023/2/1)来举例,距离阳历新年已经过去的天数:
solarToInt(2023, 2, 1) - solarToInt(2023, 1, 1) 刚好等于31;这个方法可以获取任意两个阳历日期的间隔天数,比如到今天为止,鲁迅先生已经离世31516天,solarToInt(2023, 2, 1) - solarToInt(1936, 10, 19) = 31516。一想起鲁迅先生,眼睛不禁湿润了。
言归正传,现在“万事俱备,只欠东风”了。阳历转农历的计算如下:
public static int[] solarToLunar(int year, int month, int monthDay) {
int[] lunarDate = new int[4];
int index = year - solar_1_1[0];
int data = (year << 9) | (month << 5)
| (monthDay);
int solar11 = 0;
if (solar_1_1[index] > data) {
index--;
}
solar11 = solar_1_1[index];
int y = getBitInt(solar11, 12, 9);
int m = getBitInt(solar11, 4, 5);
int d = getBitInt(solar11, 5, 0);
long offset = solarToInt(year, month,
monthDay) - solarToInt(y, m, d); //已经过去的天数
int days = lunar_month_days[index];
int leap = getBitInt(days, 4, 13);
int lunarY = index + solar_1_1[0];
int lunarM = 1;
int lunarD = 1;
offset += 1;
for (int i = 0; i < 13; i++) {
int dm = getBitInt(days, 1, 12 - i) == 1 ? 30 : 29;
if (offset > dm) {
lunarM++;
offset -= dm;
} else {
break;
}
}
lunarD = (int) (offset);
lunarDate[0] = lunarY;
lunarDate[1] = lunarM;
boolean isLeap = false;
if (leap != 0 && lunarM > leap) {
lunarDate[1] = lunarM - 1;
if (lunarM == leap + 1) {
isLeap = true;
}
}
lunarDate[2] = lunarD;
lunarDate[3] = isLeap ? 1 : 0;
return lunarDate;
}
这个方法用到了上面提到的二组数据,并根据各自的规则来获取需要的信息。下面是进一步的说明:
方法的输入是阳历的年、月、日,返回是包含4条数据的数组lunarDate[],lunarDate[0]表示农历年份,lunarDate[1]表示农历月份,lunarDate[2]表示天数,lunarDate[3]表示是否是闰月,是的话为1,否则为0。
方法里首先将阳历的年、月、日按照上面第一组数据的规则进行处理,然后找到对应的数组下标。它对应的数据项包含了当年农历年首(正月初一)对应的阳历年、月、日,然后依次获得它们,并根据输入参数计算已经过去的天数offset。随后,根据第二组数据的农历信息,获取闰月leap,并通过和offset的对比找到输入日期对应的月份lunarM。如果是闰月,那么lunarM应刚好等于(leap+1)。
以今年的3月22号为例,这一天刚好是闰二月的第一天。因为闰二月,所以leap = 2。农历新年过去的天数offset = solarToInt(2023, 3, 22) - solarToInt(2023, 1, 22) = 59天,今年正月是29天,二月是30天,刚好59天。因此计算得到lunarM = 3,满足lunarM=(leap+1)。
再以4月20号为例,这是农历三月的第一天。leap=2依然成立,过去的天数offset = solarToInt(2023, 4, 20) - solarToInt(2023, 1, 22) = 88天,闰2月是29天,计算得到lunarM = 4,不满足lunarM=(leap+1),因此该月不是闰月,但因为有闰二月的存在,所以用(lunarM - 1)进行修正,得到正确的农历月份:三月。后面的农历月份计算也是据此修正。
(5)农历转阳历
本小节介绍农历转阳历。首先看一组数据,遵循下面的规则:
数据包括1900年到2099年间农历年份的相关信息,共24位bit的16进制表示,其中:
1、前4位表示该年闰哪个月;
2、5-17位表示农历年份13个月的大小月分布,0表示小,1表示大;
3、18-19位表示农历年首对应的公历月份。
4、最后5位表示农历年首对应的公历天数。
以今年的数据项0x24DAB6为例来说明,它对应的二进制数如下:
0 0 1 0 , 0 1 0 0 1 1 0 1 1 0 1 0 1 , 0 1 , 1 0 1 1 0
前四位为0010,10进制数是2,表示闰二月;5-17位(13位)表示农历大小月信息;18-19位为01,表示农历年首对应的是阳历1月份,从实际来看,一般农历年首都在阳历的1月或2月,这里用2位(4种可能)足够表示了;后5位是1 0 1 1 0,10进制数是22,结合来看,表示农历年首对应的是阳历1月22号。
这些规则和上面第(2)小节的有类似之处,不过也有不同,请注意区分。
具体数据如下:
private static final int LUNAR_INFO[] = {
0x84B6BF,/*1900*/
0x04AE53, 0x0A5748, 0x5526BD, 0x0D2650, 0x0D9544, 0x46AAB9, 0x056A4D, 0x09AD42, 0x24AEB6, 0x04AE4A,/*1901-1910*/
0x6A4DBE, 0x0A4D52, 0x0D2546, 0x5D52BA, 0x0B544E, 0x0D6A43, 0x296D37, 0x095B4B, 0x749BC1, 0x049754,/*1911-1920*/
0x0A4B48, 0x5B25BC, 0x06A550, 0x06D445, 0x4ADAB8, 0x02B64D, 0x095742, 0x2497B7, 0x04974A, 0x664B3E,/*1921-1930*/
0x0D4A51, 0x0EA546, 0x56D4BA, 0x05AD4E, 0x02B644, 0x393738, 0x092E4B, 0x7C96BF, 0x0C9553, 0x0D4A48,/*1931-1940*/
0x6DA53B, 0x0B554F, 0x056A45, 0x4AADB9, 0x025D4D, 0x092D42, 0x2C95B6, 0x0A954A, 0x7B4ABD, 0x06CA51,/*1941-1950*/
0x0B5546, 0x555ABB, 0x04DA4E, 0x0A5B43, 0x352BB8, 0x052B4C, 0x8A953F, 0x0E9552, 0x06AA48, 0x6AD53C,/*1951-1960*/
0x0AB54F, 0x04B645, 0x4A5739, 0x0A574D, 0x052642, 0x3E9335, 0x0D9549, 0x75AABE, 0x056A51, 0x096D46,/*1961-1970*/
0x54AEBB, 0x04AD4F, 0x0A4D43, 0x4D26B7, 0x0D254B, 0x8D52BF, 0x0B5452, 0x0B6A47, 0x696D3C, 0x095B50,/*1971-1980*/
0x049B45, 0x4A4BB9, 0x0A4B4D, 0xAB25C2, 0x06A554, 0x06D449, 0x6ADA3D, 0x0AB651, 0x095746, 0x5497BB,/*1981-1990*/
0x04974F, 0x064B44, 0x36A537, 0x0EA54A, 0x86B2BF, 0x05AC53, 0x0AB647, 0x5936BC, 0x092E50, 0x0C9645,/*1991-2000*/
0x4D4AB8, 0x0D4A4C, 0x0DA541, 0x25AAB6, 0x056A49, 0x7AADBD, 0x025D52, 0x092D47, 0x5C95BA, 0x0A954E,/*2001-2010*/
0x0B4A43, 0x4B5537, 0x0AD54A, 0x955ABF, 0x04BA53, 0x0A5B48, 0x652BBC, 0x052B50, 0x0A9345, 0x474AB9,/*2011-2020*/
0x06AA4C, 0x0AD541, 0x24DAB6, 0x04B64A, 0x6a573D, 0x0A4E51, 0x0D2646, 0x5E933A, 0x0D534D, 0x05AA43,/*2021-2030*/
0x36B537, 0x096D4B, 0xB4AEBF, 0x04AD53, 0x0A4D48, 0x6D25BC, 0x0D254F, 0x0D5244, 0x5DAA38, 0x0B5A4C,/*2031-2040*/
0x056D41, 0x24ADB6, 0x049B4A, 0x7A4BBE, 0x0A4B51, 0x0AA546, 0x5B52BA, 0x06D24E, 0x0ADA42, 0x355B37,/*2041-2050*/
0x09374B, 0x8497C1, 0x049753, 0x064B48, 0x66A53C, 0x0EA54F, 0x06AA44, 0x4AB638, 0x0AAE4C, 0x092E42,/*2051-2060*/
0x3C9735, 0x0C9649, 0x7D4ABD, 0x0D4A51, 0x0DA545, 0x55AABA, 0x056A4E, 0x0A6D43, 0x452EB7, 0x052D4B,/*2061-2070*/
0x8A95BF, 0x0A9553, 0x0B4A47, 0x6B553B, 0x0AD54F, 0x055A45, 0x4A5D38, 0x0A5B4C, 0x052B42, 0x3A93B6,/*2071-2080*/
0x069349, 0x7729BD, 0x06AA51, 0x0AD546, 0x54DABA, 0x04B64E, 0x0A5743, 0x452738, 0x0D264A, 0x8E933E,/*2081-2090*/
0x0D5252, 0x0DAA47, 0x66B53B, 0x056D4F, 0x04AE45, 0x4A4EB9, 0x0A4D4C, 0x0D1541, 0x2D92B5 /*2091-2099*/
};
这里还要用到一组数据,用来表示阳历每月前的天数:
/**
* 公历每月前的天数
*/
private static final int DAYS_BEFORE_MONTH[] = {0, 31, 59, 90, 120, 151, 181,
212, 243, 273, 304, 334, 365};
/**
* 支持转换的最小农历年份
*/
public static final int MIN_YEAR = 1900;
/**
* 支持转换的最大农历年份
*/
public static final int MAX_YEAR = 2099;
现在再来计算农历转阳历:
/**
* 将农历日期转换为公历日期
*
* @param year 农历年份
* @param month 农历月
* @param monthDay 农历日
* @param isLeapMonth 该月是否是闰月
* @return 返回农历日期对应的公历日期,year0, month1, day2.
*/
public static final int[] lunarToSolar(int year, int month, int monthDay,
boolean isLeapMonth) {
int dayOffset;
int leapMonth;
int i;
if (year < MIN_YEAR || year > MAX_YEAR || month < 1 || month > 12
|| monthDay < 1 || monthDay > 30) {
throw new IllegalArgumentException(
"Illegal lunar date, must be like that:\n\t" +
"year : 1900~2099\n\t" +
"month : 1~12\n\t" +
"day : 1~30");
}
dayOffset = (LUNAR_INFO[year - MIN_YEAR] & 0x001F) - 1;
if (((LUNAR_INFO[year - MIN_YEAR] & 0x0060) >> 5) == 2)
dayOffset += 31;
for (i = 1; i < month; i++) {
if ((LUNAR_INFO[year - MIN_YEAR] & (0x80000 >> (i - 1))) == 0)
dayOffset += 29;
else
dayOffset += 30;
}
dayOffset += monthDay;
leapMonth = (LUNAR_INFO[year - MIN_YEAR] & 0xf00000) >> 20;
// 这一年有闰月
if (leapMonth != 0) {
if (month > leapMonth || (month == leapMonth && isLeapMonth)) {
if ((LUNAR_INFO[year - MIN_YEAR] & (0x80000 >> (month - 1))) == 0)
dayOffset += 29;
else
dayOffset += 30;
}
}
if (dayOffset > 366 || (!isLeapYear(year) && dayOffset > 365)) {
if (isLeapYear(year))
dayOffset -= 366;
else
dayOffset -= 365;
year += 1;
}
int[] solarInfo = new int[3];
for (i = 1; i < 13; i++) {
int iPos = DAYS_BEFORE_MONTH[i];
if (isLeapYear(year) && i > 2) {
iPos += 1;
}
if (isLeapYear(year) && i == 2 && iPos + 1 == dayOffset) {
solarInfo[1] = i;
solarInfo[2] = dayOffset - 31;
break;
}
if (iPos >= dayOffset) {
solarInfo[1] = i;
iPos = DAYS_BEFORE_MONTH[i - 1];
if (isLeapYear(year) && i > 2) {
iPos += 1;
}
if (dayOffset > iPos)
solarInfo[2] = dayOffset - iPos;
else if (dayOffset == iPos) {
if (isLeapYear(year) && i == 2)
solarInfo[2] = DAYS_BEFORE_MONTH[i] - DAYS_BEFORE_MONTH[i - 1] + 1;
else
solarInfo[2] = DAYS_BEFORE_MONTH[i] - DAYS_BEFORE_MONTH[i - 1];
} else
solarInfo[2] = dayOffset;
break;
}
}
solarInfo[0] = year;
return solarInfo;
}
/**
* 是否是闰年,是返回true,否则返回false
*
* @param year 阳历年份
*/
public static boolean isLeapYear(int year) {
if (year % 400 == 0 || (year % 4 == 0 && year % 100 != 0)) {
return true;
}
return false;
}
主要思想是根据农历年过去的天数dayOffset,来计算阳历所在的月份和天数。下面是详细的步骤说明:
第(1)步,dayOffset = 农历年首对应的阳历日期距离阳历年首的天数,如果是1月份,直接等于天数;如果是2月,那么dayOffset += 31;因为阳历1月是大月,有31天。
第(2)步,根据输入参数month,依次获取对应的农历月份天数,加到dayOffset上;并加上输入参数monthDay。
第(3)步,判断农历闰月情况,如果有闰月,并且输入参数month正是闰月或者大于闰月,那么dayOffset再加上闰月的天数。
第(4)步,进行数据修正,如果此时dayOffset大于366,year加一,dayOffset也进行相应的修正;此外,如果是平年,而dayOffset=366,也进行同样的修正。到此,阳历的年数已确定下来,接下来确定阳历的月和天。
第(5)步,找到大于等于dayOffset对应的月份 i (对应每月前的天数),恰好为阳历的月;再根据月内的偏移、闰年和是否为2月的情况,计算得到天。
(6)更大的范围
总的来说,上面的各种计算过程都是基于已有的多组遵循特定格式的数据。如果没有这些数据,一切无从谈起。那么,如果想获取更大范围的数据呢?比如唐、宋、元、明、清,甚至更早的秦、汉、三国时候的数据。这里记录一款叫做“寿星万年历”的日历,这上面的时间跨度范围非常之广,可称得上真正的万年历。数据可以它里面获取,如果研究一下源码(js实现),找出用代码自动获取数据的方式,那就再好不过了。有需要或者感兴趣的朋友可以深入研究一下。
(7)某时段内的循环提醒事件
上面的小节都是讨论日历数据及其计算,本小节介绍一种在实际场景中的应用。
亲朋好友过生日时,你想及时给他们送去祝福和礼物。一旦错过,就容易留下遗憾。你可以在日历上建立提醒事件,好提前得到通知。
一般来说,这种提醒是循环的,生日每年都要过。
从编程角度,当日期临近时,就需要告诉用户:“你的好友李四三天后过生日”。用户的创建日期可以是任意一天,但只要时间落在[ 今天,3天后 ]这个范围内,就要提醒他。有的人过阳历生日,有的人过农历生日,还有大小月、闰月等情况。年三十是过年,没有三十就二十九。总之,要用最恰当的日期。
扩展一下,不止生日,周报、月报等,也有提前提醒的需要。问题可以更加地一般化,即:在某个时间范围内,有哪些需要提醒的事件,以及最恰当的显示时间是什么?
在本小节中,这个事件限制为循环提醒事件。非循环事件只需比较一下日期即可。
数据准备,事件类Event :
public class Event {
//标题
String title;
//内容
String description;
//事件开始时间
Date startTime;
//是否是农历,1表示农历,0表示阳历
int isLunar;
//循环类型
int repeatType;
public static final int REPEAT_EVERY_WEEK = 1; //每周
public static final int REPEAT_EVERY_MONTH = 2; //每月
public static final int REPEAT_EVERY_YEAR = 3; //每年
}
问题可以归为两部分,第一部分是:判断某个Event是否在两日期之间。第二部分是:修正Event显示的时间,这个时间必须是最合适的。
先来看第一部分,判断某个Event是否在两日期之间,根据循环类型,分别处理:
//event是否在这两个日期之间
static boolean isBetweenTheDate(Calendar beginCalendar, Calendar endCanlendar, Event event) {
//event开始日期
Calendar eventStartCalendar = Calendar.getInstance();
eventStartCalendar.setTime(event.startTime);
//event开始日期毫秒数
long eventStartMill = eventStartCalendar.getTimeInMillis();
long beginMill = beginCalendar.getTimeInMillis();
long endMill = endCanlendar.getTimeInMillis();
if (eventStartMill > endMill) { //event开始日期,比endCanlendar还大,说明不在该区间,返回false
return false;
}
if (event.repeatType == Event.REPEAT_EVERY_WEEK) {
return isBetweenWeek(beginCalendar, endCanlendar, event);
}
if (event.repeatType == Event.REPEAT_EVERY_MONTH) {
return isBetweenWeek(beginCalendar, endCanlendar, event);
}
if (event.repeatType == Event.REPEAT_EVERY_YEAR) {
return isBetweenWeek(beginCalendar, endCanlendar, event);
}
return false;
}
每周循环情况处理,主要思路:
先计算日期区间、Event的startTime是星期几,然后进行对比判断即可。需要处理的异常情况:1)日期区间是否超过一周?如果是,因为Event是周循环,那Event必然在此区间;2)如果出现本周四到下周二这种结束日期对应的星期几比起始日期的小,需要适当的修正。
// 一天的毫秒数
public static final int DAY_MILLISECONDS = 24 * 3600000;
//每周循环事件,是否在这两个日期之间
private static boolean isBetweenWeek(Calendar beginCalendar, Calendar endCanlendar, Event event) {
//event开始日期
Calendar eventStartCalendar = Calendar.getInstance();
eventStartCalendar.setTime(event.startTime);
long beginMill = beginCalendar.getTimeInMillis();
long endMill = endCanlendar.getTimeInMillis();
//如果间隔区间大于7天,那么肯定返回true
if (endMill - beginMill > 7 * DAY_MILLISECONDS) {
return true;
}
int beginDayOfWeek = beginCalendar.get(Calendar.DAY_OF_WEEK);
int endDayOfWeek = endCanlendar.get(Calendar.DAY_OF_WEEK);
int eventStartDayOfWeek = eventStartCalendar.get(Calendar.DAY_OF_WEEK);
if (endDayOfWeek < beginDayOfWeek) { // 会出现这种情况:本周四 —— 下周二
endDayOfWeek += 7;
}
if (eventStartDayOfWeek >= beginDayOfWeek && eventStartDayOfWeek <= endDayOfWeek) {
return true;
}
return false;
}
每年循环情况处理,主要思路:
根据事件Event的startTime,获得对应的月和日;然后判断月和日是否在该日期区间里。判断的方法是比较(month *31 + day)的值,这个31并不代表大月的天数,仅表示权值,只要使得当month加1时,结果大于任何month不变day取任意值的情况,即31。不关心startTime对应的year,因为每年都要发生。如果使用的是农历,将日期转换为阳历。
//每年循环事件,是否在这两个日期之间
private static boolean isBetweenYear(Calendar beginCalendar, Calendar endCanlendar, Event event) {
//event开始日期
Calendar eventStartCalendar = Calendar.getInstance();
eventStartCalendar.setTime(event.startTime);
int eventStartDay, eventStartMonth;
int beginDay, beginMonth;
int endDay, endMonth;
if (event.isLunar == 1) {
int[] eventStarLunar = solarToLunar(eventStartCalendar.get(Calendar.YEAR), eventStartCalendar.get(Calendar.MONTH) + 1, eventStartCalendar.get(Calendar.DAY_OF_MONTH));
eventStartDay = eventStarLunar[2];
eventStartMonth = eventStarLunar[1];
int[] beginLunar = solarToLunar(beginCalendar.get(Calendar.YEAR), beginCalendar.get(Calendar.MONTH) + 1, beginCalendar.get(Calendar.DAY_OF_MONTH));
beginDay = beginLunar[2];
beginMonth = beginLunar[1];
int[] endLunar = solarToLunar(endCanlendar.get(Calendar.YEAR), endCanlendar.get(Calendar.MONTH) + 1, endCanlendar.get(Calendar.DAY_OF_MONTH));
endDay = endLunar[2];
endMonth = endLunar[1];
} else {
eventStartDay = eventStartCalendar.get(Calendar.DAY_OF_MONTH);
eventStartMonth = eventStartCalendar.get(Calendar.MONTH) + 1;
beginDay = beginCalendar.get(Calendar.DAY_OF_MONTH);
beginMonth = beginCalendar.get(Calendar.MONTH) + 1;
endDay = endCanlendar.get(Calendar.DAY_OF_MONTH);
endMonth = endCanlendar.get(Calendar.MONTH) + 1;
}
int beginKey = beginMonth * 31 + beginDay;
int endKey = endMonth * 31 + endDay;
int eventStartKey = eventStartMonth * 31 + eventStartDay;
if (beginKey > endKey) { //跨年情况
if (eventStartKey >= beginKey || eventStartKey <= endKey) {
return true;
}
} else {
if (eventStartKey >= beginKey && eventStartKey <= endKey) {
return true;
}
}
return false;
}
每月循环情况处理,主要思路:
使用年、月、日根据(year * 366 + month * 31 + day)来计算权重,看是否落在设置的日期区间,这里需要考虑大小月、农历闰月等。如果是闰月,那么month需要额外加1。
在进行下一步之前,介绍一个方法addLunarMonth():农历month自增,涉及闰月、溢出等情况,要求使用最恰当日期,如下:
/**
* 农历月份加1
* <p>
* 先转换成农历,然后把农历月加1,然后再转换成公历,以及闰月、溢出情况处理
*
* @param startLunarDay 初始农历日,如大年三十
*/
public static Calendar addLunarMonth(Calendar calendar, int startLunarDay) {
int tmpYear = calendar.get(Calendar.YEAR);
int tmpMonth = calendar.get(Calendar.MONTH) + 1;
int tmpDay = calendar.get(Calendar.DAY_OF_MONTH);
int[] lunar = solarToLunar(tmpYear, tmpMonth, tmpDay);
int lunarYear = lunar[0];
int lunarMonth = lunar[1];
int lunarDay = lunar[2];
boolean isLeapMonth = lunar[3] == 1;
int leapMonth = leapMonth(lunarYear); //闰月
boolean lunarFlag = false; //add一个月后,是否是闰月的标记
if (leapMonth > 0) {
//lunarYear 该年有闰月,那么分几种情况:(1)leapMonth=12 闰12月 ; (2)当月不是闰月,但下月是闰月 (3)当月是闰月,但下月不是闰月
if (leapMonth == 12) {
if (lunarMonth == 12) {
if (isLeapMonth) {
//如果当月刚好是闰12月,那么下一月是下一年的1月
lunarYear += 1;
lunarMonth = 1;
} else {
//如果当月刚好是12月,那么下一月是闰12月
lunarFlag = true;
}
} else {
//如果当前月不是12月,那么直接把月份加1就行
lunarMonth += 1;
}
} else {
if (lunarMonth == leapMonth) {
if (isLeapMonth) {
//是闰月,那么直接月份加1就行。比如闰4月,下一月是5月
lunarMonth += 1;
} else {
//不是闰月,那么下一月是闰月。比如2020年4月,那么下一月是闰4月
lunarFlag = true;
}
} else {
if (lunarMonth == 12) {
lunarYear += 1;
lunarMonth = 1;
} else {
lunarMonth += 1;
}
}
}
} else {
//lunarYear 该年没有闰月,那么直接把lunarMonth加1,如果超出了,就把lunarYear加1,lunarMonth置为1月
if (lunarMonth == 12) {
lunarYear += 1;
lunarMonth = 1;
} else {
lunarMonth += 1;
}
}
int maxDay = getLunarMaxMonth(lunarYear, lunarMonth, lunarFlag);
if (lunarDay > maxDay) {
lunarDay = maxDay;
}
//使用初始日纠正,有那天的使用那天;使用最恰当的日期
if (startLunarDay <= maxDay && lunarDay != startLunarDay) {
lunarDay = startLunarDay;
}
int[] solar = lunarToSolar(lunarYear, lunarMonth, lunarDay, lunarFlag);
int solarYear = solar[0];
int solarMonth = solar[1];
int solarDay = solar[2];
calendar.set(Calendar.YEAR, solarYear);
calendar.set(Calendar.MONTH, solarMonth - 1);
calendar.set(Calendar.DAY_OF_MONTH, solarDay);
return calendar;
}
/**
* 获取农历year和month 对应的最大天数
*
* @param year 农历年
* @param month 农历月
* @return
*/
private static int getLunarMaxMonth(int year, int month, boolean isLeapMonth) {
int leapMonth = leapMonth(year);
if (leapMonth > 0 && month == leapMonth && isLeapMonth) {
return leapDays(year);
}
return monthDays(year, month);
}
再看每月循环事件的处理:
//每月循环事件,是否在这两个日期之间
private static boolean isBetweenMonth(Calendar beginCalendar, Calendar endCanlendar, Event event) {
//event开始日期
Calendar eventStartCalendar = Calendar.getInstance();
eventStartCalendar.setTime(event.startTime);
int eventStartDay, eventStartMonth, eventStartYear;
int beginDay, beginMonth, beginYear;
int endDay, endMonth, endYear;
boolean isBeginLeap = false, isEndLeap = false, isEventStartLeap = false;
int leapBeginMonth = 0, leapEndMonth = 0, leapEventStartMonth = 0;
if (event.isLunar == 1) {
int[] eventStarLunar = solarToLunar(eventStartCalendar.get(Calendar.YEAR), eventStartCalendar.get(Calendar.MONTH) + 1, eventStartCalendar.get(Calendar.DAY_OF_MONTH));
eventStartDay = eventStarLunar[2];
eventStartMonth = eventStarLunar[1];
eventStartYear = eventStarLunar[0];
isEventStartLeap = eventStarLunar[3] == 1;
int[] beginLunar = solarToLunar(beginCalendar.get(Calendar.YEAR), beginCalendar.get(Calendar.MONTH) + 1, beginCalendar.get(Calendar.DAY_OF_MONTH));
beginDay = beginLunar[2];
beginMonth = beginLunar[1];
beginYear = beginLunar[0];
isBeginLeap = beginLunar[3] == 1;
int[] endLunar = solarToLunar(endCanlendar.get(Calendar.YEAR), endCanlendar.get(Calendar.MONTH) + 1, endCanlendar.get(Calendar.DAY_OF_MONTH));
endDay = endLunar[2];
endMonth = endLunar[1];
endYear = endLunar[0];
isEndLeap = endLunar[3] == 1;
leapBeginMonth = leapMonth(beginYear);
leapEndMonth = leapMonth(endYear);
leapEventStartMonth = leapMonth(eventStartYear);
} else {
eventStartDay = eventStartCalendar.get(Calendar.DAY_OF_MONTH);
eventStartMonth = eventStartCalendar.get(Calendar.MONTH) + 1;
eventStartYear = eventStartCalendar.get(Calendar.YEAR);
beginDay = beginCalendar.get(Calendar.DAY_OF_MONTH);
beginMonth = beginCalendar.get(Calendar.MONTH) + 1;
beginYear = beginCalendar.get(Calendar.YEAR);
endDay = endCanlendar.get(Calendar.DAY_OF_MONTH);
endMonth = endCanlendar.get(Calendar.MONTH) + 1;
endYear = endCanlendar.get(Calendar.YEAR);
}
boolean isBeginAdd = false, isEndAdd = false, isEventStartAdd = false;
if (leapBeginMonth > 0) { //有闰月的情况
if (beginMonth > leapBeginMonth) {
isBeginAdd = true;
} else if (beginMonth == leapBeginMonth && isBeginLeap) {
isBeginAdd = true;
}
}
if (leapEndMonth > 0) {//有闰月的情况
if (endMonth > leapEndMonth) {
isEndAdd = true;
} else if (endMonth == leapEndMonth && isEndLeap) {
isEndAdd = true;
}
}
if (leapEventStartMonth > 0) {
if (eventStartMonth > leapEventStartMonth) {
isEventStartAdd = true;
} else if (eventStartMonth == leapEventStartMonth && isEventStartLeap) {
isEventStartAdd = true;
}
}
int beginKey = beginYear * 366 + (isBeginAdd ? beginMonth + 1 : beginMonth) * 31 + beginDay;
int endKey = endYear * 366 + (isEndAdd ? endMonth + 1 : endMonth) * 31 + endDay;
int eventStartKey = eventStartYear * 366 + (isEventStartAdd ? eventStartMonth + 1 : eventStartMonth) * 31 + eventStartDay;
if (eventStartKey >= beginKey && eventStartKey <= endKey) {
return true;
}
if (eventStartYear < beginYear - 1) { //如果初始时间设置非常早,将年数设置近一些
eventStartYear = beginYear - 1;
eventStartCalendar.set(Calendar.YEAR, eventStartYear);
}
int tmpStartDay = eventStartDay;
while (eventStartKey < beginKey) {
boolean isTmpLeap = false, isTmpAdd = false;
int tmpLeapMonth = 0;
if (event.isLunar == 1) {
addLunarMonth(eventStartCalendar, tmpStartDay);
int[] eventStarLunar = solarToLunar(eventStartCalendar.get(Calendar.YEAR), eventStartCalendar.get(Calendar.MONTH) + 1, eventStartCalendar.get(Calendar.DAY_OF_MONTH));
eventStartDay = eventStarLunar[2];
eventStartMonth = eventStarLunar[1];
eventStartYear = eventStarLunar[0];
isTmpLeap = eventStarLunar[3] == 1;
tmpLeapMonth = leapMonth(eventStartYear);
if (tmpLeapMonth > 0) {
if (eventStartMonth > tmpLeapMonth) {
isTmpAdd = true;
} else if (eventStartMonth == tmpLeapMonth && isTmpLeap) {
isTmpAdd = true;
}
}
} else {
eventStartCalendar.add(Calendar.MONTH, 1);
int maxDay = eventStartCalendar.getActualMaximum(Calendar.DAY_OF_MONTH);
eventStartDay = eventStartCalendar.get(Calendar.DAY_OF_MONTH);
if (tmpStartDay <= maxDay && tmpStartDay != eventStartDay) { //大小月处理
eventStartDay = tmpStartDay;
eventStartCalendar.set(Calendar.DAY_OF_MONTH, eventStartDay);
}
eventStartMonth = eventStartCalendar.get(Calendar.MONTH) + 1;
eventStartYear = eventStartCalendar.get(Calendar.YEAR);
}
eventStartKey = eventStartYear * 366 + (isTmpAdd ? eventStartMonth + 1 : eventStartMonth) * 31 + eventStartDay;
}
if (eventStartKey < endKey) {
return true;
} else {
return false;
}
}
第二部分,修正显示时间。根据类型,分别处理:
private void correctShowTimeBetween(Event event, Calendar beginCalendar, Calendar endCalendar) {
if (event.repeatType == Event.REPEAT_EVERY_WEEK) {
correctWeekShowTime(beginCalendar, endCalendar, event);
}
if (event.repeatType == Event.REPEAT_EVERY_MONTH) {
correctMonthShowTime(beginCalendar, endCalendar, event);
}
if (event.repeatType == Event.REPEAT_EVERY_YEAR) {
correctYearShowTime(beginCalendar, endCalendar, event);
}
}
纠正每周循环显示时间:
private void correctWeekShowTime(Calendar beginCalendar, Calendar endCalendar, Event event) {
Calendar eventStartCalendar = Calendar.getInstance();
eventStartCalendar.setTime(event.startTime);
int eventStartYear = eventStartCalendar.get(Calendar.YEAR);
int eventStartMonth = eventStartCalendar.get(Calendar.MONTH) + 1;
int eventStartDay = eventStartCalendar.get(Calendar.DAY_OF_MONTH);
int beginYear = beginCalendar.get(Calendar.YEAR);
int beginMonth = beginCalendar.get(Calendar.MONTH) + 1;
int beginDay = beginCalendar.get(Calendar.DAY_OF_MONTH);
if (eventStartYear < beginYear - 1) { //如果初始时间设置非常早,将年数设置近一些
eventStartYear = beginYear - 1;
eventStartCalendar.set(Calendar.YEAR, eventStartYear);
}
if (eventStartMonth < beginMonth -1 ){
eventStartMonth = beginMonth -1;
eventStartCalendar.set(Calendar.MONTH, eventStartMonth);
}
int eventStartKey = eventStartYear * 366 + eventStartMonth * 31 + eventStartDay;
int beginKey = beginYear * 366 + beginMonth * 31 + beginDay;
if (eventStartKey < beginKey) {
while (eventStartKey < beginKey) {
eventStartCalendar.add(Calendar.DAY_OF_MONTH, 7);
eventStartYear = eventStartCalendar.get(Calendar.YEAR);
eventStartMonth = eventStartCalendar.get(Calendar.MONTH) + 1;
eventStartDay = eventStartCalendar.get(Calendar.DAY_OF_MONTH);
eventStartKey = eventStartYear * 366 + eventStartMonth * 31 + eventStartDay;
}
event.startTime = eventStartCalendar.getTime();
}
}
纠正每年显示时间:
private void correctYearShowTime(Calendar beginCalendar, Calendar endCalendar, Event event) {
Calendar eventStartCalendar = Calendar.getInstance();
eventStartCalendar.setTime(event.startTime);
int eventStartYear = eventStartCalendar.get(Calendar.YEAR);
int eventStartMonth = eventStartCalendar.get(Calendar.MONTH) + 1;
int eventStartDay = eventStartCalendar.get(Calendar.DAY_OF_MONTH);
int eventStartKey = eventStartYear * 366 + eventStartMonth * 31 + eventStartDay;
int beginYear = beginCalendar.get(Calendar.YEAR);
int beginMonth = beginCalendar.get(Calendar.MONTH) + 1;
int beginDay = beginCalendar.get(Calendar.DAY_OF_MONTH);
int beginKey = beginYear * 366 + beginMonth * 31 + beginDay;
boolean isLunar = event.isLunar == 1;
if (isLunar) {
int[] eventStartLunar = solarToLunar(eventStartCalendar.get(Calendar.YEAR), eventStartCalendar.get(Calendar.MONTH) + 1, eventStartCalendar.get(Calendar.DAY_OF_MONTH));
eventStartDay = eventStartLunar[2];
eventStartMonth = eventStartLunar[1];
eventStartYear = eventStartLunar[0];
eventStartKey = eventStartYear * 366 + eventStartMonth * 31 + eventStartDay;
}
if (isLunar){
int[] eventBeginLunar = solarToLunar(beginCalendar.get(Calendar.YEAR), beginCalendar.get(Calendar.MONTH) + 1, beginCalendar.get(Calendar.DAY_OF_MONTH));
beginDay = eventBeginLunar[2];
beginMonth = eventBeginLunar[1];
beginYear = eventBeginLunar[0];
beginKey = beginYear * 366 + beginMonth * 31 + beginDay;
}
if (eventStartYear < beginYear - 1) { //如果初始时间设置非常早,将年数设置近一些
eventStartYear = beginYear - 1;
eventStartCalendar.set(Calendar.YEAR, eventStartYear);
}
if (eventStartKey < beginKey) {
while (eventStartKey < beginKey) {
if (isLunar){
eventStartCalendar = addLunarYear(eventStartCalendar,eventStartDay);
int[] eventStartLunar = solarToLunar(eventStartCalendar.get(Calendar.YEAR), eventStartCalendar.get(Calendar.MONTH) + 1, eventStartCalendar.get(Calendar.DAY_OF_MONTH));
eventStartDay = eventStartLunar[2];
eventStartMonth = eventStartLunar[1];
eventStartYear = eventStartLunar[0];
eventStartKey = eventStartYear * 366 + eventStartMonth * 31 + eventStartDay;
} else {
eventStartCalendar.add(Calendar.YEAR, 1);
eventStartYear = eventStartCalendar.get(Calendar.YEAR);
eventStartMonth = eventStartCalendar.get(Calendar.MONTH) + 1;
eventStartDay = eventStartCalendar.get(Calendar.DAY_OF_MONTH);
eventStartKey = eventStartYear * 366 + eventStartMonth * 31 + eventStartDay;
}
}
event.startTime = eventStartCalendar.getTime();
}
}
/**
* 农历年加1
* <p>
* 需要处理闰月等情况
*
* @param startLunarDay 初始农历日,如大年三十
* @return
*/
public static Calendar addLunarYear(Calendar calendar, int startLunarDay) {
int tmpYear = calendar.get(Calendar.YEAR);
int tmpMonth = calendar.get(Calendar.MONTH) + 1;
int tmpDay = calendar.get(Calendar.DAY_OF_MONTH);
int[] lunar = solarToLunar(tmpYear, tmpMonth, tmpDay);
int lunarYear = lunar[0];
int lunarMonth = lunar[1];
int lunarDay = lunar[2];
lunarYear += 1;
int maxDay = getLunarMaxMonth(lunarYear, lunarMonth, false);
if (lunarDay > maxDay) {
lunarDay = maxDay;
}
//使用初始日纠正,有那天的使用那天;使用最恰当的日期
if (startLunarDay <= maxDay && lunarDay != startLunarDay) {
lunarDay = startLunarDay;
}
int[] solar = lunarToSolar(lunarYear, lunarMonth, lunarDay, false);
int solarYear = solar[0];
int solarMonth = solar[1];
int solarDay = solar[2];
calendar.set(Calendar.YEAR, solarYear);
calendar.set(Calendar.MONTH, solarMonth - 1);
calendar.set(Calendar.DAY_OF_MONTH, solarDay);
return calendar;
}
通过上面每年、每周的纠正方法可以看出,其实是根据权值对比,设置正确的时间。这个权值对比,在第一部分的每月循环事件里已经见到过了。不过,那里并没有更改Event的startTime值。每月的纠正方法,只需更改一下它即可。这里不再重复了。
Over !