天天看点

NET framework2.0中的农历类(全)

  一、简介

过年是中国 ( 以及日本、韩国等国 ) 人民的第一大节日。你怎么知道哪天过年?查日历或者听别人说?程序员当然有程序员的办法,就是写程序啦。

虽然公历 ( 俗称的 “ 阳历 ”) 已经成了全世界的通用标准,而且也具有多方面的优越性。但在东亚地区,还是离不开 “ 农历”,春节、元宵、端午、中秋、重阳这些节日是农历的,大部份人的老爸老妈的生日也是农历的。

早在 1.0 框架出来的时候,我就认为微软公司不应该 “ 厚彼薄此 ” ,在 .net 框架中提供了希伯来历等,却没有提供更广泛使用的 “ 农历 ” 。

而在 .net 2.0 中,微软公司终于做出了这个小小的改进。

.net 2.0 在 System.Globalization 命名空间中新增加了 EastAsianLunisolarCalendar 类及以继承它的 ChineseLunisolarCalendar, JapaneseLunisolarCalendar, KoreanLunisolarCalendar, TaiwanLunisolarCalendar 等几个类。 LunisolarCalendar 顾名思义应为 “ 阴阳历 ” ,我的理解是因为我们所用的农历虽然按照月亮公转来编月份,但用 “ 闰月 ” 的方式来调整年份与地球公转的误差,严格意义上来说是结合了月亮公转和地球公转的成份,因此属于 “ 阴阳历 ” 。但我这里还是按照习惯称之为 “ 农历 ” 。

二、新的农历类还是没有公民待遇

为了测试新的日历类,我兴冲冲地写了几句代码:(省略了调用这个方法的其它代码)

运行报错,错误信息是: "Not a valid calendar for the given culture "

为了说明问题,继续测试

可以正常运行,结果是 95年x月x日(民国纪年),注释掉中间那条语句,结果是2006年x月x日(也就是使用公历),将中间那条语句修改成:ci.DateTimeFormat.Calendar = new TaiwanLunisolarCalendar(),照样出错。查相关资料,原来DateTimeFormat的Calendar属性只能为CultureInfo的OptionalCalendars属性所指定范围。 于是再写一段代码测试 OptionalCalendars的内容, 对于 zh-CN语言,惟一可用于日期格式的calendar是 本地化的 GregorianCalendar(也就是公历)。对于zh-TW,可用于日期格式的calendar是美国英语和本地化的GregorianCalendar以及TaiwanCalendar(即公历的年份减1911),都没有包括农历。 也就是说 .net2.0虽然提供了农历类,但对它的支持并不及同样有闰月的希伯来历。我查资料的时候找到了博客堂的一篇文章http://blog.joycode.com/percyboy/archive/2004/09/17.aspx ,作者在一年半以前发现了农历类不支持日期格式化的问题,并认为这是一个bug。当然还算不上bug,只不过微软没有重视而已(责任在微软吗?我想应该不是,在商业社会我们有多重视微软就会有多重视。和以色列比起来,我们对传统文化的重视程度差得太远)。

三、农历类的使用 既然 .net 框架不支持直接将日期转换成农历格式的字符串,那么要将显示农历格式的日期,就只要自已写代码了。不过由于已经有了 ChineseLunisolarCalendar 类实现了公历转换为农历日期的功能,所以要写这样的代码也比较简单。需要用到 ChineseLunisolarCalendar 以下几个主要方法: int GetYear (DateTime time) 获取指定公历日期的农历年份,使用的还是公历纪元。在每年的元旦之后春节之前农历的纪年会比公历小 1, 其它时候等于公历纪年。虽然农历使用传说中的耶稣生日纪元似乎不太妥当,不过我们确实已经几十年没有实行一个更好的纪年办法,也只有将就了。 int GetMonth (DateTime time) 获取指定公历日期的农历月份。这里要注意了,由于农历有接近三分之一的年份存在闰月,则在这些年份里会有十三个,而具体哪一个月是闰月也说不准,这里不同于希伯来历。以今年为例,今年闰七月,则此方法在参数为闰七月的日期是返回值为 8 ,参数为农历十二月的日期时返回值为 13 bool IsLeapMonth ( int year,   int month) 获取指定农历年份和月份是否为闰月,这个函数和上个函数配合使用就可以算出农历的月份了。 int GetDayOfMonth (DateTime time) 获取指定公历日期的农历天数,这个值根据大月或者小月取值是 1 到 30 或者 1 到 29, MSDN 上说的 1 到 31 显然是错的 , 没有哪个农历月份会有 31 天。 int GetSexagenaryYear (DateTime time) 获取指定公历日期的农历年份的干支纪年,从 1 到 60 ,分别是甲子、乙丑、丙寅、 …. 癸亥 , 比如戊戌变法、辛亥革命就是按这个来命名的。当然算八字也少不了这个。 int GetCelestialStem (int sexagenaryYear) 获取一个天支的天干 , 从 1 到 10, 表示甲、乙、丙 …. ,说白了就是对 10 取模。 int GetTerrestrialBranch (int sexagenaryYear) ) 获取一个干支的地支, , 从 1 到 12, 表示子、丑、寅、 … 今年是狗年,那么今年年份的地支就是“戌”。 有了这几个方法,显示某天的农历月份日期、农历节日等都是小菜一碟,算命先生排八字用这几个方法,又快又准确,写出的代码也很短。

  四、几种东亚农历类的区别 经过我的测试,ChineseLunisolarCalendar, JapaneseLunisolarCalendar, KoreanLunisolaCalendarr, TaiwanLunisolarCalendar这四种日历,无论哪一种,以2006年2月6日为参数,调用它们的GetMonth方法得到的结果都是1,GetDayOfMonth得到的结果都是8。想想也是,我们过的端午节和韩国的不太可能不是一天。 但是调用GetYear方法得到结果就有区别了ChineseLunisolarCalendar和KoreanLunisolarCalendar都返回2006,也就是公历纪年,TaiwanLunisolarCalendar的返回值是95,依然是民国纪年,JapaneseLunisolarCalendar的返回值是18, 平成纪年。 另外的一个区别是这四种日历的MinSupportedDateTime 和MaxSupportedDateTime各不一样,以下是对照表:

