公历农历相互转换的算法及其VCL实现

发表于:2007-07-14来源:作者:点击数: 标签:
抱雪 你到过我的主页吗?在我的主页上有这样一个地方: 你注意到了吗,在显示时间的地方除了显示公历之外,还显示了农历:农历辛已(蛇)年二月廿三日未时,比一般的网站上只显示公历就酷多了(怎么像自吹自擂?别的网站千万别去告我违反了广告法)。这是怎
抱雪
你到过我的主页吗?在我的主页上有这样一个地方:
    你注意到了吗,在显示时间的地方除了显示公历之外,还显示了农历:农历辛已(蛇)年二月廿三日未时,比一般的网站上只显示公历就酷多了(怎么像自吹自擂?别的网站千万别去告我违反了广告法)。这是怎么做的呢?其实很简单,只要一个小小的PHP或者JavaScript程序就可以了。

       你不要着急地问我要PHP或JS的程序,最关键的是要了解算法,如果你明白了转换的道理,就可以达到圣人所说的:举一而三反焉,到时不管是用PHP、DELPHI、JS还是JSP、VB,你都可以很快地写公历农历相互转换的程序来出来。我记得有高人曾经说过,编程语言只是工具,数据结构才是最重要的,此言诚不虚也。

       闲话少说,下面我就来介绍一下具体的算法。

首先是要保存公农历之间的转换信息:以任意一年作为起点,把从这一年起若干年(若干是多少?就看你的需要了)的农历信息保存起来(在我的VCL中,是以1921年作为起点)。回想一下,我们平常是怎样来转换公历农历的呢?是查万年历,万年历有每一天的公历农历,直接一查就可以了。那么我们可不可以也这样做呢?当然可以,但是,这样做就要收录每一天的信息,工作量就会很大,所以我们要简化这些信息。怎么简化呢?要保存一年的信息其实只要两个信息就可以了:1、农历每个月的大小;2、今年是否有闰月,闰几月以及闰月的大小。用一个整数来保存这些信息就足够了。具体的方法是:用一位来表示一个月的大小,大月记为1,小月记为0,这样就用掉12位(无闰月)或13位(有闰月),再用高4位来表示闰月的月份,没有闰月记为0。比如说,2000年的信息数据是是0xC96,化成二进制就是110010010110B,表示的含义是指1、2、5、8、10、11月大,其余月小;2001年的农历信息数据是0x41A95,其中4表示今年闰四月,月份大小信息就是0x1A95(因为闰月,所以有13位),具体的就是1、2、4、5、8、10、12月大,其余月份小(0x1A95=1101010010101B),要注意在四月的后面那一个0表示的是闰四月小,接着的那个1表示5月大。这样就可以用一个数组来保存这些信息。在我的VCL程序中是用ChineseCalendarData[]这个数组来保存这些信息。

为了方便对算法的理解,首先来看看我的VCL组件hsDivineCalendar的头文件

//---------------------------------------------------------------------------

  

#ifndef hsDivineCalendarH

#define hsDivineCalendarH

#define ALLYEARS 100        //定义转换的年数:100年

//---------------------------------------------------------------------------

#include <SysUtils.hpp>

#include <Controls.hpp>

#include <Classes.hpp>

#include <Forms.hpp>

//---------------------------------------------------------------------------

class PACKAGE ThsDivineCalendar : public TComponent

{

private:

   int ChineseCalendarData[ALLYEARS];       //农历数据

   AnsiString str2,num;         //要用的字符串

   void __fastcall c2e();              //农历到公历

   void __fastcall e2c();              //公历到农历

   TDateTime TheDate;                  //日期

   int FYear,FMonth,FDay,FTime;          //公历年月日时

   int FcYear,FcMonth,FcDay,FcTime;      //农历年月日时

   AnsiString __fastcall GetDateString();     //获取日期字符串

   AnsiString __fastcall GetcDateString();    //获取农历日期字符串

   int __fastcall GetBit(int m,int n);    //获取1bit

