Зона кода

А давайте немного попрограммируем...

Переходы между датами и номерами дней в григорианском календаре

C99Решение задач

Захотелось мне узнать, сколько дней я прожил на свете. И решил не считать, а написать программку. Об этой программе я и собираюсь рассказать.

В основе этой программы будут лежать две функции. Первая из них будет по дате находить номер соответствующего ей дня, исходя из того, что нумерация дней стартует от начала летоисчисления по григорианскому календарю. Используя эту функцию, мы сможем найти количество дней, отделяющих одну заданную дату от другой, путём получения номеров дней, соответствующих этим датам, и вычисления их разности.

Вторая функция будет обратной по отношению к первой. Она будет заниматься преобразованием номера дня в дату. Данная функция, используемая совместно с первой, позволит нам получать дату, отстоящую от заданной на указанное количество дней в ту или иную строну. Кстати, сразу же скажу, что в этой статье слово "день" мы будем понимать как синоним слова "сутки".

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

Программу напишем на языке C99. Однако формулы и приёмы для работы с датами, которые мы получим, могут быть реализованы на любом языке программирования.

Статья адресована новичкам в программировании, однако замечу, что алгоритмы обработки дат, которые мы рассмотрим, не настолько тривиальны, как может показаться на первый взгляд.

Предварительные замечания

Григорианский календарь, в рамках которого мы будем рассматривать обработку дат в данной статье, был введён в 1582 году в католических странах. Он пришёл на смену юлианскому календарю. В Советском Союзе переход от юлианского календаря к григорианскому состоялся лишь в 1918-м году. А, например, в Саудовской Аравии григорианский календарь был принят почти век спустя — в 2016-м году.

Разумеется, совсем несложно распространить григорианский календарь и на время, предшествующее его созданию. Такой расширенный григорианский календарь стал называться пролептическим. Отметим, что пролептический григорианский календарь захватывает сразу весь формально бесконечный промежуток времени до 1582 года. Даты, попадающие в период, предшествующий началу летоисчисления по данному календарю, содержат отметку "до нашей эры", позволяющую отличать их от остальных дат.

Таким образом, с формальной точки зрения, любой момент времени от −∞ до +∞ приходится на конкретную дату по григорианскому календарю.

В этой статье мы не будем работать с датами, предшествующими началу летоисчисления. Таким образом, мы будем иметь дело с неким "усечённым" пролептическим григорианским календарём, называя его в дальнейшем, для простоты, просто григорианским.

Перед тем, как перейти к рассмотрению структуры григорианского календаря, сделаю небольшое отступление.

Сейчас, когда я пишу эти строки, зима 2019-го года доживает последние деньки. Сегодня — 27-е февраля. Любой (ну или почти любой) человек, рассматривая сегодняшнюю дату, понимает, что 27-е число — это текущий день, который ещё на закончился. А 2-й месяц в году, т.е. февраль — это текущий месяц, который ещё не закончился также.

Но, похоже, что далеко не все понимают (или до конца не осознают), что 2019-й год также пока ещё не закончился и пока ещё является текущим. Таким образом, количество полных лет, прошедших с начала летоисчисления, равно 2018-ти, а не 2019-ти.

Очень часто в голове обывателя номер года ассоциируется с возрастом человека. Ведь когда мы говорим, что кому-то 19 лет, то понимаем, что 19 лет ему уже исполнилось и уже идёт 20-й год. А вот с номером года ситуация, как я уже сказал выше, иная.

Возможно, читатель с удивлением воспримет мои слова и усомнится в том, что кто-то может не понимать такой очевидной вещи, заключающейся в том, что 2019-й год ещё не закончился. В ответ я предлагаю мысленно перенестись в конец 1999-го года и начало 2000-го. Те читатели, которые в сознательном возрасте застали этот период времени, наверняка помнят о том, какое большое количество народа праздновало окончание второго тысячелетия и начало третьего.

Даже глава государства, которому вдруг захотелось 31-го декабря 1999-го года уйти в отставку, в своём прощальном выступлении выдал фразы: "Наступает 2000 год. Новый век, новое тысячелетие. <…> Сегодня, в последний день уходящего века, я ухожу в отставку. ". Печально, что никто его не поправил, и безграмотность транслировалась на столь высоком уровне. Это же заблуждение повторяет и режиссёр Виталий Манский, снявший нашумевший документальный фильм о том времени, который я сравнительно недавно посмотрел.

