农历与阳历的转换

农历的日期需要推算,其与阳历之间没有什么必然的联系。现有的所有农历阳历的工具软件网页等等应该都是基于对预先存储的农历数据库进行查找,匹配出对应阳历日期的农历数据,从而实现农历的结果输出。现在还没有任何公式可以将农历与阳历直接进行公式转换。电脑手机上农历的换算一般都是遵循这样的步骤:譬如要查询2000年11月1日,那么首先查询2000年记录里面是否有闰年,如果有闰年那么闰月是闰几月?闰月为大月还是小月?再查询其他一到十二月每个月的大小(农历大月30天,小月29天。农历的大小月没有规律,闰几月也没有常规公式可计算的规律),然后查询2000年的正月初一是当年阳历年的哪一天,再根据每农历月的天数得出每农历月的初一对应的阳历日期,通过以上这些数据,就可以推算出2000年11月1日的农历日期。基本上农历查询都是遵循这样的步骤,缺一不可。

现有的存放农历数据我只找到两种,一种是现在多数的万年历网站所使用的一套数据(特征是4位16进制),如天气万年历网站,百度网站,在线日历等等。可能手机中的农历以及Windows10下的农历也是基于这个版本的数据(未验证),另一种是一篇C语言编程文章中提到的另外一套数据(特征是6位16进制),这套数据尚未发现有日历网站采用这套数据。

第一套数据的特征是:共21组数据,每组数据形式位0xad50这样格式,适用于1900年~2100年。

第二套数据的特征是:共600组数据,每组数据形式为:0x38 0xb6 0x4A这样,适用于1900年~2099年。

两套数据涵盖范围基本一样,里面分解出来的结果也基本相同,但是在后期的数据分解对比存在微量的数据不一致。第一套数据存在4处错误,第二套数据存在一处错误。具体位置如下:

第一套数据:

2097年原始数据为:A4D0,转换后为:1010010011010000,其中记录6月大,7月小;
                实际应该是7月大,6月小,修正后数据为:1010001011010000,修正转换后为 A2D0
                
2089年原始数据为:D260,转换后为:1101001001100000,其中记录7月大,8月小;
                实际应该是7月小,8月大,修正后数据为:1101000101100000,修正转换后为 D160
                
2057年原始数据为:6B20,转换后为:0110101100100000,其中记录8月大,9月小;
                实际应该是8月小,9月大,修正后数据为:0110101010100000,修正转换后为 6AA0
                
1906年原始数据为:5554,转换后为:0101010101010100,其中记录3月小,4月大; 
                实际应该是3月大,4月小,修正后数据为:0110010101010100,修正转换后为 6554

第二套数据:

1914年数据是原数据为:309556,转换后为:001100001001010101010110,1914年为闰四月,
                所以最后13位 1010101010110,其中10月大,9月小;
                实际应该是9月大,10月小,修正后数据为:001100001001001101010110,转后应该是309356

两套数据思路有区别,所以在后续数据处理与获取的方法上有较大的不同。



第一套数据的分解思路

第一套数据采用的4位16进制,转换为二进制后为最高16为,其中高位16→5位记录的1到12月升序的常规农历月的大小,大月30天为1,小月29天为0。低位4→1为闰月值,转换为十进制如果为0,表示当年为平年,无闰月。如果转换为10进制不为0,则对应的数值就表示当年该月有闰月,位于该月的后面。

如果光靠这两组数据是无法完成农历的匹配与查询的,因为这里面缺少了两个重要的数据,第一就是当年的农历春节正月初一对应的是阳历哪一天?另外就是当年如果有闰月,闰月是大月还是小月?后来分析得知,原有的代码里面设置了两个计算:

第一是根据1900年/1/31日为1900年春节这个参照点(或者1900年3月1日为1900年二月初一)。

第二是闰月的天数是利用下一月的后四位数值与0xf(对应二进制的1111)运算,因为闰年后面一年肯定不是闰年,所以后面一年的末四位可以是0000,但是也可以是1111,这两个都可以表示当年为平年(不在1~12取值范围内),如果上面闰年的闰月为大月,那么这后面一个月的后四位设成1111,与0xf运算结果为0xf,表示大月,如果上面闰年的闰月为小月,那么这后面一年的末四位设成0000,与0xf预算结果为0,表示小月,这样就相当于间接存储了闰月大小的信息。

那么为什么不直接在当年数据里面设置一位1或者0来存储闰月的大小月信息呢?这个应该是跟16进制有关系。现有的12位+4位,最高对应的16进制FFFF,如果再加一位的话,就变成了5位的6进制,这样数据的存储要求就加大了。除此之外也想不出还有什么原因。

举例分析:

1900年对应的数据:0x4bd8

首先将0x4bd8转换为二进制,数据为:0100101111011000,如结果不足16位,则前面补0补足16位。

其中前面12位010010111101,分别表示平月:1小2大3小4小5大6小7大8大9大10大11小12大。

其中后面4位1000,转换为十进制为8,表示1900年闰8月。

闰月天数数据获取:1901年对应的数据:0x4ae0

转换为二进制位:0100101011100000,最后4为0000,将0000 & 0xf ==0xf? 30:29,说明前一年的闰八月为小月,即29天。

而1900年1月31日为正月初一,根据每月天数结合闰月天数,可以计算出下一月的初一对应的阳历日期,进而可以计算出下一年春节即正月初一对应的阳历日期为1901/2/19日。依次类推,生成每一年每个月初一对应的阳历日期列表,从而获得1900到2100每个月的对应信息。