   void __fastcall GetYMD();              //获取年月日

   void __fastcall SetDate(TDateTime);             //用一个TDateTime类型的变量转换

   //-----------分别修改公历的年月日时-------------------------------------

   void __fastcall SetYear(int AYear){SetBy(AYear,FMonth,FDay,FTime);};

   void __fastcall SetMonth(int AMonth){SetBy(FYear,AMonth,FDay,FTime);};

   void __fastcall SetDay(int ADay){SetBy(FYear,FMonth,ADay,FTime);};  

   void __fastcall SetTime(int ATime){SetBy(FYear,FMonth,FDay,ATime);};

   //-----------分别修改农历的年月日时---------------------------------------

   void __fastcall SetcYear(int AcYear){SetByc(AcYear,FcMonth,FcDay,FcTime);};

   void __fastcall SetcMonth(int AcMonth){SetByc(FcYear,AcMonth,FcDay,FcTime);};

   void __fastcall SetcDay(int AcDay){SetByc(FcYear,FcMonth,AcDay,FcTime);};

   void __fastcall SetcTime(int AcTime){SetByc(FcYear,FcMonth,FcDay,AcTime);};

   TDateTime __fastcall GetLastJie(); //取得上一个节

   TDateTime __fastcall GetNextJie();       //取得下一个节

   TDateTime __fastcall GetLastQi();  //取得上一个中气

   TDateTime __fastcall GetNextQi(); //取得下一个中气

   int __fastcall GetDayOfWeek();      //取得一周的第几天

   AnsiString __fastcall GetWeekString();   //返回星期几的字符串

protected:

public:

   void __fastcall SetBy(int,int,int,int);    //用公历设置

   void __fastcall SetByc(int,int,int,int);   //用农历设置

   __fastcall ThsDivineCalendar(TComponent* Owner);

   //属性:年月日时

   __property int Year={read=FYear,write=SetYear};

   __property int Month={read=FMonth,write=SetMonth};

   __property int Day={read=FDay,write=SetDay};

   __property int Time={read=FTime,write=SetTime};

   //属性:农历年月日时

   __property int cYear={read=FcYear,write=SetcYear};

   __property int cMonth={read=FcMonth,write=SetcMonth};

   __property int cDay={read=FcDay,write=SetcDay};

   __property int cTime={read=FcTime,write=SetcTime};

   //公历农历日期字符串

   __property AnsiString DateString={read=GetDateString};

   __property AnsiString cDateString={read=GetcDateString};

   //其他属性

   __property TDateTime DateTime={read=TheDate,write=SetDate};

   __property TDateTime LastJie  = { read=GetLastJie };

   __property TDateTime NextJie  = { read=GetNextJie };

   __property TDateTime LastQi  = { read=GetLastQi };

   __property TDateTime NextQi  = { read=GetNextQi };

   __property int DayOfWeek  = { read=GetDayOfWeek };

   __property AnsiString WeekString  = { read=GetWeekString };

__published:

};

//---------------------------------------------------------------------------

#endif

下面介绍转换的具体算法。

一、公历转换成农历

1、计算出所求时间到起始年正月初一的天数。

2、从起始年份开始,减去每一月的天数,一直到剩余天数没有下一个月多为止

此时,ChineseCalendarData[]的下标到了多少,就是减去了多少年,用起始年份加上这个下标就可以得到农历年份;然后看减去了几个月,如果本年不闰月或者闰月还在后面,就可以直接得到农历月份,如果在闰月月份数后面一个月,则这个月就是闰月,如果在闰月的后面,则要减去1才能得到月份数;剩余的天数就是农历日;农历时用(公历时+1)/2就可以简单地得到了。具体的代码如下:

//---------------------------------------------------------------------------

void __fastcall ThsDivineCalendar::e2c()