日历类 MinSupportedDateTime MaxSupportedDateTime
ChineseLunisolarCalendar 公元1901年1月初1 公元2100年12月29
TaiwanLunisolarCalendar 民国1年1月初1 民国139年12月29
JapaneseLunisolarCalendar 昭和35年1月初1 平成61年12月29
KoreanLunisolarCalendar 公元918年1月初1 公元2050年12月29

韩国农历类支持的最小日期为918 年(也即高丽王朝建立的年份),以此而论,中国农历类支持的最小日期不说从商周算起,从汉唐算总该没问题吧?微软公司啊,又在“厚彼薄此”,唉。 其次,日本还以天皇纪年,如果哪天xxxx, 岂不是使用JapaneseLunisolarCalendar写出的程序都有问题啦? 五、写自已的日期格式化器 昨天看了一篇文章,说目前大家用的“农历”这个术语是文革时期才有的,目的是反封建。这里为了省事,还是继续使用这个术语。而英文名称 ChineseLunisolarCalendar 太长,我自己的代码中就用 ChineseCalendar 为相关功能命名,这个名字也还过得去吧。 我原先设想自定义一个类,使得能写出这样的代码:

NET framework2.0中的农历类(全)

string  s =  DateTime.Now.ToString( new  MyFormatProvider());   虽然不能为 DataTime 写自定义的格式器,但还有另外一个途径,就是为 String 类的 Format 方法写自定义格式化器,我测试了一下,效果还不错,调用方式如下:

NET framework2.0中的农历类(全)

string  s =  String.Format( new  ChineseCalendarFormatter(),  " {0:D} " ,DateTime.Now); 可以得到“二〇〇六年正月初九”

NET framework2.0中的农历类(全)

string  s =  String.Format( new  ChineseCalendarFormatter(),  " {0:d} " ,DateTime.Now); 可以得到“丙戌年正月初九” 虽然没有前面所设想的方便,但也还能接受,全部代码帖出如下:

第一个类,主要是封装了农历的一些常用字符和对日历处理的最基本功能

就能得出我想要的农历日期字符串,经过测试却失败了,依据我的分析,微软公司在.net框架中把日期时间型的格式写死了,只能依据相关的地区采用固定的几种显示格式,没法再自行定义。而前文已经说过,而所有的相关格式微软公司都放到一个名为culture.nlp的文件中(这个文件在以前的.net框架是一个独立的文件,在.net 2.0被作为一个资源编译到mscorlib.dll中。) (我的这个不能为DateTime写自已的格式化器的观点没有资料佐证,如有不当之处,请大家指正) using  System;

using  System.Collections.Generic;

using  System.Text;

using  System.Globalization;

public  static  class  ChineseCalendarHelper

{

     public  static  string  GetYear(DateTime time)

    {

        StringBuilder sb  =  new  StringBuilder();

         int  year  =  calendar.GetYear(time);

         int  d;

         do

        {

            d  =  year  %  10 ;

            sb.Insert( 0 , ChineseNumber[d]);

            year  =  year  /  10 ;

        }  while  (year  >  0 );

         return  sb.ToString();

    }

     public  static  string  GetMonth(DateTime time)

    {

         int  month  =  calendar.GetMonth(time);

         int  year  =  calendar.GetYear(time);

         int  leap  =  0 ;

         // 正月不可能闰月

         for  ( int  i  =  3 ; i  <=  month; i ++ )

        {

             if  (calendar.IsLeapMonth(year, i))

            {

                leap  =  i;

                 break ;   // 一年中最多有一个闰月

            }

        }

         if  (leap  >  0 ) month -- ;

         return  (leap  ==  month  +  1  ?  " 闰 "  :  "" )  +  ChineseMonthName[month  -  1 ];

    }