Человек к моменту своего 20-летия уже прожил 20 лет. А вот в ночь с 31.12.1999 на 01.01.2000 2000-й год, т. е. последний год второго тысячелетия, а также, 20-го века, только начался. А третье тысячелетие началось годом позже, с началом 2001-го года, т. е. в ночь с 31.12.2000 на 01.01.2001. И я не припомню, чтобы "истинное" начало третьего тысячелетия отмечалось с таким же размахом, как и "ложное".

Итак, год, "входящий" в дату, по отношению к ней является текущим, а чтобы получить полное количество лет, прошедших с начала летоисчисления до этой даты, нужно из данного года вычесть единицу. Таким образом, летоисчисление началось 1-го января 1-го года в 0 часов, 0 минут и 0 секунд. При этом нулевого года не существует, а за секунду до начала летоисчисления был 1-й год до нашей эры.

Давайте теперь договоримся о том, что все дни григорианского календаря, приходящиеся на "нашу эру", мы будем нумеровать, начиная с 01.01.0001. Т. е. этот день будет иметь номер 1, день 02.01.0001 — номер 2, день 03.01.0001 — номер 3 и т. д.

Мы напишем функцию date2day(), преобразующую дату в номер дня и функцию day2date(), выполняющую обратное преобразование.

Теперь поговорим немного о структуре григорианского календаря. Как известно, все года делятся на високосные, состоящие из 366-ти дней, и невисокосные, включающие в себя 365 дней.

Високосным называется год, кратный 4 и при этом, либо не кратный 100, либо кратный 400. Можно то же самое сформулировать иначе: високосный год — это год, либо кратный 400, либо кратный 4, но при этом не кратный 100. Года, отличные от високосных, называются невисокосными.

Примеры високосных лет: 96-й, 2000-й, 2016-й. Примеры невисокосных — 100-й, 1900-й, 2019-й.

А теперь переходим к построению алгоритмов и реализующих их функций.

Алгоритм преобразования даты в номер дня

Итак, предположим, что мы хотим найти n — номер дня, приходящегося на число d месяца номер m года y. Эти день, месяц и год будем называть, в дальнейшем, текущими.

Для начала выясним, сколько дней суммарно содержится в годах, предшествующих текущему. Количество таких лет, как легко заметить, равно y − 1. Если k — количество високосных лет, приходящихся на года, начиная с 1-го и заканчивая годом y − 1, то искомое количество дней (обозначим его, например, s) можно, очевидно, вычислить по формуле:

s = 366 · k + 365 · (y − 1 − k) = 365 · (y − 1) + k.

Как найти k? Нужно, в соответствии с определением високосного года, найти количество чисел, кратных 4, не превышающих y − 1, после чего вычесть из него количество чисел, не превышающих y − 1, кратных 100, но не кратных 400. Количество чисел, кратных a, но не превышающих b, где a и b — натуральные, очевидно, равно [b / a]. Здесь квадратными скобками обозначается целая часть числа, помещённого внутрь них. Таким образом, получаем:

k = [(y − 1) / 4] − ([(y − 1) / 100] − [(y − 1) / 400]) = [(y − 1) / 4] + [(y − 1) / 400] − [(y − 1) / 100].

Теперь найдём количество дней, суммарно содержащихся в месяцах, предшествующих текущему, при условии, что год невисокосен. Обозначим: ri, где 1 ≤ i ≤ 11, — суммарное количество дней в месяцах с 1-го по i-й включительно и, для удобства, положим: r0 = 0. Тогда искомое количество дней равно rm−1.

Наконец, количество дней, прошедших с начала месяца, включая текущий, равно d.

Теперь уже можно переходить к вычислению n. Очевидно, что для случая, когда год y невисокосен, справедлива формула

n = s + rm−1 + d,

или, более подробно,

n = 365 · (y − 1) + [(y − 1) / 4] + [(y − 1) / 400] − [(y − 1) / 100] + rm−1 + d.

Чтобы формула стала справедливой для произвольного года, нужно учесть, что в високосном году февраль содержит на 1 день больше, чем в невисокосном. Так что в случае високосного года формулу нужно скорректировать добавлением единицы к правой части, при условии, что текущий месяц — март или более поздний месяц. Таким образом, общую формулу, справедливую для произвольного года, можно записать так:

n = 365 · (y − 1) + [(y − 1) / 4] + [(y − 1) / 400] − [(y − 1) / 100] + rm−1 + d + ty, m,