{

        int total,m,n,k;

        bool isEnd=false;    //用以判断是否不够减了

        total=(int)TheDate-7709;     //到1921-2-8(正月初一)的天数

        for(m=0;;m++)

        {

/*判断本年是否闰月,用以确定月份信息的起点

  有闰月有13位(0~12),无12位(0~11)*/

k=(ChineseCalendarData[m]<0xfff)?11:12; for(n=k;n>=0;n--)

                {

                                          //如果不够减

                        if(total<=29+GetBit(ChineseCalendarData[m],n))

{

isEnd=true; //设置标志

break; //退出内层循环

}

/*够减,减去一个月的天数

先减去29天如果月大,则对应的信息位为1,

又减去一天*/

total=total-29-GetBit(ChineseCalendarData[m],n);

}

if(isEnd)break; //如果不够减,退出外层循环

}

FcYear=1921 + m; //农历年=起始年份+下标

FcMonth=k-n+1; //农历月=本年的月份数(k+1)减去已经减去的月份数(n)

FcDay=total; //农历日=剩余天数

unsigned short int t1,t2,t3,t4;

TheDate.DecodeTime(&t1,&t2,&t3,&t4);

FcTime=(t1+1)>>1;             //农历时

        if(k==12)              //如果本年有闰月

        {

                if(FcMonth==ChineseCalendarData[m]/0x10000+1)//就是闰月

                        FcMonth=1-FcMonth;

                if(FcMonth>ChineseCalendarData[m]/0x10000+1)//闰月后面

                        FcMonth--;

        }

}

//----------------------------------------------------------------------------

二、农历到公历的转换

这个算法比较简单,只要计算所求时候到起始年正月初一的总天数就可以了,要计算总天数,只要统计出本月以前的大月小月书就可以了,然后把这个值赋予TdateTime类型的TheDate就可以用TdateTime的成员函数DecodeDate得到公历的年月日了。具体代码如下:

//----------------------------------------------------------------------------

void __fastcall ThsDivineCalendar::c2e()

{

    int i,k,m,p,y[]={0,0};

       //y[0]:小月、y[1]:大月

    //本年以前的大月小月数

    for(i=0;i<FcYear-1921;i++)

{

k=(ChineseCalendarData[i]<0xfff)?11:12;

for(m=0;m<=k;m++)

y[GetBit(ChineseCalendarData[i],m)]++;

}

//统计本年本月以前的大月小月数

//本年不是闰年

if(ChineseCalendarData[i]<0xfff)

for(m=13-FcMonth;m<=11;m++)

y[GetBit(ChineseCalendarData[i],m)]++;

else // 是闰年

{

k=ChineseCalendarData[i]/0x10000;

//根据在闰月前后决定统计的起始位置

p=(FcMonth>k)?13-FcMonth:14-FcMonth;

        if(k+FcMonth==0)p=13+FcMonth;     //本月就是闰月

        for(m=p;m<=12;m++)

y[GetBit(ChineseCalendarData[i],m)]++;

}

//7709就是1920年腊月三十

TheDate=7709+y[0]*29+y[1]*30+FcDay+FcTime*2.0/24;

}

//----------------------------------------------------------------------------

void __fastcall ThsDivineCalendar::GetYMD()

{

unsigned short y,m,d,t;

TheDate.DecodeDate(&y,&m,&d);

FYear=y;

FMonth=m;

FDay=d;

TheDate.DecodeTime(&t,&y,&m,&d);

FTime=t;

}

//----------------------------------------------------------------------------

以上就是公历农历相互转换的算法和VCL代码,只要理解了这些算法,你就不难写出其他的程序,我就写了JavaScript和PHP的代码,其实PHP、JS的代码简单得多,只需要有公历到农历的转换就可以了。如果你想要这些代码和完整的VCL源代码,你就说嘛,你不说我怎么知道你想要呢?虽然你很有诚意地看着我……,哈哈,我又中《大话西游》的毒了:=)。其实,这些代码你可以在《电脑爱好者》网站或者我的主页(http://bcbtop.126.com)的主页下载
http://hugsnow.myetang.com/source/hugsnow1.zip

原文转自:http://www.ltesting.net