公历农历相互转换的算法及其VCL实现
【打印文章】
抱雪
你到过我的主页吗?在我的主页上有这样一个地方:
你注意到了吗,在显示时间的地方除了显示公历之外,还显示了农历:农历辛已(蛇)年二月廿三日未时,比一般的网站上只显示公历就酷多了(怎么像自吹自擂?别的网站千万别去告我违反了广告法)。这是怎么做的呢?其实很简单,只要一个小小的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
你到过我的主页吗?在我的主页上有这样一个地方:
你注意到了吗,在显示时间的地方除了显示公历之外,还显示了农历:农历辛已(蛇)年二月廿三日未时,比一般的网站上只显示公历就酷多了(怎么像自吹自擂?别的网站千万别去告我违反了广告法)。这是怎么做的呢?其实很简单,只要一个小小的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
本栏文章均来自于互联网,版权归原作者和各发布网站所有,本站收集这些文章仅供学习参考之用。任何人都不能将这些文章用于商业或者其他目的。( Pfan.cn )
【编程爱好者论坛】