где ty, m = 1, если год y — високосный, а m ≥ 3, и ty, m = 0 в противном случае.

Поговорим о программном нахождении числа ty, m. Переформулируем первое определение високосного года, заменив "кратность" эквивалентным ему понятием "нулевой остаток от деления": високосным называется год, имеющий нулевой остаток от деления на 4 и, при этом, либо имеющий ненулевой остаток от деления на 100, либо нулевой от деления на 400. Таким образом, если год y хранится в переменной y, то выполнение следующего фрагмента кода на языке C99

int p = y % 4, q = y % 100, r = y % 400;
int u = !p && (q || !r);

приведёт к тому, что значение переменной u будет равно 1, если год y — високосный; в противном случае u примет нулевое значение.

Рассмотрим теперь следующий фрагмент кода, предполагая, что в переменной m хранится текущий месяц:

int p = y % 4, q = y % 100, r = y % 400;
int t = !p && (q || !r) && m >= 3;

В результате выполнения этого фрагмента в переменную t будет помещено единичное значение, если год y — високосный, а текущий месяц — март или более поздний. В противном случае значение t будет нулевым. Другими словами, переменная t будет содержать число ty, m. Итак, с программным нахождением этого числа мы разобрались.

Преобразование даты в номер дня — функция date2day()

Теперь перейдём к построению функции date2day(). Но для начала создадим пустой файл с расширением "c" и поместим в него директиву препроцессора #include:

#include <stdio.h>

Нам потребуется константный массив для хранения чисел ri. Назовём его days. Этот массив будет глобальным. Объявим и инициализируем его следующим образом:

const int days[] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334};

Как мы видим, элемент массива с индексом i содержит суммарное количество дней, содержащихся в месяцах невисокосного года, номера которых не превышают i.

А вот и код функции date2day():

int date2day(int d, int m, int y)
{
    int p = y % 4, q = y % 100, r = y % 400;
    y--;
    int k = y / 4 + y / 400 - y / 100;
    return 365 * y + k + days[m - 1] + d + (!p && (q || !r) && m >= 3);
}

Функция принимает в качестве аргументов компоненты даты (день, месяц, год), а возвращает номер дня, соответствующий этой дате. Полагаю, что алгоритм нахождения номера дня был описан настолько подробно, что код функции в комментариях не нуждается.

Примеры использования функции date2day()

Рассмотрим несколько вариантов программ, содержащих функцию date2day(). Отличаться друг от друга они будут, в первую очередь, функциями main(), из которых функция date2day() будет вызываться. Кроме, того, в третьей программе будет присутствовать дополнительная функция.

Начнём с программы, находящей номер дня, соответствующего дате её вызова. Для нахождения даты вызова программы воспользуемся инструментами, содержащимися в стандартной библиотеке time.h, предварительно подключив соответствующий заголовочный файл:

#include <time.h>

Переходим к рассмотрению кода функции main().

1.int main()
2.{
3.    time_t cur_t = time(0) + 10800;
4.    struct tm t = *gmtime(&cur_t);
5.    int d = t.tm_mday, m = t.tm_mon + 1, y = t.tm_year + 1900;
6.    printf("Сегодня %02d.%02d.%d\nЭто %d-й день с начала летоисчисления по григорианскому календарю",
7.           d, m, y, date2day(d, m, y));
8.    return 0;
9.}

Типы time_t и struct tm, а также функции time() и gmtime() определены в библиотеке time.h.

Тип time_t является целочисленным. Переменные этого типа служат для хранения времени в секундах, прошедшего с 00:00:00 01.01.1900 по григорианскому календарю. Функция time() возвращает среднее время по Гринвичу, используя системные часы компьютера, в формате time_t. Функция принимает в качестве параметра адрес переменной типа time_t, по которому записывается то же самое значение, которое возвращается функцией. Если передать функции нулевой адрес, то он будет проигнорирован.

Объект типа struct tm представляет собой время, "разделённое" на компоненты: номер дня месяца, номер месяца, номер года, часы, минуты, секунды и т. д. Эти компоненты содержатся в полях данного объекта.

Функция gmtime() принимает в качестве параметра адрес переменной типа time_t и преобразует её в объект типа struct tm , после чего возвращает адрес этого объекта.

Итак, записываем в переменную cur_t текущее время в формате time_t (стр. 3). Слагаемое 10800, представляющее собой количество секунд в 3-х часах, добавляется к значению, возвращённому функцией time(), с целью получения из среднего времени по Гринвичу московского. Преобразуем полученное время в объект t типа struct tm (стр. 4).

