一、簡介
過年是中國 ( 以及日本、南韓等國 ) 人民的第一大節日。你怎麼知道哪天過年?查月曆或者聽别人說?程式員當然有程式員的辦法,就是寫程式啦。
雖然公曆 ( 俗稱的 “ 陽曆 ”) 已經成了全世界的通用标準,而且也具有多方面的優越性。但在東亞地區,還是離不開 “ 農曆”,春節、元宵、端午、中秋、重陽這些節日是農曆的,大部份人的老爸老媽的生日也是農曆的。
早在 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 為相關功能命名,這個名字也還過得去吧。 我原先設想自定義一個類,使得能寫出這樣的代碼:
string s = DateTime.Now.ToString( new MyFormatProvider()); 雖然不能為 DataTime 寫自定義的格式器,但還有另外一個途徑,就是為 String 類的 Format 方法寫自定義格式化器,我測試了一下,效果還不錯,調用方式如下:
string s = String.Format( new ChineseCalendarFormatter(), " {0:D} " ,DateTime.Now); 可以得到“二〇〇六年正月初九”
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);
}