     public  static  string  GetDay(DateTime time)

    {

         return  ChineseDayName[calendar.GetDayOfMonth(time)  -  1 ];

    }

     public  static  string  GetStemBranch(DateTime time)

    {

         int  sexagenaryYear  =  calendar.GetSexagenaryYear(time);

         string  stemBranch  =  CelestialStem.Substring(sexagenaryYear  %  10  -  1 ,  1 )  +  TerrestrialBranch.Substring(sexagenaryYear  %  12  -  1 ,  1 );

         return  stemBranch;

    }

     private  static  ChineseLunisolarCalendar calendar  =  new  ChineseLunisolarCalendar();

     private  static  string  ChineseNumber  =  " 〇一二三四五六七八九 " ;

     public  const  string  CelestialStem  =  " 甲乙丙丁戊己庚辛壬癸 " ;

     public  const  string  TerrestrialBranch  =  " 子丑寅卯辰巳午未申酉戌亥 " ;

     public  static  readonly  string [] ChineseDayName  =  new  string [] {

            " 初一 " , " 初二 " , " 初三 " , " 初四 " , " 初五 " , " 初六 " , " 初七 " , " 初八 " , " 初九 " , " 初十 " ,

             " 十一 " , " 十二 " , " 十三 " , " 十四 " , " 十五 " , " 十六 " , " 十七 " , " 十八 " , " 十九 " , " 二十 " ,

             " 廿一 " , " 廿二 " , " 廿三 " , " 廿四 " , " 廿五 " , " 廿六 " , " 廿七 " , " 廿八 " , " 廿九 " , " 三十 " };

     public  static  readonly  string [] ChineseMonthName  =  new  string [] {  " 正 " ,  " 二 " ,  " 三 " ,  " 四 " ,  " 五 " ,  " 六 " ,  " 七 " ,  " 八 " ,  " 九 " ,  " 十 " ,  " 十一 " ,  " 十二 "  };

}

第二个类为自定义格式化器:

using  System;

using  System.Collections.Generic;

using  System.Text;

using  System.Globalization;

using  System.Threading;

public  class  ChineseCalendarFormatter : IFormatProvider, ICustomFormatter

{

     // 实现IFormatProvider

     public  object  GetFormat(Type formatType)

    {

         if  (formatType  ==  typeof (ICustomFormatter))

             return  this ;

         else

             return  Thread.CurrentThread.CurrentCulture.GetFormat(formatType);

    }

     // 实现ICustomFormatter

     public  string  Format( string  format,  object  arg, IFormatProvider formatProvider)

    {

         string  s;

        IFormattable formattable  =  arg  as  IFormattable;

         if  (formattable  ==  null )

            s  =  arg.ToString();

         else

            s  =  formattable.ToString(format, formatProvider);

         if  (arg.GetType()  ==  typeof (DateTime))

        {

            DateTime time  =  (DateTime)arg;

             switch  (format)

            {

                 case  " D " :  // 长日期格式

                    s  =  String.Format( " {0}年{1}月{2} " ,

                        ChineseCalendarHelper.GetYear(time),

                        ChineseCalendarHelper.GetMonth(time),

                        ChineseCalendarHelper.GetDay(time));

                     break ;

                 case  " d " :  // 短日期格式

                    s  =  String.Format( " {0}年{1}月{2} " , ChineseCalendarHelper.GetStemBranch(time),

                        ChineseCalendarHelper.GetMonth(time),

                        ChineseCalendarHelper.GetDay(time));

                     break ;

                 case  " M " :  // 月日格式

                    s  =  String.Format( " {0}月{1} " , ChineseCalendarHelper.GetMonth(time),

                        ChineseCalendarHelper.GetDay(time));

                     break ;

                 case  " Y " :  // 年月格式

                    s  =  String.Format( " {0}年{1}月 " , ChineseCalendarHelper.GetYear(time),

                        ChineseCalendarHelper.GetMonth(time));

                     break ;

                 default :

                    s  =  String.Format( " {0}年{1}月{2} " , ChineseCalendarHelper.GetYear(time),

                        ChineseCalendarHelper.GetMonth(time),

                        ChineseCalendarHelper.GetDay(time));

                     break ;

            }

        }

         return  s;

    }

} 这段代码中间处理格式那部份稍做改进,就可以支持更多的日期格式。

有了这两段代码为原型,要实现计算和显示一个日期的农历日期及其它功能,基本上就很容易了。   private  string  getDateString(DateTime dt)

{

    CultureInfo ci  =  new  CultureInfo( " zh-TW " );

    ci.DateTimeFormat.Calendar  =  new  TaiwanCalendar();

     return  dt.ToString( " D " ,ci);

}

private  string  getDateString(DateTime dt)

{

    CultureInfo ci  =  new  CultureInfo( " zh-CN " );

    ci.DateTimeFormat.Calendar  =  new  ChineseLunisolarCalendar();

     return  dt.ToString( " D " ,ci);

}