Сохраняем в переменных d, m, y число месяца, номер месяца и номер года соответственно, извлекая данную информацию из полей объекта t (стр. 5). При этом учитываем особый формат, используемый для хранения номеров месяца и года в t: месяцы и года нумеруются, начиная с 0, причём 1900-й год имеет номер 0.

Далее выводим на печать сегодняшнюю дату и соответствующий ей номер дня, полученный с помощью функции date2day() (стр. 6-7).

Если скомпилировать программу и запустить её на выполнение 2-го марта 2019-го года, то будет получен следующий результат:

Сегодня 02.03.2019
Это 737120-й день с начала летоисчисления по григорианскому календарю

Следующая программа запрашивает у пользователя дату его рождения, после чего вычисляет и печатает количество дней, которое исполняется пользователю в день вызова программы. Поясним, что под "исполняется" мы понимаем "исполнится" или "уже исполнилось".

Нам понадобится функция round(), округляющая вещественное число до ближайшего целого, поэтому подключим стандартную математическую библиотеку, в которой эта функция определена:

#include <math.h>

Переходим к коду функции main().

 1.int main()
 2.{
 3.    int d1, m1, y1, d2, m2, y2;
 4.    printf("Введите число, номер месяца и год своего рождения, разделённые точками\n");
 5.    scanf("%d.%d.%d", &d1, &m1, &y1);
 6.    time_t cur_t = time(0);
 7.    struct tm t = *localtime(&cur_t);
 8.    d2 = t.tm_mday, m2 = t.tm_mon + 1, y2 = t.tm_year + 1900;
 9.    const char* const strings[] = {"дней", "день", "дня"};
10.    int r = date2day(d2, m2, y2) - date2day(d1, m1, y1);
11.    printf("Сегодня, %02d.%02d.%d, вам исполняется %d %s", d2, m2, y2, r,
12.           strings[(int) round(sqrt(r % 10) + 0.3) % 3]);
13.    return 0;
14.}

Сделаем 2 замечания.

Во-первых, обратите внимание на строки 6 и 7. На этот раз я, для разнообразия, не стал добавлять к результату, возвращаемому функцией time(), число 10800 (стр. 6). Но зато я получил объект t с помощью функции localtime() (стр. 7), а не gmtime(), как ранее. Читатель, думаю, уже догадался, что функция localtime() отличается от gmtime() лишь тем, что создаёт объект типа struct tm, содержащий местое время, т. е. сама учитывает необходимую поправку (+3 часа в моём случае). Функция localtime() так же, как gmtime(), определена в стандартной библиотеке time.h.

Второе замечание касается строк 9 и 12. Я использую механизм, позволяющий после числа записывать корректную форму слова "день", соответствующую этому числу. Этот механизм подробно описан в этой статье, поэтому не буду на нём останавливаться.

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

Вот пример диалога пользователя с программой, запущенной 2 марта 2019-го года.

Введите число, номер месяца и год своего рождения, разделённые точками
07.07.1997
Сегодня, 02.03.2019, вам исполняется 7908 дней

Следующая программа будет вычислять время в днях, прожитое людьми, уже ушедшими из жизни, по датам их рождения и смерти. Уточню, что говоря "такой-то человек прожил столько-то дней", будем понимать, что именно столько дней (суток) исполнилось или исполнилось бы этому человеку в день смерти.

Сначала создадим вспомогательную функцию calc_days():

1.void calc_days(const char *name, int d1, int m1, int y1, int d2, int m2, int y2)
2.{
3.    const char* const strings[] = {"дней", "день", "дня"};
4.    int r = date2day(d2, m2, y2) - date2day(d1, m1, y1);
5.    printf("%s (%02d.%02d.%d-%02d.%02d.%d) прожил %d %s\n", name, d1, m1, y1, d2, m2, y2, r,
6.           strings[(int) round(sqrt(r % 10) + 0.3) % 3]);
7.}

Функция принимает в качестве аргументов (в порядке перечисления соответствующих формальных параметров) адрес строки, содержащей имя человека, день, месяц и год его рождения, день, месяц и год его смерти и выводит на печать сообщение о том, сколько дней прожил этот человек.

Давайте выясним, сколько дней прожили некоторые поэты, каждый из которых умер в относительно молодом возрасте: Пушкин, Лермонтов, Маяковский, Блок, Есенин, Байрон. Вот код функции main(), из которой многократно вызывается функция calc_days() для вычисления времени, в днях, прожитого каждым из поэтов:

