Эффективный алгоритм преобразования количества дней в несколько лет (включая високосные годы)

Проблема

Я пишу класс для хранения дат в С++, и я нашел следующую проблему:

У меня есть количество дней N со ссылочной даты (в моем случае это будет 1 января 0001 г. н.э.), включая дни високосного перехода, прошедшие со дня ссылки. Как я могу преобразовать это число в год Y, месяц M и день D эффективно?

Я хотел бы сделать это максимально эффективно, поэтому наилучшая реализация, очевидно, будет иметь сложность O (1).

В следующих разделах рассказывается о некоторых вещах, которые я уже узнал.

Високосные годы

Чтобы определить, является ли год прыжком или нет, существует несколько правил:

  • Годы, которые делятся на 4, являются прыжками
  • Исключение к правилу 1: годы, делящиеся на 100, не являются прыжками.
  • Исключение к правилу 2: годы, делящиеся на 400, являются прыжками

Это будет выглядеть так:

bool IsLeapYear(int year)
{
    // Corrected after Henrick 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).

Другие примечания

Я надеюсь, что мой алгоритм все еще верен, потому что я внес некоторые изменения из оригинала для этого вопроса. Если я что-то пропустил или что-то не так, сообщите мне, чтобы изменить его. И извините за длинный вопрос.

Ответ 1

Вот несколько указателей. Примечание. Для этого упражнения я предполагаю, что когда 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: Вы можете сделать вашу жизнь намного легче, если вы будете лечить 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

Чтобы использовать начальную шутку старой шутки, "я бы не начинал здесь".

Вы хотите прочитать о различных изменениях в календаре до "современных" времен, например, что произошло в 1752 году.

Ответ 3

Это

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

неверно. Он возвращает false за 2000. Лучше:

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

Ответ 4

Очевидно, узким местом является расчет года. Я предлагаю вам это сделать. Когда вы инициализируете календарь, приблизитесь к году (очень грубо), разделив дни на 365. После этого предварительно сформируйте список всех високосных лет до этой оценки. Это должно быть довольно быстро, так как вам не нужно считать все из них, просто добавьте 4 года каждый раз. Кроме того, делая это, подсчитайте, сколько у вас таких. Фактически, вы можете даже считать их в больших пакетах (т.е. Существует 100 високосных годов каждые 400 лет), но вам нужно будет тщательно проверить исключения за високосный год, а не пропустить некоторые из них.

В конце этого вы получите приблизительную оценку года и сумму всех високосных лет до этого. Теперь вы можете рассчитывать на точный год очень легко, без необходимости итерации через что-либо:

leapYearCount * 366 + (lastCalculatedYear - leapYearCount) * 365

Ответ 5

Позвольте мне упростить вопрос, я не буду рассматривать исключения для объяснения. Каждые 4 года происходит прыжок, если у вас 365 * 5 дней, должен быть високосный год (если только не применяется исключение 2). Вы могли бы просто использовать деление для того, чтобы иметь число високосных лет (если игнорировать исключения).

Затем вы можете легко использовать деление и остаток для непиковых лет/месяцев/дней.

Используйте ту же основную интуицию для разрешения Исключения 1 (если количество лет кратно 100, тогда также проверьте Исключение 2)

  • Годы, которые делятся на 4, являются прыжками
  • Исключение к правилу 1: годы, делящиеся на 100, не являются прыжками.
  • Исключение к правилу 2: годы, делящиеся на 400, являются прыжками

Ответ 6

У меня есть число дней N с даты ссылки (в моем случае это было бы 1 января 0001 года AD)...

В этом случае "эффективность" при применении правила 4-100-400 и поиске длин месяца не является вашей основной проблемой. Также помните о многочисленных проблемах, связанных с применением сегодняшнего григорианского календаря к датам, предшествующим его созданию, и тому факту, что григорианский язык не был введен равномерно. (*)

Wikipedia является хорошим, начиная point для очень вовлеченного субъекта.

(*): В зависимости от страны, где-либо между 15 октября 1582 и 15 февраля 1923 года, вообще нет.

Ответ 7

Чтобы ускорить вычисление года, вы можете создать таблицу поиска

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

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

Я сделал ряд неудачных попыток решить проблемы григорианской даты на протяжении многих лет. Я разработал этот код около 15 лет назад, и он продолжает работать хорошо. Потому что я написал версии этого кода так давно, он в native C, но легко скомпилирован в С++-программы. Не стесняйтесь обернуть их в класс Date, если хотите.

Мой код основан на объединении всех правил високосного года с 400-летним циклом. В соответствии с правилами григорианского високосного года каждый 400-летний цикл имеет ровно 146 097 дней.

Оптимизация, которую я использовал, - это переместить январь и февраль к концу предыдущего года. Это означает, что високосный день (если присутствует) всегда выпадает на последний день года. Это позволяет мне построить таблицу (dayOffset), которая обеспечивает расстояние в дни с 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() возвращает код даты для "кардинального" дня определенного месяца (например, 2-я среда августа 2014 года).

Здесь реализация:

#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/С++ предоставляет оператор модуля (%), который, казалось бы, был бы лучшим выбором; однако в стандартах не указывается, как эта операция должна обрабатывать отрицательные дивиденды. (См. здесь для получения дополнительной информации).

Существует, вероятно, переносное решение, которое использует эффективную математику с целым числом; однако я выбрал здесь тот, который немного менее эффективен, но гарантированно корректен на всех платформах.

Код даты - это просто смещение в дни от базовой даты. Я выбрал 1600-марта-01, потому что это начало 400-летнего григорианского цикла, который достаточно рано, так что все даты, которые мы, вероятно, встретим, приведут к тому, что код даты будет положительным целым числом. Тем не менее, нет ничего неправильного в отношении кодов даты до базовой даты. Поскольку мы используем стабильную/портативную модульную операцию, вся математика хорошо работает для отрицательных кодов даты.

Некоторым не нравится моя нестандартная базовая дата, поэтому я решил принять стандартную эпоху Unix, которая начинается с 1970-января-01. Я определил unixEpochBeginsOnDay для смещения кода даты, чтобы начать с нужной даты. Если вы хотите использовать другую базовую дату, вы должны заменить это значение на тот, который вы предпочитаете.

Вычисление кода даты так же просто, как передача месяца, dayOfMonth и year to dateCode():

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

Я написал dateCode так, чтобы он принимал значения, которые находятся вне диапазона для month и 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 ));

Вызов dateCode с неканоническими месячными и dayOfMonth значениями, а затем преобразование обратно с 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);

Выход должен быть:

Через 90 дней после 2014-08-20 - 2014-11-18

decodeDate() всегда производит канонические значения для месяца и дняOfMonth.

dayOfWeek() просто возвращает модуль 7 кода даты, но я должен был уклонить дату Кодекса на 3 с 1970 по январь-01 в четверг. Если вы предпочитаете начать свою неделю в другой день, чем в понедельник, затем исправьте перечисление будних дней и измените предвзятость по желанию.

cardinalCode() обеспечивает интересное применение этих методов. Первый параметр предоставляет номер недели месяца ( "nth" ), а второй параметр - день недели. Итак, чтобы найти четвертую субботу в августе 2007 года, вы бы:

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

Что дает ответ:

8/25/2007

Обратите внимание, что 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);
}

Ответ 10

Почему вы изобретаете даты?

Математика даты хорошо понята. Стандартная библиотека C (справа, C, а не только С++) имеет функции даты в течение многих лет.

Как показали другие, классы повышения дат также хорошо разработаны и просты в использовании.

При поиске ответа первый вопрос должен быть, проблема уже решена. Эта проблема была решена в течение многих лет.