关于c ++:将天数转换为年(包括闰年)的高效算法

Efficient algorithm for converting number of days to years (including leap years)

问题

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

自参考日期起,我有几天的时间(在我的例子中是公元0001年1月1日),包括自参考日期起经过的闰日。如何有效地将此数字转换为年份Y、月份M和日期D

我希望尽可能高效地完成这项工作,所以最好的实现显然具有O(1)复杂性。

下一节将解释我已经学到的一些东西。

闰年

要确定一年是否为闰年,有几个规则:

  • 可被4除的年份是跳跃
  • 规则1的例外情况:可以用100除的年份不是跳跃
  • 规则2的例外情况:可以用400除的年份是闰年。
  • 这将以如下代码进行转换:

    1
    2
    3
    4
    5
    6
    7
    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;
    }

    计算一年前跨越多少年的有效方法是:

    1
    2
    3
    4
    5
    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中减去这个数字。这将给出一年中的哪一天。

    把每个月开始的天数放在一张表中,我们可以很容易地计算出月份。我还创建了一个函数,如果年份是闰年,月份大于或等于2,它将加1。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 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];
    }

    我的主意

    我的算法相当复杂,看起来是这样的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    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时,它不起作用)。

    其他注意事项

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


    使用一个老笑话的要点,"我不会从这里开始"。

    例如,你想了解一下"现代"之前历法的各种变化,比如1752年发生的变化。


    这里有一些提示。注:在这个练习中,我假设当N=0时,Y % 400 == 0是。

    1:每400年有固定的天数,按1〔3〕天计算。

    +100表示闰年,+1表示每400年闰年,-4表示每100年不闰年。

    所以您的第一行代码是:

    1
    2
    3
    4
    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)中的代码,我们就可以算出年份了。记住,每四个世纪都从闰年开始。因此,您可以使用以下内容完成年度计算:

    1
    2
    3
    4
      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。


    多年来,我在解决公历日期问题上做了许多失败的尝试。我大约在15年前开发了这个代码,它继续运行良好。因为我很久以前就编写了这个代码的版本,它在原生C中,但是很容易编译成C++程序。如果你愿意的话,可以在约会课上把这些包起来。好的。

    我的代码基于将所有闰年规则合并为400年周期。根据公历闰年的规定,每400年的周期正好有146097天。好的。

    我采用的一种优化方法是将1月和2月移到上一年年底。这使得闰日(如果有)总是落在一年的最后一天。这允许我构建一个表(dayoffset),它以天为单位提供从3月1日开始的距离。因为闰日会在最后落下,所以这个表对于闰年和非闰年是准确的。好的。

    我将从头文件开始。好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    #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由四种方法组成:date code()计算公历日期的日期代码。decodeDate()根据日期代码计算公历的月、日和年。dayOfWeek()返回日期代码的星期几。cardinal code()返回特定月份的"基数"日(例如,2014年8月2日星期三)的日期代码。好的。

    实现方法如下:好的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    #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日开始。我定义了unixepochbeginsoday,将日期代码偏向于在所需日期开始。如果您想使用不同的基准日期,可以用您喜欢的日期替换该值。好的。

    计算日期代码与传递month、dayofmonth和year to date code()一样简单:好的。

    1
    int dc = dateCode( 2, 21, 2001 );  // returns date code for 2001-Feb-21

    我想写第2代如此接受它的值是从一个dayofmonth和范围。你认为作为一个整数加1后的数月或一年。这是一个测试来证明:

    1
    2
    3
    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 ));

    一个非canoncial呼叫第2代和dayofmonth值,然后转换回一个有效的方式来decodedate,要约会。例如:

    1
    2
    3
    4
    int m, d, y;
    decodeDate( &m, &d, &y, dateCode( 8, 20 + 90, 2014 ));
    printf("90 days after 2014-08-20 is %4d-%02d-%02d
    "
    , y, m, d);

    输出应:

    90天之后,2014年08月20是2014年11 18

    decodedate(典型值)和一个副车架总dayofmonth。

    简单的dayofweek(7)返回系数的第2代,但我们有一个偏置由第2代3自1970年01月是星期四。如果你想开始你的一天在一周不同的比,然后修复weekdays枚举和改变为所需的偏压。

    cardinalcode()方法的应用提供了有趣的论文。第一个参数提供一个月的周数("N"),第二个参数提供的工作。所以找到四星期六在2007年8月,你会:

    1
    2
    3
    4
    int m, d, y;
    decodeDate( &m, &d, &y, cardinalCode( 3, eSaturday, 8, 2007 ) );
    printf("%d/%02d/%d
    "
    , m, d, y );

    答案:这副车架

    2007年8月25日

    注意参数的影响,在上面的例子应该提醒3,第四星期六。我debated是否这个参数应该零基或酮基。为解决基于任何原因,I 0 = 1 = 2第一,第二,第三等),甚至每一个月有四条occurrences学院工作。4有一个特殊的价值意义。它返回一个惊喜第五周日发生的请求;然而,从一个可能或不可能有一个第五的发生,决定返回的最后一个月发生。

    例如,一个显示每个月的最后一年。

    1
    2
    3
    4
    5
    6
    int i, m, d, y;
    for (i=1; i <= 12; ++i) {
        decodeDate( &m, &d, &y, cardinalCode( 4, eMonday, i, 2015 ) );
        printf("%d/%02d/%d
    "
    , m, d, y );
    }

    最后一个例子使用一个cardinalcode,illustrating()显示,数天,直到下一个大选。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    #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
    "
    );
           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
    eday is %d/%02d/%d, %d days from today.
    "
    ,
               bdtm.tm_mon + 1, bdtm.tm_mday, bdtm.tm_year + 1900,
               eM, eD, eY, eday - dc);
    }

    好的。


    I have a number of days N since a reference date (in my case that would be Jan 1, 0001 AD)...

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

    维基百科是一个涉及面很广的学科的良好起点。

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


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

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

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

  • Years which are divisible by 4 are leap
  • Exception to rule 1: years that are divisible with 100 are not leap
  • Exception to rule 2: years that are divisible with 400 are leap

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

    最后,您将得到一年的粗略估计,以及在此之前所有闰年的数量。现在,您可以很容易地计算精确的年份,而无需迭代任何内容:

    1
    leapYearCount * 366 + (lastCalculatedYear - leapYearCount) * 365


    这个

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

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

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


    1
    2
    3
    4
    5
    6
    7
    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;
    }


    为了加快计算年份,您可以构建一个查找表

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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)。


    你为什么要重新设计日期?

    日期数学很好理解。标准C库(也就是说,C,不只是C++)已经有很多年的日期函数了。

    正如其他人所指出的,Boost-Date类的设计也很好,而且易于使用。

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