int main()
{
    calc_days("Александр Пушкин", 6, 6, 1799, 10, 2, 1837);
    calc_days("Михаил Лермонтов", 15, 10, 1814, 27, 7, 1841);
    calc_days("Владимир Маяковский", 19, 7, 1893, 14, 4, 1930);
    calc_days("Александр Блок", 28, 11, 1880, 7, 8, 1921);
    calc_days("Сергей Есенин", 3, 10, 1895, 28, 12, 1925);
    calc_days("Владимир Высоцкий", 25, 1, 1938, 25, 7, 1980);
    calc_days("Джордж Байрон", 22, 1, 1788, 19, 4, 1824);
    return 0;
}

Выполнение программы приводит к следующему выводу на консоль:

Александр Пушкин (06.06.1799-10.02.1837) прожил 13763 дня
Михаил Лермонтов (15.10.1814-27.07.1841) прожил 9782 дня
Владимир Маяковский (19.07.1893-14.04.1930) прожил 13417 дней
Александр Блок (28.11.1880-07.08.1921) прожил 14861 день
Сергей Есенин (03.10.1895-28.12.1925) прожил 11043 дня
Владимир Высоцкий (25.01.1938-25.07.1980) прожил 15522 дня
Джордж Байрон (22.01.1788-19.04.1824) прожил 13236 дней

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

Для начала давайте найдём с помощью функции date2day() номер дня, соответствующий воскресенью, 10-го марта 2019-го года. Этот номер равен 737128. Несложно заметить, что данное число делится на 7. Это говорит о том, что в ночь с 10-го на 11-е марта 2019-го года исполняется целое число недель, прошедших с начала летоисчисления. Значит первый день летоисчисления, т. е. 1-е января 1-го года приходится на понедельник.

Полученная нами информация приводит нас к очень простому способу получения дня недели по номеру дня. Если мы пронумеруем дни недели, начиная с воскресенья и заканчивая субботой, числами от 0 до 6 соответственно, то номер дня недели может быть получен взятием остатка от деления номера дня на 7.

Так что разумно будет создать массив указателей на C-строки, состоящий из 7-ми элементов, каждый из которых указывает на строку, содержащую день недели, начиная с воскресенья и заканчивая субботой (в порядке возрастания индексов). Тогда остаток от деления номера дня на 7 можно будет использовать как индекс элемента массива, указывающего на строку, содержащую название нужного дня недели. Этот массив мы назовём dotw (от day of the week — день недели (англ.)) и объявим его в очередном варианте функции main().

А вот и код самой функции main():

int main()
{
    const char* const dotw[] = {"воскресенье",
                                "понедельник",
                                "вторник",
                                "среда",
                                "четверг",
                                "пятница",
                                "суббота"};
    int d, m, y;
    printf("Введите начальную дату (число, номер месяца и год, разделённые точками)\n");
    scanf("%d.%d.%d", &d, &m, &y);
    printf(dotw[date2day(d, m, y) % 7]);
    return 0;
}

Наша программа запрашивает у пользователя дату, после чего выводит на печать день недели, соответствующий этой дате. Полагаю, что код функции main() предельно понятен и не нуждается в комментариях.

После компиляции и запуска программы у неё может состояться, например, следующий диалог с пользователем:

09.05.1945
среда

Алгоритм преобразования номера дня в дату

Теперь рассмотрим обратное преобразование, т. е. получение числа месяца d, номера месяца m и номера года y по заданному номеру дня n. Задача эта является уже более сложной, чем получение номера дня по дате. Можно рассматривать различные варианты её решения; я остановлюсь на том, который показался мне оптимальным.

Первый этап заключается в нахождении количества полных лет, прошедших с начала летоисчисления, и количества "оставшихся" дней ("остатка"), т. е. дней, не вошедших в упомянутые года.

Будем исходить из того, что невисокосные года, кратные 100, встречаются относительно редко. Вначале будем действовать так, как будто високосный год — это любой год, кратный 4, а затем внесём необходимые коррективы.

Введём в рамках нашей "немного ложной" гипотезы понятие четвёрки. Четвёрка — это 4 подряд идущих года, первый из которых — високосный. Несложно подсчитать количество дней, содержащихся в четвёрке. Оно равно

366 + 365 · 3 = 1461.

