将天数转换为年数(包括闰年)的高效算法

时间:2012-07-23 09:28:05

标签: c++ algorithm time

问题

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

自参考日期(在我的情况下是公元0001年1月1日)以来,我有几天N,包括自参考日以来经过的闰日。 如何有效地将此数字转换为Y,月MD

我希望尽可能高效地执行此操作,因此最佳实现显然会具有O(1)复杂性。

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

闰年

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

  1. 可被4整除的年份是飞跃
  2. 规则1的例外:可以被100整除的年份不是跳跃
  3. 规则2的例外:可以用400整除的年份是跳跃
  4. 这会在以下代码中翻译:

    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时它不起作用)。

    其他说明

    我希望我的算法仍然正确,因为我对此问题做了一些改动。如果我错过了什么,或者出了什么问题,请告诉我修改它。抱歉这个长期的问题。

10 个答案:

答案 0 :(得分:5)

要使用旧笑话的妙语,“我不会从这里开始”。

你会想要了解“现代”时代之前对日历的各种变化,例如,1752年发生的事情

答案 1 :(得分:3)

以下是一些指示。注意:对于本练习,我将假设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),过程很简单。)

或者您可以使用boost::date

答案 2 :(得分:1)

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

不正确。它返回2000年的false。更好:

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

答案 3 :(得分:1)

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

在此结束时,您将获得年度的粗略估计,以及之前所有闰年的数量。现在,您可以非常简单地计算精确的年份,而无需迭代任何事情:

leapYearCount * 366 + (lastCalculatedYear - leapYearCount) * 365

答案 4 :(得分:1)

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

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

使用相同的基本直觉来解析例外1 (如果年数是100的倍数,那么还要检查例外2

  
      
  1. 可被4整除的年份是飞跃
  2.   
  3. 规则1的例外:可以被100整除的年份不是跳跃
  4.   
  5. 规则2的例外:可以用400整除的年份是跳跃
  6.   

答案 5 :(得分:1)

  

自参考日期起我有多天N(在我的情况下,公元0001年1月1日)......

在这种情况下,应用4-100-400规则并查找月份长度的“效率”不是您的主要问题。请注意将今天的格里高利历应用于其开始之前的日期所固有的多个问题,格里高利未统一引入的事实。 (*)

Wikipediagoodpoint为主题的{{3}}。

(*):根据国家,在1582年10月15日至1923年2月15日之间的任何地方,分别为实际上根本没有。

答案 6 :(得分:1)

多年来,我做了许多尝试解决格里高利日期问题的失败尝试。我大约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 ++提供了模数运算符(%),这似乎是一个更好的选择;但是,标准没有规定该操作应如何处理负红利。 (有关详细信息,请参阅here。)

可能有一种便携式解决方案,它使用有效的整数数学;但是,我在这里选择的效率略低,但在所有平台上都保证正确。

日期代码只是从基准日期开始的天数偏移量。我选择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具有特殊含义。人们会期望它返回所请求的工作日的第五次出现;但是,由于月份可能会或可能不会出现第五次,我决定返回该月的 last 次。

例如,要显示明年每个月的最后一个星期一:

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);
}

答案 7 :(得分: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)。

答案 8 :(得分: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;
}

答案 9 :(得分:-3)

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

日期数学很好理解。标准的C库(正确的,C,而不仅仅是C ++)已经具有多年的日期功能。

正如其他人所指出的那样,提升日期等级也设计得很好并且易于使用。

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