Приглашаем посетить
Ломоносов (lomonosov.niv.ru)

Дата и время

Назад
Глава 3 Дата и время
Вперед

Дата и время

Введение

Время и дата - очень важные величины, и с ними необходимо уметь работать. "Сколько пользователей регистрировалось за последний месяц?", "Сколько секунд я должен проспать, чтобы проснуться к полудню?" и "Не истек ли срок действия пароля данного пользователя?" - вопросы кажутся тривиальными, однако ответ на них потребует на удивление нетривиальных операций. В Peri моменты времени представлены в виде интервалов, измеряемых в секундах с некоторого момента, называемого началом эпохи. В UNIX и многих других системах начало эпохи соответствует 00 часов 00 минут 1 января 1970 года по Гринвичу (GMT1). На Macintosh дата и время измеряется в местном часовом поясе. Функция gmtime возвращает правильное время по Гринвичу, основанное на смещении местного часового пояса. Помните об этом, рассматривая рецепты этой главы. На Macintosh количество секунд с начала эпохи позволяет отсчитывать время в интервале от 00:00 1 января 1904 года до 06:28:15 6 февраля 2040 года. Говоря о времени и датах, мы часто путаем две разные концепции: момент времени (дата, время) и интервал между двумя моментами (недели, дни, месяцы и т. д.). При отсчете секунд с начала эпохи интервалы и моменты представляются в одинаковых единицах, поэтому с ними можно выполнять простейшие математические операции. Однако люди не привыкли измерять время в секундах с начала эпохи. Мы предпочитаем работать с конкретным годом, месяцем, днем, часом, минутой и секундой. Более того, название месяца может быть как полным, так и сокращенным. Число может указываться как перед месяцем, так и после него. Использование разных форматов затрудняет вычисления, поэтому введенная пользователем или В наши дни время по Гринвичу также часто обозначается сокращением UTC (Universal Coordinated Time). прочитанная из списка строка даты/времени обычно преобразуется в количество секунд с начала эпохи, с ней производятся необходимые операции, после чего секунды снова преобразуются для вывода. Для удобства вычислении количество секунд с начала эпохи всегда измеряется по Гринвичу. В любых преобразованиях всегда необходимо учитывать, представлено ли время по Гринвичу или в местном часовом поясе. Различные функции преобразования позволяют перейти от гринвичского времени в местное, и наоборот. Функция Peri time возвращает количество секунд, прошедших с начала эпохи... более или менее' точно. Для преобразования секунд с начала эпохи в конкретные дни, месяцы, годы, часы, минуты и секунды используются функции localtime и gmtime. В списковом контексте эти функции возвращают список, состоящий из девяти элементов.
Переменная Значение Интервал
$sec Секунды 0-60
$min Минуты 0-59
$hours Часы 0-23
$mday День месяца 1-31
$month Месяц 0-11,0 == январь
$уеаг Год, начиная с 1900 1-138 (и более)
$wday День недели 0-6, 0 == воскресенье
$yday День года 1-366
$isdst________0 или 1___________true, если действует летнее время

Секунды изменяются в интервале 0-60 с учетом возможных корректировок; под влиянием стандартов в любой момент может возникнуть лишняя секунда. В дальнейшем совокупность "день/месяц/год/час/минута/секунда" будет обозначаться выражением "полное время" - хотя бы потому, что писать каждый раз "отдельные значения дня, месяца, года, часа, минут и секунд" довольно утомительно. Сокращение не связано с конкретным порядком возвращаемых значений. Peri не возвращает данные о годе в виде числа из двух цифр. Он возвращает разность между текущим годом и 1900, которая до 1999 года представляет собой число из двух цифр. У Peri нет своей "проблемы 2000 года", если только вы не изобретете ее сами (впрочем, у вашего компьютера и Peri может возникнуть проблема 2038 года, если к тому времени еще будет использоваться 32-разрядная адресация). Для получения полного значения года прибавьте к его представлению 1900. Не пользуйтесь конструкцией "19$уеаг", или вскоре ваши программы начнут выдавать "год 19102". Мы не можем точно зафиксировать интервал года, потому что все зависит от размера целого числа, используемого вашей системой для представления секунд с начала эпохи. Малые числа дают небольшой интервал; большие (64-разрядные) числа означают огромные интервалы. Скорее, менее. В момент написания книги функция возвращала на 21 секунду меньше. В соответствии со стандартом POSIX функция time не должна возвращать секунды, которые накапливаются из-за замедления вращения Земли, обусловленного воздействием приливов. За дополнительной информацией обращайтесь к разделу 3 sci. astro FAQno адресу http://astrosun.tn.cornell.edu/students/ lazio.sci.astro.S.FAQ. В скалярном контексте localtime и gmtime возвращают дату и время, отформатированные в виде ASCII-строкн:
Fri Apr 11 09:27:08 1997
Объекты стандартного модуля Time::tm позволяют обращаться к компонентам даты/времени по именам. Стандартные модули Time::locakime и Time::gmtime переопределяют функции localtime и gmtime, возвращающие списки, и заменяют их версиями, возвращающими объекты Time::tm. Сравните два следующих фрагмента:
и Массив

