Nickolay.info. Тексты. Строим вечный календарь

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

Функции определения дня недели, входящие в ядро Windows и доступные из любой среды программирования, отпадают сразу же из-за ограниченности времени их работы (так, стандартный календарь в Windows ограничен диапазоном лет от 1980 до 2099 включительно). Не лучше обстоят дела и в интерпретируемых языках, таких как Java и JavaScript.

Между тем, существующий григорианский календарь при условии отсутствия корректировок, накопит погрешность в 1 сутки по отношению к продолжительности тропического года лишь в 4317 г. [1]. Насколько я помню, корректировки эталонных часов периодически проводятся и в СМИ не раз объявляли, что "этот год будет на секунду длиннее" (короче). Поэтому есть смысл написать приложение, собственными средствами рассчитывающее по заданной дате день недели. На основе анализа литературы [1, 2] можно выделить 2 алгоритмических подхода к определению дня недели.

Первый подход - это арифметические формулы, наиболее известной из которых является формула Зеллера (en.wikipedia.org/wiki/Zeller's_congruence):

dday= ([(m+1)*26/10]+d+y+[y/4]+[c/4]-2*c)%7

Здесь [] - обозначение целой части числа, % - взятие остатка от деления, D- день месяца, M - номер месяца, Y - год столетия (0-99), C - номер столетия (20 для 2008 г.), dday - полученный день недели (0-Сб, 1-Вс, 2-Пн, 3-Вт, 4-Ср, 5-Чт, 6-Пт). При этом, в оригинальном алгоритме январь и февраль считаются как месяцы 13 и 14 предыдущего года, то есть, в расчете нужно сделать что-то вроде

Если Месяц<3 То Нач
 Добавить к месяцу 12; Уменьшить год на 1;
Кон

Формулу можно встретить в самых разных модификациях, например, я одно время считал правильной такую:

Если m<3 То m=m+10
Иначе m=m-2 
Если m>10 То y=y-1
nday=[2.6*m - 0.2]+d+y+[y/4]+[c/4] - 2*c
dday = (nday + 777)%7

Модификации формулы Зеллера обычно отличаются между собой тем, на каких промежутках времени они работают корректно, а на каких "врут" на плюс или минус сколько-то дней.

В общем и целом, недостаток подхода состоит в том, что он требует операций с вещественными числами и потенциально не точен для больших промежутков времени. Однако, для 4-значных лет точность его вполне приемлема. Кроме того, операций с вещественными числами можно избежать. В покойном ныне ресурсе [3] была предложена, например, следующая модификация формулы Зеллера:

if (mn < 3) { mn += 12 ; yr -= 1 ; } 
int n1 = (26 * (mn + 1)) / 10 ; 
int n2 = (int) ((125 * (long) yr) / 100) ; 
day = ((date + n1 + n2 - (yr / 100) + (yr / 400) -1) % 7) ; 

Здесь date, mn, yr - день, месяц и год (обратите внимание, что год в этой формуле - четырехзначный), возвращаемая величина day находится в диапазоне от 0 (Вс) до 6 (Сб). В приведенном коде на Си используется тот факт, что деление целых дает в этом языке целое.

Другой подход основан на том факте, что григорианский календарь выполняет "полный цикл" за 400 лет (с учетом того, что последние годы столетия раз в 400 лет бывают високосными - 1600, 2000, 2400, ...). Поэтому, взяв любой из таких лет за точку отсчета, затем вычисляя номер года в 400-летнем цикле и имея таблицу, связывающую, например, номер года в цикле и день недели для 1 января этого года, легко вычислить день недели для любой даты, не используя операций с вещественными числами. Этот подход предложен в [1]. После некоторых адаптации и упрощения, сделанных мной (в частности, исключения отдельного расчета для дат по старому стилю), получаем следующий алгоритм:

char var[]=
 {0x01,0x2a,0x56,0x08,0x34,0x5d,0x12,0x3b,0x60, 
  0x19,0x45,0x67,0x23,0x4c,0x04,0x02,0x00,0x0c}; 
//Таблица вариантов для 1 дня года
int y; //В переменной y должен быть полный год
int p; char byte;
int leap=0; //Признак лет 100, 200, 300, 400 цикла
y%=400; if (y==0) y=400; // Год от 1 до 400 по полному циклу
p=y/100;     // Столетие от 0 до 3
if (! ((y+100)%100) ) { p--; leap=1; } // Поправка для лет 100,200,300,400
int ind=(y-p*100)%28+p*4; // Индекс в таблице вариантов с учетом смещения
if (ind>28) ind-=28; ind--; // Поправка цикличности
if (leap) byte=var[14+p]; //Годы 100,200,300,400
else {
  byte=var[ind/2];
  if ((ind+2)%2) byte&=0x0f; // Годы 2,4,6...,98
  else byte>>=4;  // Годы 1,3,5...,99
 } // В byte - номер варианта по таблице 400-годичного цикла
int num=((((byte+0x07)%0x07)+0x01)&0x07);
//num - День начала года, приведенный к схеме 1-Пн, :., 7-Вс

Код сильно "заоптимизирован", так как написан в те времена, когда мне нравились подобные вещи. Данный подход заметно более сложен, кроме того, он хорош для вывода годичного календаря, так как определяет день недели для 1 января, а для других месяцев требуются дополнительные вычисления. Тем не менее, вот ранее не публиковавшаяся программа на С++ для DOS с исходником, реализующая этот подход: скачать calendar.zip (9 Кб). Возможно, в ней приведённый код выглядит чуть иначе, писалось давно, не помню :)

Литература
1. Хренов Л.С., Голуб И.Я. Время и календарь. М.: "Наука", 1989
2. Форсайт Р. Паскаль для всех. М,: "Машиностроение", 1986
3. Мир программирования - ресурсы для думающих людей (src.fitkursk.ru)

См. также:
calend.zip - вечный календарь на C++ Builder, реализует формулу Зеллера в целых числах;
Календари в разделе JavaScript

Рейтинг@Mail.ru

вверх гостевая; E-mail