那么如何获取某一天如2000年11月1日的农历呢?

首先对2000/11/1进行数值转换,得到的数值为36831,将此数据通过match方式从列表中匹配,找到不大于该数值的最大数位置,再用index来获取结果为36826(如果正好列表中有数等于查询日期的数字,那么说明这天就是初一),将36831-36826+1=6,就是对应的农历日期就是初六,再结合同行异列的农历月份结果为十月,合并就是2000/11/1日的农历日期就是十月初六。

这个里面有两个代码记录:

代码1:生成1900到2099之间每一年全农历月按序排列(含闰月)的记录列表。内容就是农历月初一对应的阳历日期,因为这里面涉及到闰月位置的不固定以及闰月天数的插入。

代码思路:通过闰月指标是否存在判断,如果不存在,则直接根据12位的大小月记录写十二月的天数记录,如果存在,则首先写1到闰月值(非闰月,在闰月前),这个写入与前面写入一样,区分大小月就可以。然后写闰月指标,第三步写闰月后到12的大小月数据,结合这三步就可以完成12个月或者13个月的每条记录。

演示代码如下:

K = Range("A65536").End(xlUp).Row  '年份序列

'清空内容并背景重置
With Range("P3:AA" & K)
.Interior.ColorIndex = xlNone
.ClearContents
End With


For i = 3 To K
        dayall = 0 '每月总天数
        rydata = Cells(i, "n") '闰月标记,不等于0即为闰月
   
        If rydata = 0 Then '表示当年无闰月,则直接从1写到12
             
                For j = 1 To 12
                     If Mid(Cells(i, "o"), j, 1) = 1 Then '表示当月为大月
                        Cells(i, j + 15) = 30 & "|" & j
                        dayall = dayall + 30
                     Else
                        Cells(i, j + 15) = 29 & "|" & j
                        dayall = dayall + 29
                     End If
                Next
            
            Else '表示当年有闰月
        
                   '先写到闰月前的同月
                    For j = 1 To rydata 
                           If Mid(Cells(i, "o"), j, 1) = 1 Then
                                Cells(i, j + 15) = 30 & "|" & j
                                dayall = dayall + 30
                           Else
                                Cells(i, j + 15) = 29 & "|" & j
                                dayall = dayall + 29
                           End If
                    Next j
                
                    '继续写入闰月数据            
                       If Cells(i, "l") = 29 Then
                        Cells(i, rydata + 1 + 15) = 29 & "|" & rydata & "R" '带R标记作为闰月的标记
                        dayall = dayall + 29
                       ElseIf Cells(i, "l") = 30 Then
                        Cells(i, rydata + 1 + 15) = 30 & "|" & rydata & "R"
                        dayall = dayall + 30
                       End If
                       
                       '闰月做标记
                        Cells(i, rydata + 1 + 15).Interior.ColorIndex = 3
                    
                    '写剩下的月份数据
                   For j = rydata + 1 To 12 '先写到闰月前的同月
                           If Mid(Cells(i, "o"), j, 1) = 1 Then
                                Cells(i, j + 1 + 15) = 30 & "|" & j
                                dayall = dayall + 30
                           Else
                                Cells(i, j + 1 + 15) = 29 & "|" & j
                                dayall = dayall + 29
                           End If
                    Next j
            
        End If
        
        Cells(i, "ac") = dayall '获得当月总天数
Next i
’完成12位数据以及闰月数据的分析

代码2:就是在前面阳历查询的时候,通过对日期的查询同时获得农历日期,并将它显示在阳历日期的下面一行同列单元格内。

该代码在阳历对应的填写代码区域时同步写入;

代码如下:

         lunar_mon = "index(三、数据生成!h:h,MATCH(DATE(I2,J2," & cols(j + 1) & "2),三、数据生成!j:j))"
        '通过日期查询到不大于的农历初一对应的月份,从而获得当前日期的农历月
    
        lunar_day = "INDEX(三、数据生成!t:t,MATCH((DATE(I2,J2," & cols(j + 1) & "2)-INDEX(三、数据生成!j:j,MATCH(DATE(I2,J2," & cols(j + 1) & "2),三、数据生成!j:j)))+1,三、数据生成!s:s))"
        '首先通过获取当前日期与正月初一日期的差值(要加1)后等于该天数的农历日期,再用此数据与表列J匹配,获得农历日期描述K列
    
         Cells(3, j + 1) = Chr(61) & lunar_mon & Chr(38) & lunar_day
         '写入同列下一行的位置

这样以来就可以完成阳历阴历同时查询显示的功能。

备注1:下拉菜单切换时如果想自动执行代码获得结果,不是写在worksheet.selectionchange事件中,而是卸载worksheet的change事件中,并先后设置Application.EnableEvents = False与Application.EnableEvents = true。

备注2:1900/2/29日这个bug会影响到1900年的二月初一正确日期,该农历日期对应阳历日期需要手动+1后变成1900/03/01,从而获得正确结果。

备注3:excel中hex2bin存在12位的限制,可以将16进制拆分为两组,进行hex2bin(clip1,8) & hex2bin(clip2,8)这样的处理就可以获得超过12为二进制的结果数据。clip1可以不固定8位长度,但是clip2一定要固定8位长度。三组的16进制同样也是这样,分别拆分后按照固定8位长度来获得正确结果。