print "Today is day ", (localtime()[7], " of the current year.\n";
Today is day 117 of the current year.
# Объекты Time::tm $tm = localtime;
print "Today is day ", $tm->yday, " of the current year.\n";
Today is day 117 of the current year.


Чтобы преобразовать список в количество секунд с начала эпохи, воспользуйтесь стандартным модулем Time::Local. В нем имеются функции timelocal и timegm, которые получают список из девяти элементов и возвращают целое число. Элементы списка и интервалы допустимых значений совпадают с теми, которые возвращаются функциями localtime и gettime. Количество секунд с начала эпохи ограничивается размером целого числа. Беззнаковое 32-разрядное целое позволяет представить время по Гринвичу от 20:45:52 13 декабря 1901 года до 03:14:07 19 января 2038 года включительно. Предполагается, что к 2038 году в компьютерах должны использоваться целые числа большей разрядности. Во всяком случае, будем надеяться на это. Чтобы работать с временем за пределами этого интервала, вам придется воспользоваться другим представлением или выполнять операции со отдельными значениями года, месяца и числа. Модули Date::Calc и Date::Manip с CPAN работают с этими отдельными значениями, но учтите - они не всегда вычитают из года 1900, как это делает localtime, а нумерация месяцев и недель в них не всегда начинается с 0. Как всегда, в страницах руководства можно найти достоверные сведения о том, какая информация передается модулю, а какая - возвращается им. Только представьте, как будет неприятно, если рассчитанные вами финансовые показатели уйдут на 1900 лет в прошлое!

3.1. Определение текущей даты

Проблема

Требуется определить год, месяц и число для текущей даты.

Решение

Воспользуйтесь функцией localtime. Без аргументов она возвращает текущую дату и время. Вы можете вызвать localtime и извлечь необходимую информацию из полученного списка:
($DAY, $MONTH, $YEAR) = (localtime)[3,4,5]; Модуль timc::localtimc переопределяет localtime так, чтобы функция возвращала объект time::tm:

use Time::localtime;
$tm = localtime;
($DAY, $MONTH, $YEAR) = ($tm->mday, $tm->mon, $tm->year);

Комментарий

Вывод текущей даты в формате ГПТ-ММ-ДД с использованием стандартной функции localtime выполняется следующим образом:
($day, $month, $year) = (localtime)[3,4,5];
printf("The current date is %04d %02d %02\n", $year+1900, $month+1, $day):
The current date is 1999 04 28


Нужные ноля из списка, возвращаемого localtime, извлекаются с помощью среза. Запись могла выглядеть иначе:
($day, $month, $year) = (localtime)[3..5];

А вот как текущая дата выводится в (формате ГПТ-ММ-ДД (рекомендованном стандартом ISO 8601) с использованием Time::localtime:

use Time: : localtline;
$tm = localtime;
printf("The current date is %04d-%02d-%02\n", $tm->year+1900, ($tm->mon)+1, $tm->mday);
The current date is 1999-04-28


В короткой программе объектный интерфейс выглядит неуместно. Однако при большом объеме вычислений с отдельными компонентами даты обращения по имени заметно упрощают чтение программы. То же самое можно сделать и хитроумным способом, не требующим создания временных переменных:
printf("The current date is %04d-%02d-%02\n",
sub {($_[5]+1900, $_[4]+1, $_[3])}->(localtime));


Кроме того, в модуле POSIX имеется функция strftime, упоминаемая в рецепте 3.8:

use POSIX qw(strftime):
print strftime "%Y-%m-%d\n", localtime:


Функция gmtime работает аналогично localtime, но возвращает время но Гринвичу, а не для местного часового пояса.

Смотри также: Описание функций localtime и gmtime в perlfunc(1); документация по стандартному модулю Time::locakime.

3.2. Преобразование полного времени в секунды с начала эпохи

Проблема

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

Решение

Воспользуйтесь функцией timelocal или timegm стандартного модуля Time::Local. Выбор зависит от того, относится ли дата/время к текущему часовому поясу или Гринвичскому меридиану:

use Time::Local;
$TIME = timelocal($sec, $min, $hours, $mday, $rnon, $year);
$TIME = timegm($sec, $min, $hours, $mday, $mon, $year);

Комментарий

Встроенная функция localtime преобразует количество секунд с начала эпохи в компоненты полного времени; процедура timelocal из стандартного модуля Time::Local преобразует компоненты полного времени в секунды. Следующий пример показывает, как определяется количество секунд с начала эпохи для текущей даты. Значения дня, месяца и года получаются от localtime:
# $hours, $niinntes и $seconds задают время для текущей даты U и текущего часового пояса use Time::Local:
$time = timelocal($seconds, $mlnutes, $hours, (localtime)[3.4,5]);

Если функции timelocal передаются месяц н год, они должны принадлежать тем же интервалам, что и значения, возвращаемые localtime. А именно, нумерация месяцев начинается с 0, а из года вычитается 1900. Функция timelocal предполагает, что компоненты полного времени соответствуют текущему часовому поясу. Модуль Time::Local также экспортирует процедуру timegm, для которой компоненты полного времени задаются для Гринвичского меридиана. К сожалению, удобных средств для работы с другими часовыми поясами, кроме текущего или Гринвичского, не существует. Лучшее, что можно сделать, - преобразовать время к Гринвичскому и вычесть или прибавить смещение часового пояса в секундах. В следующем фрагменте демонстрируется как применение timegm, так и настройка интервалов года и месяца:

# $day - день месяца (1-31)
# $month - месяц (1-12)
# $уеаг - год, состоящий из четырех цифр (например, 1999)
# $hours, $minutes и $seconds - компоненты времени по Гринвичу

use Time::Local;
$time = timegm($seconds, $nnnutes, $hours, $day, $month-1, $year-1900);

Как было показано во введении, количество секунд с начала эпохи не может выходить за пределы интервала от 20:45:52 13 декабря 1901 года до 03:14:07 19 января 2038 года включительно. Не преобразуйте такие даты - либо воспользуйтесь модулем Date:: с СРАМ, либо выполняйте вычисления вручную.

Смотри также: Документация по стандартному модулю Time::Local. Обратное преобразование рассматривается в рецепте 3.3.

3.3. Преобразование секунд с начала эпохи в полное время

Проблема

Требуется преобразовать количество секунд с начала эпохи в отдельные значения дня, месяца, года и т. д.

Решение

Воспользуйтесь функцией localtime или gmtime в зависимости от того, хотите ли вы получить дату/время для текущего часового пояса или для Гринвичского меридиана.
($seconds, Sminutes, $hours, $day_of_month, $year, $wday, $yday, $isdst) = localtime($time);
Стандартные модули Time::timelocal н Tirne::gmtirne переопределяют функции localtime и gmtime так, чтобы к компонентам можно было обращаться по именам:
use Time::localtlme; # или
Time::gmtime $tm = localtime($time); # или
gmtime($TIME) $seconds = $tm->sec;
"..."

Комментарий

Функции localtime и gettime возвращают несколько странную информацию о годе и месяце; из года вычитается 1900, а нумерация месяцев начинается с 0 (январь). Не забудьте исправить полученные величины, как это делается в следующем примере:
($seconds, $minutes. $hours, $day_of_month, $month, $year,
$wday, $yday, $isdst) = localtime($time);
printf("Dateline: %02d:%02d:%02d-%04d/%02d/%02d\n",
$hours, $minutes, $seconds, $year+1900, $month+1,
$day_of_month);

Модуль Time::localtime позволяет избавиться от временных переменных:
use Time::localtlme;
$tm = localtime($time);
printf("Dateline: %02d:%02d:%02d-%04d/%02d/%02d\n" $tm->hour, $tm->min, $tm->sec, $tm->year+1900, $tm->mon+1, $tm->mday):


Смотри также: Описание функции localtime в perlfunc(1) документация по стандартным модулям Time::localtime и Tiine::gmtiine. Обратное преобразование рассматривается в рецепте 3.2.

3.4. Операции сложения и вычитания для дат

Проблема

Имеется значение даты/времени. Требуется определить дату/время, отделенную от них некоторым промежутком в прошлом или будущем.

Решение

Проблема решается простым сложением или вычитанием секунд с начала эпохи:
$when = $now + $difference;
$then = $now - sdifference;
Если у вас имеются отдельные компоненты полного времени, воспользуйтесь модулем Date::Calc с CPAN. Если вычисления выполняются только с целыми днями, примените функцию Add_Delta_Days (смещение $offset может представлять собой как положительное, так и отрицательное целое количество дней):
use Date::Calc qw(Add_Delta_Days);
($у2, $m2, $d2) = add_delta_days($y, $m, $d, $offset);
Если и вычислениях используются часы, минуты и секунды (то есть не только дата, но и время), воспользуйтесь функцией Add_Delta_DHMS:
use Date::DateCalc qw(Add_Delta_DHMS);
($year2, $month2, $day2, $h2, $m2, $s2) ,=
Add_Delta_DHMS( $year, $month,o$day, $hour, $minute, $seconds, $days_offset, $hour_offset, $minute_offset, $seconds_offset );

Комментарий

Вычисления с секундами от начала эпохи выполняются проще всего (если не считать усилий па преобразования даты/времени в секунды и обратно). В следующем фрагменте показано, как прибавить смещение (в данном примере - 55 дней, 2 часа, 17 минут и 5 секунд) к заданной базовой дате и времени:
$birthtime = 96176750; # 18 января 1973 года, 03:45:50
$interval = 5 + # 5 секунд
17 * 60 + # 17 минут '
2 * 60 * 60 + # 2 часа
55 * 60 * 60 * 24; # и 55 дней
$fhen = $birthtime + $interval;
print "Then is ", scalar(localtime($then)), "\n";
Then is Wed Mar 14 06:02:55 1973 Мы также могли воспользоваться функцией Add_Delta_DHMS и обойтись без преобразований к секундам с начала эпохи и обратно:
use Date::Calc qw(Add_Delta_DHMS):
($year, $month, $day, $hh, $mm, $ss) = add_delta_dhms(
1973, 1, 18, 3, 45, 50, # 18 января 1973 года, 03:45:50
55, 2, 17, 5); # 55 дней, 2 часа, 17 минут, 5 секунд
print "To be prcise: $hh:$miTi:$ss, $month/$day/$year\n";
To be precise: 6:2:55, 3/14/1973

Как обычно, необходимо проследить, чтобы аргументы функции находились в правильных интервалах. Add_Delta_DHMS получает полное значение года (без вычитания 1900). Нумерация месяцев начинается с 1, а не с 0. Аналогичные параметры передаются и функции Add_Delta_Days модуля Date::DateCalc:
use Date::DateCalc qw(Add_Delta_Days);
($year, $month, $day) = add_delta_days( 1973, 1, 18, 55);
print "Nat was 55 days old on: $month/$day/$year\n":
Nat was 55 days old on: 3/14/1973


Смотри также: Документация по модулю Date::Calc от CPAN.

3.5. Вычисление разности между датами

Проблема

Требуется определить количество дней между двумя датами или моментами времени.

Решение

Если даты представлены в виде секунд с начала эпохи и принадлежат интервалу от 20:45:52 13 декабря 1901 года до 03:14:07 19 января 2038 года включительно, достаточно вычесть одну дату из другой и преобразовать полученные секунды в дни: $seconds = $recent = $earlier: Если вы работаете с отдельными компонентами полного времени или беспокоитесь об ограничениях интервалов для секунд с начала эпохи, воспользуйтесь модулем date::calc с cpan. on позволяет вычислять разность дат:
use Date::Calc qw(Delta_DHMS);
($days, $hours, $minutes, $seconds) = delta_dhms( $year1, $month1, $day1, $hour1, $minute1, $seconds1, # Ранний # момент
$year2, $month2, $day2, $hour2, $minute2, $seconds2, # Поздний # момент

Комментарий

Одна из проблем, связанных с секундами с начала эпохи, - преобразование больших целых чисел в форму, понятную для человека. Следующим пример демонстрирует один из способов преобразования секунд с начала эпохи в привычные недели, дни, часы, минуты и секунды:
$bree = 361535725; # 04:35:25 16 июня 1981 года
$nat = 96201950; # 03:45:50 18 января 1973 года
$difference = $bree - $nat;
print "There were $difference seconds between Nat and Bree\n";
There were 266802575 seconds between Nat and Bree
$seconds = $difference % 60;
$difference = ($difference - $seconds) / 60;
$minutes = $difference % 60;
$difference = ($difference - $minutes) / 60;
$hours = $difference $ 24;
$diff'erence = ($difference - $hours) / 24;
$days = $difference % 7;
$weeks = ($difference - $days) / 7;

print "($weeks weeks, $days days, $hours:$minutes:$seconds)\n";
(441 weeks, 0 days, 23: 49: 35) Функции модуля Date::Calc упрощают подобные вычисления. Delta_Days возвращает количество дней между двумя датами. Даты передаются ei'i в виде списка "год/месяц/день" в хронологическом порядке, то есть начиная с более ранней.

Смотри также: Документация по стандартному модулю Dale::Calc с CPAN.


Назад
Вперед