Сколько полных четвёрок можно выделить из n дней? Ответ очевиден — [n / 1461]. Пусть y1 — это количество лет, содержащихся в [n / 1461] четвёрках, т. е.

y1 = [n / 1461] · 4.

Найдём теперь n1 — количество дней, не вошедших в четвёрки:

n1 = n − [n / 1461] · 1461.

Теперь вспоминаем, что, на самом деле, не все года, кратные 4, являются високосными и находим l — количество "ложных" високосных лет, имеющихся среди y1 лет. Очевидно,

l = [y1 / 100] − [y1 / 400].

Каждый "ложный" високосный год даёт нам поправку к остатку дней n1 в виде одного неучтённого дня. Пусть n2 — это новый, "уточнённый" остаток. Тогда

n2 = n1 + l = n1 + [y1 / 100] − [y1 / 400].

Итак, уже без всяких "ложных" гипотез мы может утверждать: n дней теперь разбиты на дни, содержащиеся в y1 годах, и оставшиеся дни в количестве n2. Но ведь может случиться, что из n2 дней можно выделить отличное от нуля количество четвёрок. Это будет иметь место в случае, если число

f = [n2 / 1461]

отлично от 0. Что ж, в этом случае корректируем количество лет, образованных четвёрками, и количество оставшихся дней, не участвующих в образовании четвёрок. Обозначим новое значение количества лет символом y2, а новое значение количества дней — символом n3. Тогда

y2 = y1 + f = y1 + [n2 / 1461],

n3 = n2f · 1461 = n2 − [n2 / 1461] · 1461.

Вполне возможно, что в состав y2 лет входят неучтённые невисокосные года. Найдём их количество t:

t = [y2 / 100] − [y2 / 400] − l.

Если t отлично от 0, то снова корректируем остаток дней n3. Новое значение остатка n4 будет равно

n4 = n3 + t = n3 + [y2 / 100] − [y2 / 400] − l.

Мы снова можем утверждать, что n дней теперь разбиты на дни, содержащиеся в y2 годах, и оставшиеся дни в количестве n4. И снова должны допускать возможность того, что n4 превышает или равно 1461. Если это так, то мы снова должны скорректировать и количество лет, образованных четвёрками, и количество оставшихся дней. Таким образом мы должны действовать до тех пор, пока очередной остаток после корректировки не будет меньше 1461.

К счастью, опытным путём было установлено, что при n ≤ 106, количество лет, образованных четвёрками, не потребуется корректировать более одного раза. Таким образом, при указанном ограничении, наложенном на n, в худшем случае окончательными результатами итераций будут y2 и n4. В дальнейшем, для определённости, будем оперировать именно этими числами.

Если оказывается, что n4 = 0, то искомой датой, очевидно, будет 31-го декабря y2 года, т. е.

d = 31, m = 12, y = y2.

В противном случае выясняем, сколько полных невисокосных лет содержится в n4 днях и это количество добавляем к y2, получая тем самым новое количество полных лет. Корректируем и остаток n4, вычитая из него количество дней, из которых сформированы эти невисокосные года. Обозначим новое количество полных лет и новый остаток символами  y3 и n5. соответственно. Имеем:

y3 = y2 + [ n4 / 365],

n5 = n4 − [ n4 / 365] · 365.

Итак, первый этап завершён! Количество полных лет, прошедших с начала летоисчисления, равно y3, а количество оставшихся дней, не вошедших в эти года, равно n5.

Если n5 = 0, то сразу же, по окончании первого этапа, можно сказать, что текущим годом будет y3, а текущим днём — 365-й день в году, т.е. 30-го декабря, если год y3 високосный, или 31-го декабря в противном случае. Иными словами,

Если год y3 — високосный, то d = 30, m = 12, y = y3.

Если год y3 — невисокосный, то d = 31, m = 12, y = y3.

Если же n5 ≠ 0, то переходим ко второму этапу.

Второй этап состоит в разбиении остатка n5 на дни, образующие полные месяцы, и дни, входящие в "новый" остаток с последующим получением номера текущего месяца и текущего числа месяца. Отметим, что к началу второго этапа текущий год можно считать уже установленным: очевидно, y = y3 + 1.

Для начала найдём h — количество полных месяцев, содержащихся в n5 днях, в предположении, что текущий год — невисокосный. Как несложно заметить, h равно наибольшему значению индекса i, при котором выполняется неравенство rin5, а "новый" остаток n6 находится по формуле

n6 = n5rh.

