Дата и время

1999, 2007, 30.11.2017

Проблема 2000 года вызвала интерес к представлению времени и даты в компьютерах, компьютерных программах и операционных системах. Но только ли 2000 год может вызвать сбои? К сожалению нет. Дата и время, в современных операционных системах и компьютерах, часто представляются как число секунд, прошедших с 00:00 1 января 1970 года. Если дату, в этом формате, хранить как двойное слово (32 разряда) со знаком, то проблемы начнутся через 67,5 лет, то есть в 2038 году. Не так долго ждать осталось, правда? Как же лучше хранить дату и время в программах, и как с ними работать?

Казалось бы, решение очевидно. Хранить дату в виде трех чисел: день, месяц, год. Если обеспечить хранение четырех цифр года, то неприятности откладываются до конца 9999 года. Это и в самом деле не плохое решение, если Вам надо вводить, хранить и выводить дату. А если надо узнать, сколько дней между 20 марта 1998 года и 1 февраля 1975? Или, какое число какого года будет, если с 1 января 1999 года пройдет 567 дней? Напомним, что по правилам григорианского календаря, того самого, которым мы сейчас пользуемся, в феврале может быть 28 дней (обычный год) или 29 (високосный год) дней. Високосным считается год, порядковый номер которого делится на 4. При этом, вековые годы, число сотен которых не делится на 4 без остатка, считаются простыми, например 1900. 2000 год будет високосным. Сможете написать алгоритм нахождения числа дней, прошедших между двумя датами? Задачи такого рода часто возникают в банковском деле, бухгалтерии, археологии, истории и многих других областях. Для облегчения таких вычислений дата и хранится в виде одного числа, можно просто складывать и вычитать, результат будет правильным.

Буду надеяться, что в преимуществах хранения даты как одного числа Вы убедились. Остается определиться с точкой отсчета и правилами перевода даты в число и обратно. В астрономических и хронологических расчетах принято вести непрерывный счет дней с 1 января 4713 года до нашей эры. Это так называемый юлианский период. Его ввел в 1583 году французский ученый Жозеф Скалигер. За начало юлианской даты принимается средний полдень на нулевом (гринвичском) меридиане. При этом сутки отсчитываются от среднего гринвичского полудня, следующего за средней гринвичской полуночью, которой определяется начало рассматриваемой календарной даты. Сложно? Не пугайтесь, нам из этого определения нужна только дата начала юлианского периода. Номер юлианского дня JD на момент 0,5 января любого года R григорианского календаря можно определить по формуле:

Переход от григорианской даты к юлианской

К принимает значения 1, 1.75, 1.50, 1.25, если R високосный год или 1-й, 2-й и 3-й после ближайшего предшествующего високосного года. Все, теория на этом заканчивается. Начинается практика.

За основу возьмем алгоритм 199б из "Библиотеки алгоритмов". Выпускались в 60х - начале 80-х прошлого века такие сборники, издательством "Радио и связь". Вот текст оригинала программы, на ALGOL-60, преобразующей дату в номер дня юлианского периода

        procedure jday(d,m,y) result:(j);
        value d,m,y; integer d,m,y,j;
        begin
            integer c,ya;
            if m>2 then m:=m-3 else
                begin m:=m+9; y:=y-1; end;
             c:=y/100; ya:=y-100*c;
             j:=(146097*c)/4+(1461*ya)/4+(153*m+2)/5+d+1721119
        end jday;

А вот текст программы, выполняющей обратное преобразование

        procedure jdate(j) result:(d,m,y);
        value j; integer j,d,m,y;
        begin
            j:=j-1721119;
            y:=(4*j-1)/146097; d:=(4*y-1-146097*y)/4;
            j:=(4*d+3)/1461; d:=(4*d+7-1461*j)/4;
            m:=(5*d-3)/153 d:=(5*d+2-153*m)/5; y:=100*y+j
            if m<10 then m:=m+3 else
                begin m:=m-9; y:=y+1 end
         end jdate;

