14

问题

我正在编写一个用于在 C++ 中保存日期的类,我发现了以下问题:

N自参考日期(在我的情况下是公元 001 年 1 月 1 日)以来,我有很多天,包括自参考日期以来经过的闰日。如何有效地将这个数字转换为年、YM和日D

我想尽可能高效地做到这一点,所以最好的实现显然具有 O(1) 复杂性。

接下来的部分将解释我已经学到的一些东西。

闰年

要确定一年是否是闰年,有一些规则:

  1. 能被4整除的年份是闰年
  2. 规则 1 的例外:能被 100 整除的年份不是闰年
  3. 规则 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 时它不起作用)。

其他注意事项

我希望我的算法仍然正确,因为我对这个问题的原始算法做了一些更改。如果我遗漏了什么,或者有什么问题,请告诉我修改它。很抱歉这个冗长的问题。

4

10 回答 10

5

用一句老笑话的话来说,“我不会从这里开始”。

您需要了解“现代”之前日历的各种变化,例如 1752 年发生的事情

于 2012-07-23T09:45:59.187 回答
4

这里有一些建议。注意:对于本练习,我将假设N=0Y % 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),过程就简单了)。

或者,您可以使用boost::date

于 2012-07-23T10:09:57.000 回答
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 月的第二个星期三)。

这是实现:

#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);
}
于 2014-08-20T22:44:35.827 回答
1

bool IsLeapYear(int year) 
{ 
    if ((year % 4 == 0) && (year % 100 != 0) && (year % 400 == 0)) return true; 
    else return false; 
}

是不正确的。它返回false2000 年。更好:

bool IsLeapYear(int year) 
{ 
    if (year % 400 == 0) return true; 
    if ((year % 4 == 0) && (year % 100 != 0)) return true; 
    return false; 
}
于 2012-07-23T09:33:36.107 回答
1

显然,瓶颈是年份计算。我建议你这样做。初始化日历时,通过将天数除以 365 来近似年份(非常粗略)。之后,在此估计之前预先形成所有闰年的列表。它应该相当快,因为​​您不需要计算所有这些,只需每次添加 4 年。另外,在做的时候,数一数你有多少这样的东西。实际上,您甚至可以将它们计算在更大的包中(即每 400 年有 100 个闰年),但您需要仔细检查闰年的例外情况,而不是跳过其中的一些。

在此结束时,您将获得年份的粗略估计,以及之前所有闰年的数量。现在您可以非常轻松地计算精确的年份,而无需遍历任何内容:

leapYearCount * 366 + (lastCalculatedYear - leapYearCount) * 365
于 2012-07-23T09:41:20.753 回答
1

让我简化问题,我不会考虑解释的例外情况。每 4 年发生一次闰年,如果您有 365*5 天,则必须有闰年(除非应用了例外 2)。您可以只使用除法来获取闰年的数量(如果忽略例外情况)。

然后,您可以轻松地使用除法和余数来获得非闰年/月/日。

使用相同的基本直觉来解决异常 1(如果年数是 100 的倍数,则还要检查异常 2

  1. 能被4整除的年份是闰年
  2. 规则 1 的例外:能被 100 整除的年份不是闰年
  3. 规则 2 的例外:能被 400 整除的年份是闰年
于 2012-07-23T09:47:31.380 回答
1

自参考日期(在我的情况下是Jan 1, 0001 AD)以来,我有 N 天...

在这种情况下,应用 4-100-400 规则和查找月份长度的“效率”不是您的主要问题。还请注意将今天的公历应用到其成立之前的日期所固有的多个问题,以及公历没有统一引入的事实。(*)

维基百科是一个非常复杂的主题的良好起点。

(*):取决于国家,1582 年 10 月 15 日至 1923 年 2 月 15 日之间的任何时间,分别。实际上,一点也不。

于 2012-07-23T09:55:34.827 回答
0

为了加快年份的计算,你可以建立一个查找表

int[] YearStartDays =
{
    0,                     // 1 AD
    365,                   // 2 AD
    365 + 365,             // 3 AD
    365 + 365 + 365,       // 4 AD
    365 + 365 + 365 + 366, // 5 AD (4 was a leap year)
    /* ... */
};

然后,您可以在此数组中进行二进制搜索,即 O(log N) 而不是当前年份查找算法的 O(N)。

于 2012-07-23T09:45:25.307 回答
0
bool IsLeapYear(int year)
{
    boost::gregorian::date d1(year, 1, 1);
    boost::gregorian::date d2 = d1 + boost::gregorian::years(1);
    boost::gregorian::date_duration diff = d2 - d1;
    return diff.days() != 365;
}
于 2014-08-20T20:18:51.073 回答
-3

你为什么要重新发明日期?

日期数学很好理解。标准 C 库(没错,是 C,而不仅仅是 C++)已经有很多年的日期函数了。

正如其他人所指出的那样,提升日期类也设计精良且易于使用。

在寻找答案时,第一个问题应该是,问题是否已经解决。这个问题已经解决了很多年。

于 2012-07-23T15:36:15.613 回答