Next Previous Contents

Unix Review Column 25 -- Получение даты с помощью Perl

Randal Schwartz

Апрель 1999

Перевод Anton Petrusevich <casus@mail.ru> и Alex Ott <ott@phtd.tpu.edu.ru>

И компьютерная литература и популярная пресса наполнены множеством статей о ``конце тысячелетия'' и ``Y2K'', некоторые из которых являются позитивными, а некоторые пугающими. Много людей спрашивают ``готов ли Perl к Y2K?'' Хорошо, вот то что говорит об этом документация на Perl:

Имеет ли Perl проблему 2000 года? Готов ли Perl к Y2K?

Короткий ответ: Нет, Perl не имеет проблемы 2000-го года. Да, Perl готов к Y2K. Программистов наняли для его использования, однако вы вероятно не используете его.

Длинный ответ: Perl готов к Y2K также как и ваш карандаш -- не больше и не меньше. Функции работы с временем и датами, входящие в библиотеку Perl (gmtime и localtime) предоставляют адекватную информацию для правильного определения года большего 2000 (2038 будет являться проблемой для 32-битных машин). Год, возвращаемый этими функциями при вызову в списочном контексте, равен текущему году минус 1900. Для годов между 1910 и 1999 это значение равно двухзначному десятичному числу. Для того, чтобы избежать проблемы 2000-года просто не используйте занчение года как двухзначное число. Оно не является таковым.

При использовании функций gmtime() и localtime() в строковом контексте, они возвращают строку времени, которая содержит полное значение года. Например: $timestamp = gmtime(1005613200) устанавливает $timestamp равной "Tue Nov 13 01:00:00 2001". Так что в этом случае нет никакой проблемы 2000-года.

Это не значит, что Perl не может быть использован для создания не готовых к Y2K программ. Он может быть использован. Но точно также, как и ваш карандаш. Это будет ошибкой пользователя, но не языка. С риском вспыхивания NRA: ``Perl не ломает Y2K, это делают люди''. Для более длинного объяснения смотрите http://language.perl.com/news/y2k.html.

Так, мы получили ответ на вопрос. Но поскольку мы говорим о датах, то давайте рассмотрим несколько вещей, которые Perl умеет делать с датами.

Во первых, имеется три встроенных оператора времени. Нет, ни один из них не дает вам подписку на журнал Time --- просто число (как беззнаковое 32-битное число) секунд с начала отсчета (1 Января 1970, 0000 GMT). В момент написания, оно достигло значения примерно 900 миллионов и скоро пересечет отметку миллиард. Когда точно? Мы можем использовать другую встроенную функцию Perl для перевода этого времени в что-то такое, что может понять человек:

        my $when = gmtime 1000000000;
        print "timestamp turns a billion at $when\n";

Выполнив этот код я получил:

        timestamp turns a billion at Sun Sep  9 01:46:40 2001

Ахх... так где-то в идее Артура Кларка о новом тысячелетии мы получим новую цифру в значении возвращаемом time. Я удивлюсь, что программы дадут сбой, когда это случится.

Та строка, которая возвращается функцией gmtime является временем по GMT. Если вы не живете в Лондоне или в Западной Африке, то вам будет удобней получать время соответственно вашей временной зоне. Это делается легко: просто используйте функцию localtime:

        my $now = localtime time;
        print "It is now $now\n";

И gmtime и localtime возвращают строку похожую на результат команды UNIX date, в тех случаях, когда они вызываются в скалярном контексте, так как я выполнил это. Вы можете также получить части даты, используя эти две функции в списочном контексте:

        my ($sec,$min,$hour,$mday,$mon,$year,
          $wday,$yday,$isdst) = localtime time;

И это даст вам доступ к частям даты. Заметьте, что $mon отсчитывается с 0 (вы наверное захотите добавить к ней 1), а $year является смещением с 1900 года. Да, она будет равен 100 в 2000 году, так что просто исправим это с помощью команды $year += 1900. Для дополнительной информации прочитайте perldoc -f localtime.

Для обратного преобразования есть другой способ, Perl поставляется со стандартным модулем Time::Local. (Документация доступна с помощью команды perldoc Time::Local, конечно если кто-то не испортил ваш дистрибутив). Например, для получения значения с начала отсчета для начала 2000 года в моей временной зоне, я могу использовать следующий код:

        use Time::Local;
        my $time = timelocal(0,0,0,1,0,100);
        my $string = localtime $time;
        print "the big ball falls at $time => $string\n";

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

        use Time::Local;
        my $time = timelocal(0,0,0,1,0,100);
        my $diff = $time - time;
        $diff /= 86400; # seconds to days
        if ($diff > 0) {
                printf "we've got %.2f days to go\n", $diff;
        } else {
                print "we made it!\n";
        }

Конечно, значение переменной $time не изменяется между запусками программы, так что лучше всего вычислить его один раз и записать в программу. Но это хорошо для демонстрационной программы. Для того, чтобы увидеть более тщательно разработанный обратный счетчик, посетите мою домашнюю страницу по адресу http://www.stonehenge.com/merlyn/.

