天天看點

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);

}