问题
我正在编写一个用c ++保存日期的类,我发现了以下问题:
我有好几天了 N
自参考日期(在我的情况下将是公元0001年1月1日),包括自参考日以来经过的闰日。 如何将此数字转换为一年 Y
,月 M
和白天 D
有效率的?
我希望尽可能高效地完成这项工作,因此最佳实现显然会具有O(1)复杂性。
接下来的部分将解释我已经学到的一些东西。
闰年
要确定一年是否跳跃,有一些规则:
- 可被4整除的年份是飞跃
- 规则1的例外:可以被100整除的年份不是跳跃
- 规则2的例外:可以被400整除的年份是飞跃
这将转换为这样的代码:
bool IsLeapYear(int year)
{
// Corrected after Henrick's suggestion
if (year % 400 == 0) return true;
if ((year % 4 == 0) && (year % 100 != 0)) return true;
return false;
}
计算一年前飞跃多少年的有效方法是:
int LeapDaysBefore(int year)
{
// Years divisible by 4, not divisible by 100, but divisible by 400
return ((year-1)/4 - (year-1)/100 + (year-1)/400);
}
计算月份
一旦我找到了这一年,我可以计算到当年的天数,我可以从N中减去这个数字。这将给我一年中的这一天。
保持每月开始的日期编号表,我们可以轻松计算月份。我还创建了一个函数,如果年份是跳跃,则将加1,月份大于或等于2。
// What day each month starts on (counting from 0)
int MonthDaySt[] = { 0, 31, 59, 90, 120, 151, 181, 212,
243, 273, 304, 334, 365 };
int MonthDayStart(int month, bool leap)
{
if (leap && month >= 2) return MonthDaySt[month]+1;
return MonthDaySt[month];
}
我的想法
我的算法非常复杂,看起来像这样:
void GetDate(int N, int &Y, int &M, int &D)
{
int year_days;
// Approximate the year, this will give an year greater or equal
// to what year we are looking for.
Y = N / 365 + 1;
// Find the actual year, counting leap days
do {
Y--;
// Calculate the actual number of days until the
// approximate year
year_days = Y * 365 + LeapDaysBefore(year);
} while (year_days > N);
// Add 1, because we start from year 1 AD (not 0)
Y++;
// Calculate month
uint64_t diff = N - year_days; // Will give us the day of the year
bool leap = IsLeapYear(Y); // Is current year leap?
// Use table to find month
M = 0;
while (MonthDayStart(M, leap) <= diff && M <= 12)
M++;
// Calculate day
D = diff - MonthDayStart(M - 1, leap) + 1;
}
该函数可能有一些错误(例如,当N为0时,它不起作用)。
其他说明
我希望我的算法仍然正确,因为我对这个问题做了一些改动。如果我错过了什么,或者出了什么问题,请告诉我修改它。抱歉这个长期的问题。
这里有几点建议。注意:对于本练习,我将假设何时 N=0
那 Y % 400 == 0
。
1: 每400年有固定的天数 (400 * 365) + 100 + 1 - 4
。
该 +100
是为了闰年, +1
是每400年闰年和 -4
是因为每100年没有闰年。
所以你的第一行代码将是:
GetDate(int N, int &Y, int &M, int &D) {
const int DAYS_IN_400_YEARS = (400*365)+97;
int year = (N / DAYS_IN_400_YEARS) * 400;
N = N % DAYS_IN_400_YEARS;
2: 如果您将3月1日视为一年的第一天,您可以让您的生活更轻松
3: 添加到代码中 (1),我们可以锻炼一年。请记住,每四个世纪都以闰年开始。因此,您可以使用以下内容完成年度计算:
const int DAYS_IN_100_YEARS = (100*365) + 24;
year += 100 * (N / DAYS_IN_100_YEARS) + (N < DAYS_IN_100_YEARS ? 1 : 0); // Add an extra day for the first leap year that occurs every 400 years.
N = N - (N < DAYS_IN_100_YEARS ? 1 : 0);
N = N % DAYS_IN_400_YEARS;
4: 现在你已经整理了几年,其余的很容易就像馅饼一样(只记得 (2),这个过程很简单)。
或者你可以使用 提高::日期。
多年来,我在解决格里高利日期问题上做了一些失败的尝试。我大约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月的第2个星期三)。
这是实施:
#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年3月1日,因为它是400年格里高利周期的开始,这个周期足够早,所以我们可能遇到的所有日期都会产生一个正整数的日期代码。但是,基准日期之前的日期代码没有任何错误。由于我们使用稳定/便携式模运算,因此所有数学运算都适用于负数日期代码。
有些人不喜欢我的非标准基准日期,所以我决定采用标准的Unix纪元,从1970年1月1日开始。我定义了unixEpochBeginsOnDay来偏置日期代码以在所需的日期开始。如果要使用其他基准日期,则应将此值替换为您喜欢的值。
计算日期代码就像将月份,dayOfMonth和year传递给dateCode()一样简单:
int dc = dateCode( 2, 21, 2001 ); // returns date code for 2001-Feb-21
我编写了dateCode,以便它接受超出month和dayOfMonth范围的值。您可以将月份视为一个加上给定年份的1月之后的整数月份。这里有一些测试来证明:
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()始终为month和dayOfMonth生成规范值。
dayOfWeek()只返回dateCode的模数7,但我不得不将dateCode偏向3,因为1970年1月1日是星期四。如果您希望在与星期一不同的一天开始您的一周,那么请修复工作日枚举并根据需要更改偏差。
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);
}