Кроме встроенных функций и библиотек, также на CPAN существует достаточное количество пакетов для работы с временем и датами. Наиболее претенциозным из них является Date::Manip, который вы можете установить на свою систему (если он у вам еще не установлен) используя инструкции описанные в perldoc perlmodinstall. (Если у вас нет perlmodinstall, то вы должны обновить свою версию Perl до версии 5.005 или более поздней).

Как говорится в первом параграфе документации модуль Date::Manip:

Это набор подпрограмм, спроектированный для облегчения общих действий с датами и временем, Легко выполняются такие операции, как сравнение двух дат, вычисления диапазона между двумя датами или разбор дат записанных в национальном формате. С самого Date::Manip был нацелен на легкое выполнение ЛЮБЫХ операций с датами и временем, не обязательно быстро. Также, он ориентирован на выполнение тех операций, в каких терминах думаем мы (как люди), а не на операции выполняемые компьютерами. Также существуют другие модули, которые могут выполнять небольшое подмножество операций выполняемых Date::Manip более быстро, чем данный модуль, так что если скорость выполнения играет важную роль, то вы должны использовать что-то другое. Посмотрите на CPAN список модулей работы с датами и временем. Но я верю, что для получения гибких решений Date::Manip является лучшим выбором для вас.

Однако, как упомянуто в этом тексте, Date::Manip является достаточно тяжелым, только для загрузки занимая примерно пол секунды CPU на моей мощной машине. Вероятно это нормально для программ выполняющихся длительное время, но это очень много для коротко-живущих приложений CGI.

Для многих приложений, когда не достаточно встроенных функций, вы можете использовать достаточно быстрый модуль Date::Calc (который также можно найти на the CPAN). Это модуль загружается (у меня) примерно одну десятую секунды CPU, делая его удобным для применения в коротко-живущих программах CGI или других часто запускаемых утилитах.

В качестве примера возможностей Date::Calc, я решил эмулировать команду UNIX cal, которая может выдавать календарь на месяц или год на любое время от 1-го года до далекого будущего. К сожалению, поскольку Date::Calc не понимает Юлианский календарь (адаптированный в 1752 в США), то я ограничу свою программу самым недавним временем, но сохраняя тот же интерфейс командной строки.

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

    #!/usr/bin/perl -w
    use strict;

    use Date::Calc qw(
      Today Day_of_Week Days_in_Month
      Month_to_Text Day_of_Week_Abbreviation
    );

    if (@ARGV) {
      my $year = shift;
      if (@ARGV) {
        my $month = $year;
        my $year = shift;
        die "year $year must be > 1752"
          unless $year > 1752;
        &do_cal($year,$month);
      } else {
        die "year $year must be > 1752"
          unless $year > 1752;
        for my $month (1..12) {
          &do_cal($year,$month);
          print "\n" unless $month == 12;
        }
      }
    } else {
      my ($today_year, $today_month, $today_day)
        = Today;
      &do_cal($today_year, $today_month);
    }

    sub do_cal {
      my $cal_year = shift;
      my $cal_month = shift;

      my $month_name = Month_to_Text($cal_month);
      my $start_dow = Day_of_Week(
        $cal_year,$cal_month,1
      );
      my $end_day = Days_in_Month(
        $cal_year,$cal_month
      );

      my $curday = 1 - $start_dow % 7;

      print "
      $month_name $cal_year\n";
      print map { sprintf " %4.4s ",
        Day_of_Week_Abbreviation($_) } 7,1..6;
      print "\n";
      {
        for ($curday..($curday+6)) {
          if ($_ < 1) {
            print "<<<<<<";
          } elsif ($_ <= $end_day) {
            printf "[ %2d ]", $_;
          } else {
            print ">>>>>>";
          }
        }
        print "\n";
        $curday += 7;
        redo if $curday <= $end_day;
      }
    }

Здесь самыми заметными вещами является то, что я использую Date::Calc для получения сегодняшнего месяцы и года (Today), получение текстового имени месяца (Month_to_Text), нахождение дня недели (Day_of_Week), нахождение последнего дня месяца (Days_in_Month) и даже получение правильных имен для дней недели (Day_of_Week_Abbreviation).

Имея все эти подпрограммы очень легко написать программу. Что должно бы сделать ее более легкой, если заметить, что почти все мои предыдущие подпрограммы &do_cal могли быть заменены подпрограммой Date::Calc с именем Calendar, о которой я не знал, пока почти не закончил программу.

Так, что важным советом является ``Не изобретайте колесо... прочитайте всю документацию''. В следующий раз я последую своему собственному совету. И если вы все еще думаете, что Perl не достаточно полезен, то теперь вы знаете как получить дату с помощью Perl или по крайней мере правильное время. Наслаждайтесь.


Next Previous Contents