Продолжая считать текущий год невисокосным, найдём текущие число и номер месяца, которые мы обозначим символами d1 и m1 соответственно. Если n6 ≠ 0, то всё просто:

d1 = n6, m1 = h + 1.

А если n6 = 0, то h будет номером текущего месяца, а текущее число месяца будет последним числом месяца с номером h:

d1 = rhrh − 1, m1 = h.

Если текущий год действительно невисокосный, или високосный, но m1 < 3, то задачу можно считать решённой: d1 и m1 и будут текущими числом и номером месяца соответственно, т. е. получаем:

d = d1, m = m1, y = y3 + 1.

А если нет, т. е., если текущий год високосный и, при этом, m1 ≥ 3, то следует "сдвинуть" дату на 1 день назад.

Проще всего сделать это в случае, когда d1 ≠ 1:

d = d1 − 1, m = m1, y = y3 + 1.

А если d1 = 1, то текущим месяцем будет m1 − 1, т. е. h, а текущим числом месяца — последнее число месяца с номером h. Отдельно, при этом, нужно рассмотреть случай, когда h = 2. В итоге, получаем:

Если h = 2, то d = 29, m = 2, y = y3 + 1.

Если h ≠ 2, то d = rhrh − 1, m = h, y = y3 + 1.

Ну что ж, с алгоритмом разобрались, теперь можно переходить к программированию.

Преобразование номера дня в дату — функция day2date()

Число 1461 будет неоднократно использоваться в коде, поэтому давайте, для начала, создадим равную этому числу глобальную константу:

const int F = 1461;

Функция day2date() должна генерировать дату, т. е. сразу 3 значения: число месяца, номер месяца, год. Для того, чтобы функция смогла возвращать даты, опишем новый тип date, предназначенный для хранения дат:

typedef struct
{
    int d;
    int m;
    int y;
}
date;

А вот и код самой функции day2date():

 1.date day2date(int n)
 2.{
 3.    int m, y = 0, k = 0;
 4.    while (n >= F)
 5.    {
 6.        y += n / F * 4;
 7.        n %= F;
 8.        int l = y / 100 - y / 400, t = l - k;
 9.        if (t)
10.        {
11.            n += t;
12.            k = l;
13.        }
14.    }
15.    if (!n)
16.        return (date) {31, 12, y};
17.    y += n / 365;
18.    n %= 365;
19.    if (!n)
20.    {
21.        int p = y % 4, q = y % 100, r = y % 400;
22.        return (date) {!p && (q || !r) ? 30 : 31, 12, y};
23.    }
24.    y++;
25.    for (m = 1; n >= days[m] && m <= 12; m++)
26.        ;
27.    n -= days[m - 1];
28.    if (!n)
29.    {
30.        m--;
31.        n = days[m] - days[m - 1];
32.    }
33.    int p = y % 4, q = y % 100, r = y % 400;
34.    if (!p && (q || !r) && m >= 3 && !--n)
35.        n = (--m == 2) ? 29 : days[m] - days[m - 1];
36.    return (date) {n, m, y};
37.}

Прокомментируем его коротко. Функция принимает в качестве аргумента номер дня и возвращает соответствующую ему дату в виде значения типа date.

Число месяца, номер месяца и год будут формироваться в переменных n, m и y соответственно (см. стр. 1, 3). Переменная k (стр. 3) предназначена для "накопления" в ней количества високосных лет, кратных 100, но не кратных 400, среди тех полных лет, которые накапливаются, в свою очередь, в y. Обратите внимание на то, что начальные значения y и k — нулевые.

Пока "остаток", хранящийся в n, больше или равен количеству дней в четвёрках (стр. 4), выполняем следующие действия. Увеличиваем y на число "четвёрок", содержащихся в n днях (стр. 6) и уменьшаем n на количество дней, содержащихся в этих "четвёрках" (стр. 7). Выясняем, не увеличилось ли после модернизации переменной y количество "неучтённых" високосных лет, содержащихся в y; если "да", то увеличиваем n на количество "неучтённых" високосных лет и изменяем значение k (см. стр. 8-13).

Отметим, что при начальном значении переменной n, не превышающем 106, количество итераций цикла while не превысит 2.

По окончании цикла все "четвёрки" уже содержатся в y, а в n хранится число оставшихся дней, не содержащихся в этих четвёрках. Если значение n равно 0, то искомая дата — последнее число года, содержащегося в y, т. е. 31-е декабря. В этом случае возвращаем найденную дату из функции (см. стр. 15, 16).

