多年来,我在解决公历日期问题方面进行了多次失败的尝试。我在大约 15 年前开发了这段代码,并且它继续表现良好。因为我很久以前写过这段代码的版本,它是用原生 C 语言编写的,但很容易编译成 C++ 程序。如果您愿意,可以随意将它们包装在 Date 类中。
我的代码基于将所有闰年规则组合成一个 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;
/*
we take the approach of starting the year on March 1 so that leap days fall
at the end. To do this we pretend Jan. - Feb. are part of the previous year.
*/
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 has the number of days relative to 1/1/1970, shift this back to 3/1/1600 */
dateCode += unixEpochBeginsOnDay;
dateCode = mod( dateCode, nbrOfDaysPer400Years, &temp );
year = 400 * temp;
dateCode = mod( dateCode, nbrOfDaysPer100Years, &temp );
/* put the leap day at the end of 400-year cycle */
if ( temp == 4 )
{
--temp;
dateCode += nbrOfDaysPer100Years;
}
year += 100 * temp;
dateCode = mod( dateCode, nbrOfDaysPer4Years, &temp );
year += 4 * temp;
dateCode = mod( dateCode, nbrOfDaysPerYear, &temp );
/* put the leap day at the end of 4-year cycle */
if ( temp == 4 )
{
--temp;
dateCode += nbrOfDaysPerYear;
}
year += temp;
/* find the month in the table */
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 )
{
/* check that the fifth week is actually in the same month */
int tMonth, tDayOfMonth, tYear;
decodeDate( &tMonth, &tDayOfMonth, &tYear, dc );
if ( tMonth != month )
{
dc -= 7;
}
}
return dc;
}
mod() 函数是一个显而易见的效率问题。如您所料,它提供了两个积分红利的商和余数。C/C++ 提供了模数运算符 (%),这似乎是一个更好的选择;但是,标准没有具体说明该操作应如何处理负股息。(有关更多信息,请参见此处)。
可能有一个使用有效整数数学的便携式解决方案;但是,我在这里选择了一个效率稍低的,但保证在所有平台上都是正确的。
日期代码只是相对于基准日期的天数偏移量。我选择了 1600-March-01,因为它是一个 400 年的公历周期的开始,它足够早,因此我们可能遇到的所有日期都会产生一个正整数的日期代码。但是,基准日期之前的日期代码没有任何错误。由于我们使用的是稳定/便携的模运算,因此所有数学运算都适用于负日期代码。
有些人不喜欢我的非标准基准日期,所以我决定采用标准的 Unix 纪元,从 1970 年 1 月 1 日开始。我定义了 unixEpochBeginsOnDay 以使日期代码在所需日期开始。如果你想使用不同的基准日期,你可以用你喜欢的替换这个值。
计算日期代码就像将月份、dayOfMonth 和年份传递给 dateCode() 一样简单:
int dc = dateCode( 2, 21, 2001 ); // returns date code for 2001-Feb-21
我编写了 dateCode 以便它接受超出月份和 dayOfMonth 范围的值。您可以将月份视为给定年份一月之后的整数月数。这里有一些测试来证明:
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 ));
使用非规范月份和 dayOfMonth 值调用 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,但由于 1970-January-01 是星期四,我不得不将 dateCode 偏置 3。如果您希望在与星期一不同的一天开始您的一周,则修复 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=第二,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> /* only needed for time() and localtime() calls */
#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; /* move to the next election! */
}
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);
}