一、简介
过年是中国
(
以及日本、韩国等国
)
人民的第一大节日。你怎么知道哪天过年?查日历或者听别人说?程序员当然有程序员的办法,就是写程序啦。
虽然公历
(
俗称的
“
阳历
”)
已经成了全世界的通用标准,而且也具有多方面的优越性。但在东亚地区,还是离不开
“
农历”,春节、元宵、端午、中秋、重阳这些节日是农历的,大部份人的老爸老妈的生日也是农历的。
早在
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);
}