【问题标题】:Efficient algorithm for converting number of days to years (including leap years)将天数转换为年数(包括闰年)的高效算法
【发布时间】:2012-07-21 12:37:10
【问题描述】:

问题

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

我有很多天 N 自参考日期(在我的情况下是公元 0001 年 1 月 1 日),包括自参考日期以来经过的闰日。 如何有效地将这个数字转换为年份Y、月份M 和日期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 时它不起作用)。

其他说明

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

【问题讨论】:

  • 您是否考虑过将年数除以 400,然后考虑每 100 年,然后找到确切的年份?
  • @nhahtdh 是的,我想到了,但还没有实现。
  • 我很好奇:你多久调用一次这个函数来显着影响你的程序速度?
  • 使用boost::gregorian::date 可能是最好的方法。

标签: c++ algorithm time


【解决方案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; 
}

【讨论】:

  • 我想我知道为什么,因为这些不可能同时发生:(year % 100 != 0) &amp;&amp; (year % 400 == 0)。我更正了原来的问题。
【解决方案2】:

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

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

leapYearCount * 366 + (lastCalculatedYear - leapYearCount) * 365

【讨论】:

  • 更正,每400年有97个闰年。消除能被 100 整除的年份,然后再加回能被 400 整除的年份,得到 97。
  • @Tibi,是的,这就是you will need to check for the the leap year exceptions carefully, not to skip some of them. 部分的全部内容:P
【解决方案3】:

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

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)。

【讨论】:

  • 我想到了这种方法,由于闰规则每 400 年重复一次,它可以工作。但是,它以速度换取内存。
  • 一个问题是第 4 年不是闰年,因为闰年​​还没有发明。是否有 4 年是值得怀疑的。:-)
【解决方案4】:

借用一句老笑话的妙语,“我不会从这里开始”。

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

【讨论】:

  • 而 1752 年采用公历的只有大英帝国。它在其他地方的采用发生在不同的年份,或者在某些地方经过多年。
  • 儒略 - 公历问题。如果我们假设一切都在公历中,我认为这不是问题。转换也不太复杂。
  • 我好想知道这个笑话是什么!
  • 这超出了申请的目的,为了方便,我选择了0001年1月1日。
  • @Tibi - 争论是它方便,因为您必须确定 0001 年 1 月 1 日是什么意思。这不是历史日期,所以你必须弥补。 1970 年或 2000 年作为日志日期的基准年有什么问题?
【解决方案5】:

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

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

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

  1. 能被 4 整除的年份是闰年
  2. 规则 1 的例外:能被 100 整除的年份不是闰年
  3. 规则 2 的例外:能被 400 整除的年份是闰年

【讨论】:

  • 所以你的意思是我应该忽略例外情况来近似一年,然后找到真正的年份?
  • 不,永远不要忽略异常。我只向您展示了您可以使用第一条规则获得闰年的数量。现在的艰巨任务是检查如何“交叉”规则以保证两种例外情况。
【解决方案6】:

自参考日期(在我的情况下是 0001 年 1 月 1 日)我有 N 天...

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

Wikipedia 是一个good,从point 开始,指向一个非常复杂的主题。

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

【讨论】:

  • 我使用(proleptic)公历,因为它是最简单的方法,我什至不想打扰其他日历。另外,为了方便起见,我选择了参考日期,“0”日表示第一年的第一天。
  • 在引入日历之前的几年会出错,但这超出了我的申请目的。
  • @Tibi:那为什么要在第一行选择公元 1 年作为参考日期,如果公历之前的日期超出范围?
  • 我并不是说公历之前的日期超出了范围,我的意思是对于我的应用程序,它们不必是 100% 准确的。引入公历之后的日期是准确的,但之前的日期不一定是准确的。我正在尝试创建一个游戏,根据故事情节我可能需要一些较早的日期,但我还没有决定一个故事。它不是一个历史数据库或任何必须 100% 精确的东西。
【解决方案7】:

这里有一些提示。注意:对于本练习,我将假设当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),过程就简单了)。 p>

您也可以使用boost::date

【讨论】:

  • 有些东西我不明白...为什么DAYS_IN_100_YEARS = (100*365) - 24?不应该是+24吗?当你把它加到年份时,你应该乘以100
  • 最佳答案,因为它实际上帮助我解决了问题,而不是像其他答案一样添加另一个问题。
【解决方案8】:

为什么要重新发明日期?

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

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

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

【讨论】:

    【解决方案9】:
    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;
    }
    

    【讨论】:

    • 这并没有回答完整的问题,它只是改变了功能的一部分。
    【解决方案10】:

    多年来,我在解决公历日期问题方面进行了多次失败的尝试。我在大约 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++ 提供了模数运算符 (%),这似乎是一个更好的选择;但是,标准没有具体说明该操作应如何处理负股息。 (更多信息请参见here)。

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

    日期代码只是从基准日期偏移的天数。我选择了 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);
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-03-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多