Язык ALGOL сейчас практически не применяется. Поэтому приведу исходные тексты тех же самых процедур на языке С. Разумеется, можно использовать любой язык, и Pascal, и Basic, и Assembler, и т.д.

        // Преобразование григорианской даты в порядковый номер
        // дня юлианского периода

        unsigned long DateToLong(int day, int month, int year) {
            if(month>2) {
                month-=3;
            } else {
                month+=9;
            year--;
            }
            long c=year/100l;
            long ya=year-100l*c;
            return (146097l*c)/4l+(1461l*ya)/4l+(153l*month+2l)/5l+day+1721119l;
        }

        // Преобразование порядкового номера дня юлианского периода
        // в григорианскую дату

        void LongToDate(unsigned long j, int& day, int& month, int& year) {
            unsigned long d,m,y;
            j-=1721119l;
            y=(4l*j-1l)/146097l;
            d=(4l*j-1l-146097l*y)/4l;
            j=(4l*d+3l)/1461l;
            d=(4l*d+7l-1461l*j)/4l;
            m=(5l*d-3l)/153l;
            d=(5l*d+2l-153l*m)/5l;
            y=100l*y+j;
            if (m<10) {
                m+=3;
            } else {
                m-=9; y++;
            }
            month=m; year=y; day=d;
        }

Немного прокоментирую эти фрагменты. Первое, что бросается в глаза, это буква l после чисел. Она обозначает, что транслятор будет рассматривать число как длинное. Причем не зависимо от того, 16 или 32 разрядная версия используется. Дело в том, что некоторые трансляторы не совсем корректно выполняют приведение разрядностей для промежуточных результатов. В результате, могут возникать трудно уловимые ошибки. Если Вы никогда не пишете программы для машин с разрядностью меньше 32 (например, для микроконтроллеров), то можно этот суффикс не указывать. Три локальные переменные в процедуре LongToDate нужны лишь для того, что бы типы ее параметров совпадали с типами параметров процедуры DateToLong, и не требовалось явного описания преобразования типов.

Для проверки правильности работы процедур приведу несколько дат и соответствующих им номеров дней юлианского периода:

1 января 1980 года 2444240
1 января 1981 года 2444606
26 марта 1986 года 2446516
1 ноября 1962 года 2437970
1 мая 1967 года 2439612

Теперь приведу пример использования этих процедур. Ответим на вопрос: какое число, какого месяца и какого года будет через 567 дней после 1 января 1999 года.

            . . . .

        unsigned long date;
        int day, month, year;

        date=DateToLong(1,1,1999);
        date+=567;
        LongToDate(date,&day,&month,&year);

            . . . .

Теперь коснусь представления времени. С этим все просто. Время будем хранить как число секунд, прошедших с полуночи. Если такой точности мало, можно хранить число миллисекунд, прошедших с полуночи, но это не принципиальное различие.

        // Пробразование времени суток в целое число

        unsigned long TimeToLong(int hour, int minute, int seconds) {
            return (hour*60l+minute)*60l+seconds;
        }

        // Преобразование целого числа во время суток

        void LongToTime(unsigned long t, int& hour, int& min, int& sec) {
            long h,m,s;
            s=t%60l; t/=60l;
            m=t%60l;
            h=t/60l;
            hour=h; min=m; sec=s;
        }

Здесь есть одна маленькая тонкость. При выполнении арифметических операций над временем, как целым числом, мы можем получить результат больший 24 часов. Это может быть полезным, например, если дата и время являются членами одного класса, можно корректировать дату. Если же такое поведение недопустимо, то перед преобразованием времени из целого числа, нужно взять остаток от деления этого числа на 86400. Этот остаток будет временем суток, гарантированно меньшим 24 часов, Приведу пример:

            . . . .

        unsigned long date;
        unsigned long time;
        int day, month, year;
        int hour, min, sec;
    
        date=DateToLong(15,5,1994);
        time=TimeToLong(23,55,10);
        time+=1500;
        date+=time/86400l;
        time%=86400l;
        LongToDate(date,&day,&month,&year);
        LongToTime(time,&hour,&min,&sec);

            . . . .

Вот собственно и все. На основании приведенных описаний и фрагментов программ Вы можете построить свои классы даты и времени, или один общий класс, который будет содержать в себе дату и время. Можно использовать эти процедуры и вне классов, на любом языке программирования. На каком варианте остановиться решать Вам. Позволю себе только маленькую подсказку. Нельзя складывать две даты или два времени. Можно только дату, или время, и временной интервал. При вычитании дат, или времен получается временной интервал. Это семантически важно! Кстати, если вы заглянете на реализацию классов даты и времени в библиотеке MFC, то увидите, что там есть классы интервалов.