Если же значение n отлично от 0, то увеличиваем y на количество полных невисокосных лет, содержащихся в n днях, и уменьшаем само n на количество дней, содержащихся в этих годах (стр. 17, 18).

Если значение n равно 0, то искомая дата — 365-й день года, содержащегося в y, т. е. 31-е декабря, если год високосный, или 30-го декабря, если нет. В этом случае возвращаем найденную дату из функции (см. стр. 19-23).

Итак, первый этап реализован. Увеличиваем значение переменной y на единицу; теперь в y хранится текущий год (стр. 24). Остаётся лишь извлечь из переменной n число и номер месяца.

С помощью цикла for находим количество полных месяцев, прошедших с начала года, в предположении, что текущий год — невисокосный, и помещаем это количество, увеличенное на единицу, в m (стр. 25-26). Уменьшаем значение n на число дней, содержащихся в этих месяцах (стр. 27). Если значение n отлично от 0, то теперь в m содержится текущий месяц, а в n — текущее число месяца (при условии, что текущий год — невисокосный).

Ну, а если значение n равно 0, то "сдвигаем" дату на один день назад, уменьшая значение m на единицу и присваивая n последнее число месяца с номером, содержащимся в m, считая, что это месяц невисокосного года (стр. 28-32).

Итак, если текущий год — невискосный, или високосный, но значение m меньше 3, то текущие число и номер месяца содержатся в переменных n и m соответственно.

А если год високосный, и, при этом, значение m больше или равно 3, то делаем сдвиг на один день назад, либо, в случае n != 1, просто уменьшая n на единицу, либо, в противном случае, уменьшая на единицу m и присваивая n последнее число месяца с номером m, учитывая, что в феврале високосного года 29 дней (стр. 33-35).

Ну что ж, теперь в переменных n, m и y содержатся искомые текущие число месяца, номер месяца и год соответственно. Остаётся лишь сформировать из них дату, т. е. объект типа date и вернуть его из функции (стр. 36).

Примеры использования функции day2date()

Ближайшее круглое число дней, которое должно исполниться с начала летоисчисления — это 750000. Давайте выясним, когда именно это произойдёт. Для этого построим следующую версию функции main():

int main()
{
    const int D = 750000;
    date dt = day2date(D);
    printf("День номер %d выпадает на %02d.%02d.%04d", D, dt.d, dt.m, dt.y);
    return 0;
}

Компиляция и запуск программы приводит к следующему консольному выводу:

День номер 750000 выпадает на 06.06.2054

Думаю, что многие мои читатели смогут дожить до 6-го июня 2054-го года и отпраздновать 750000-й день с начала летоисчисления.

Следующая программа позволит "сдвинуть" произвольную дату на любое количество дней назад или вперёд. Соответствующая версия функции main() имеет вид:

int main()
{
    int d, m, y, n;
    printf("Введите начальную дату (число, номер месяца и год, разделённые точками)\n");
    scanf("%d.%d.%d", &d, &m, &y);
    printf("Введите сдвиг по дате в днях\n");
    scanf("%d", &n);
    date dt = day2date(date2day(d, m, y) + n);
    printf("Конечная дата: %02d.%02d.%04d", dt.d, dt.m, dt.y);
    return 0;
}

Предположим, что мы хотим узнать, когда человеку, родившемуся 7-го июля 1997-го года, исполнится 10000 дней. Для этого компилируем программу, запускаем её и вступаем с ней в следующий диалог:

Введите начальную дату (число, номер месяца и год, разделённые точками)
07.07.1997
Введите сдвиг по дате в днях
10000
Конечная дата: 22.11.2024

Итак, 10000 дней этому человеку исполнится 22-го ноября 2024-го года.

Для "сдвига" даты назад, нужно указать отрицательное число дней. Давайте решим задачу, обратную предыдущей: выясним, когда должен был родиться человек, которому 22-го ноября 2024-го года исполнится 10000 дней. Приходим к следующему диалогу с программой:

Введите начальную дату (число, номер месяца и год, разделённые точками)
22.11.2024
Введите сдвиг по дате в днях
-10000
Конечная дата: 07.07.1997

Получили ту дату, которую ожидали, так что программа сработала корректно.

Заключение

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

По ссылке ниже можно скачать исходный код в виде одного файла. В нём содержатся все 6 вариантов функции main(), рассмотренных в статье, 5 из которых закомментированы.

Скачать исходный код