多年来,我尝试解决公历日期问题,但都失败了。我大约在15年前开发了这段代码,它仍然表现良好。由于我很久以前写过这些代码,所以是用本地C编写的,但很容易编译成C++程序。如果您喜欢,可以将其包装在一个日期类中。
我的代码基于将所有闰年规则组合成一个400年周期。根据公历闰年规则,每个400年周期恰好有146,097天。
我采用的一种优化方法是将1月和2月移到前一年的末尾。这使得闰日(如果存在)始终落在当年的最后一天。这使我能够建立一个表(dayOffset),其中提供了从3月1日开始的天数距离。因为闰日会落在年底,所以这个表对于闰年和非闰年都是准确的。
我将从头文件开始。
#if !defined( TIMECODE_H_ )
#define TIMECODE_H_ 1
#if defined(__cplusplus)
extern "C" {
#endif
int dateCode( int month, int dayOfMonth, int year );
void decodeDate( int *monthPtr, int *dayOfMonthPtr, int *yearPtr, int dateCode );
int dayOfWeek( int dateCode );
int cardinalCode( int nth, int weekday, int month, int year );
enum Weekdays { eMonday, eTuesday, eWednesday, eThursday, eFriday, eSaturday, eSunday };
#if defined(__cplusplus)
}
#endif
#endif
这个API包含四个方法:dateCode(),用于计算公历日期的日期代码;decodeDate(),用于从日期代码计算公历月份、日期和年份;dayOfWeek(),返回日期代码对应的星期几;cardinalCode(),返回指定月份中“基数”日的日期代码(例如,2014年8月第二个星期三)。
下面是实现代码:
#include <math.h>
enum
{
nbrOfDaysPer400Years = 146097,
nbrOfDaysPer100Years = 36524,
nbrOfDaysPer4Years = 1461,
nbrOfDaysPerYear = 365,
unixEpochBeginsOnDay = 135080
};
const int dayOffset[] =
{
0, 31, 61, 92, 122, 153, 184, 214, 245, 275, 306, 337, 366
};
int mod( int dividend, int divisor, int* quotientPtr )
{
*quotientPtr = (int)floor( (double)dividend / divisor );
return dividend - divisor * *quotientPtr;
}
int dateCode( int month, int dayOfMonth, int year )
{
int days;
int temp;
int bYday;
int bYear = year - 1600;
bYday = dayOffset[ mod( month - 3, 12, &temp ) ] + dayOfMonth - 1;
bYear += temp;
bYear = mod( bYear, 400, &days );
days *= nbrOfDaysPer400Years;
bYear = mod( bYear, 100, &temp );
days += nbrOfDaysPer100Years * temp;
bYear = mod( bYear, 4, &temp );
days += nbrOfDaysPer4Years * temp + nbrOfDaysPerYear * bYear + bYday -
unixEpochBeginsOnDay;
return days;
}
int dayOfWeek( int dateCode )
{
int temp;
return mod( dateCode + 3, 7, &temp );
}
void decodeDate( int *monthPtr, int *dayOfMonthPtr, int *yearPtr, int dateCode )
{
int diff;
int diff2;
int alpha;
int beta;
int gamma;
int year;
int temp;
dateCode += unixEpochBeginsOnDay;
dateCode = mod( dateCode, nbrOfDaysPer400Years, &temp );
year = 400 * temp;
dateCode = mod( dateCode, nbrOfDaysPer100Years, &temp );
if ( temp == 4 )
{
--temp;
dateCode += nbrOfDaysPer100Years;
}
year += 100 * temp;
dateCode = mod( dateCode, nbrOfDaysPer4Years, &temp );
year += 4 * temp;
dateCode = mod( dateCode, nbrOfDaysPerYear, &temp );
if ( temp == 4 )
{
--temp;
dateCode += nbrOfDaysPerYear;
}
year += temp;
alpha = 0;
beta = 11;
gamma = 0;
for(;;)
{
gamma = ( alpha + beta ) / 2;
diff = dayOffset[ gamma ] - dateCode;
if ( diff < 0 )
{
diff2 = dayOffset[ gamma + 1 ] - dateCode;
if ( diff2 < 0 )
{
alpha = gamma + 1;
}
else if ( diff2 == 0 )
{
++gamma;
break;
}
else
{
break;
}
}
else if ( diff == 0 )
{
break;
}
else
{
beta = gamma;
}
}
if ( gamma >= 10 )
{
++year;
}
*yearPtr = year + 1600;
*monthPtr = ( ( gamma + 2 ) % 12 ) + 1;
*dayOfMonthPtr = dateCode - dayOffset[ gamma ] + 1;
}
int cardinalCode( int nth, int weekday, int month, int year )
{
int dow1st;
int dc = dateCode( month, 1, year );
dow1st = dayOfWeek( dc );
if ( weekday < dow1st )
{
weekday += 7;
}
if ( nth < 0 || nth > 4 )
{
nth = 4;
}
dc += weekday - dow1st + 7 * nth;
if ( nth == 4 )
{
int tMonth, tDayOfMonth, tYear;
decodeDate( &tMonth, &tDayOfMonth, &tYear, dc );
if ( tMonth != month )
{
dc -= 7;
}
}
return dc;
}
一个立即显而易见的效率问题是mod()函数。正如你所期望的,它提供了两个整数被除数的商和余数。C/C++提供了模运算符(%),似乎是更好的选择; 然而,标准没有指定这个操作应该如何处理负被除数。(有关更多信息,请参见这里。)
可能有一种便携式的解决方案,使用高效的整数运算; 然而,我在这里选择了一个稍微不那么高效但保证在所有平台上正确的解决方案。
日期代码只是相对于基准日期的天数偏移量。我选择了1600年3月1日,因为它是一个400年的公历周期的开始,早到足以使我们遇到的所有日期都产生一个正整数的日期代码。然而,在基准日期之前使用日期代码也没有错。由于我们使用的是稳定/可移植的模运算,所有的数学运算对于负日期代码都有效。
有些人不喜欢我的非标准基准日期,所以我决定采用标准的Unix纪元,它从1970年1月1日开始。我定义了unixEpochBeginsOnDay来偏置日期代码以从所需的日期开始。如果您想使用不同的基准日期,您可以用您喜欢的一个值替换这个值。
计算日期代码就像将月份、日和年传递给dateCode()一样简单:
int dc = dateCode( 2, 21, 2001 )
我已经编写了 dateCode,使其可以接受超出月份和日数范围的值。你可以将月份看作是给定年份的一月后的整数个月加一。以下是一些测试示例:
assert(dateCode( 14, 1, 2000 ) == dateCode( 2, 1, 2001 ));
assert(dateCode( 5, 32, 2005 ) == dateCode( 6, 1, 2005 ));
assert(dateCode( 0, 1, 2014 ) == dateCode(12, 1, 2013 ));
使用非规范的月份和日期值调用dateCode,然后使用decodeDate进行转换,是将日期规范化的有效方法。例如:
int m, d, y;
decodeDate( &m, &d, &y, dateCode( 8, 20 + 90, 2014 ));
printf("90 days after 2014-08-20 is %4d-%02d-%02d\n", y, m, d);
输出应该是:
2014-08-20后的90天是2014-11-18。
decodeDate()始终为月份和dayOfMonth生成规范值。
dayOfWeek()仅返回dateCode的模数7,但我必须将dateCode偏置3,因为1970年1月1日是星期四。如果您喜欢从周一开始而不是从其他日期开始,则修复Weekdays枚举并根据需要更改偏差。
cardinalCode()提供了这些方法的有趣应用程序。第一个参数提供了月份的第几周(“nth”),第二个参数提供了工作日。因此,要查找2007年8月的第四个星期六,您将:
int m, d, y;
decodeDate( &m, &d, &y, cardinalCode( 3, eSaturday, 8, 2007 ) );
printf( "%d/%02d/%d\n", m, d, y );
这将产生答案:
2007年8月25日
请注意,上面示例中的第n个参数3指定了第四个星期六。我曾经考虑过这个参数应该是基于0还是1。出于某种原因,我选择了:0=第一个,1=第二个,2=第三个等。即使最短的月份每个工作日也有四次出现。值4具有特殊含义。人们会期望它返回所请求工作日的第五次出现;然而,由于该月份可能有也可能没有第五次出现,我决定返回该月份的最后一次出现。
例如,要显示明年每个月的最后一个星期一:
int i, m, d, y;
for (i=1; i <= 12; ++i) {
decodeDate( &m, &d, &y, cardinalCode( 4, eMonday, i, 2015 ) );
printf( "%d/%02d/%d\n", m, d, y );
}
最后一个例子,展示了使用cardinalCode()的一种用途,显示距离下次大选的天数:
#include <stdio.h>
#include <time.h>
#include "datecode.h"
void main()
{
int eYear, eday, dc;
int eY, eM, eD;
time_t now;
struct tm bdtm;
time(&now);
if (localtime_r(&now, &bdtm) == NULL) {
printf("Error\n");
return 1;
}
eYear = bdtm.tm_year + 1900;
dc = dateCode(bdtm.tm_mon + 1, bdtm.tm_mday, eYear);
if ((eYear % 2) != 0) {
++eYear;
}
for(;;) {
eday = cardinalCode(0, eTuesday, 11, eYear);
if (eday >= dc) break;
eYear += 2;
}
decodeDate(&eM, &eD, &eY, eday);
printf("Today is %d/%02d/%d\neday is %d/%02d/%d, %d days from today.\n",
bdtm.tm_mon + 1, bdtm.tm_mday, bdtm.tm_year + 1900,
eM, eD, eY, eday - dc);
}
boost::gregorian::date
可能是从这里开始的最佳方式。 - Alexandre C.