Как элегантно напечатать дату в формате RFC822 в Perl?

Как я могу элегантно напечатать дату в формате RFC822 в Perl?


person Tom Feiner    schedule 05.10.2008    source источник


Ответы (4)


Пакет DateTime предоставляет вам несколько различных способов, например:

use DateTime;
print DateTime->now()->strftime("%a, %d %b %Y %H:%M:%S %z");

use DateTime::Format::Mail;
print DateTime::Format::Mail->format_datetime( DateTime->now() );

print DateTime->now( formatter => DateTime::Format::Mail->new() );

Обновление: чтобы указать время для определенного часового пояса, добавьте аргумент time_zone в now():

DateTime->now( time_zone => $ENV{'TZ'}, ... )
person ysth    schedule 05.10.2008
comment
@GuidoFlohr, не могли бы вы объяснить проблему безопасности потоков? я не знаю ни одного такого - person ysth; 13.10.2018
comment
Смотрите мой собственный ответ. Если интерпретатор Perl работает внутри потока ядра, другой поток может вызвать setlocale(). Затем DateTime::Format::Mail может использовать неанглийские названия месяца и дня. Как правило, временное переключение локали в этом контексте не является потокобезопасным. Часто не актуально, иногда имеет значение. - person Guido Flohr; 13.10.2018

Это можно сделать с помощью strftime, но его %a (день) и %b (месяц) выражаются на языке текущей локали.

От man strftime:

%a Сокращенное название дня недели в соответствии с текущей локалью.
%b Сокращенное название месяца в соответствии с текущей локалью.

Поле даты в почте должно использовать только эти имена (из rfc2822 DATE И ВРЕМЯ):

day         =  "Mon"  / "Tue" /  "Wed"  / "Thu" /  "Fri"  / "Sat" /  "Sun"

month       =  "Jan"  /  "Feb" /  "Mar"  /  "Apr" /  "May"  /  "Jun" /
               "Jul"  /  "Aug" /  "Sep"  /  "Oct" /  "Nov"  /  "Dec"

Поэтому переносимый код должен переключиться на локаль C:

use POSIX qw(strftime locale_h);

my $old_locale = setlocale(LC_TIME, "C");
my $date_rfc822 = strftime("%a, %d %b %Y %H:%M:%S %z", localtime(time()));
setlocale(LC_TIME, $old_locale);

print "$date_rfc822\n";
person Daniel Vérité    schedule 20.10.2016
comment
Этот ответ лучше, чем ответ с самым высоким рейтингом. Однако вызов time() внутри localtime() не нужен (это аргумент по умолчанию). - person neuhaus; 19.06.2017

Простое использование POSIX::strftime() имеет проблемы, которые уже были указаны в других ответах и ​​комментариях к ним:

  • Он не будет работать с MS-DOS, также известной как Windows, которая создает такие строки, как «Стандартное время Западной Европы» вместо «+0200», как того требует RFC822 для спецификации преобразования %z.
  • Он будет печатать сокращенные названия месяца и дня в текущей локали вместо английского, что опять же требуется RFC822.

Переключение локали на «POSIX» соответственно. «C» устраняет последнюю проблему, но потенциально дорог, даже больше для хорошо работающего кода, который позже переключается обратно на предыдущую локаль.

Но это также не полностью потокобезопасно. Хотя временное переключение локали будет работать без проблем внутри потоков интерпретатора Perl, бывают гонки, когда сам интерпретатор Perl запускается внутри потока ядра. Это может иметь место, когда интерпретатор Perl встроен в сервер (например, mod_perl работает в многопоточном Apache MPM).

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

sub rfc822_local {
    my ($epoch) = @_;

    my @time = localtime $epoch;

    use integer;

    my $tz_offset = (Time::Local::timegm(@time) - $now) / 60;
    my $tz = sprintf('%s%02u%02u',
                     $tz_offset < 0 ? '-' : '+',
                     $tz_offset / 60, $tz_offset % 60);

    my @month_names = qw(Jan Feb Mar Apr May Jun
                         Jul Aug Sep Oct Nov Dec);
    my @day_names = qw(Sun Mon Tue Wed Thu Fri Sat Sun);

    return sprintf('%s, %02u %s %04u %02u:%02u:%02u %s',
                   $day_names[$time[6]], $time[3], $month_names[$time[4]],
                   $time[5] + 1900, $time[2], $time[1], $time[0], $tz);
}

Но следует отметить, что преобразование секунд с начала эпохи в разбитое время и наоборот — довольно сложные и дорогостоящие операции, тем более, когда речь идет не о GMT/UTC, а о местном времени. Последнее требует проверки данных zoneinfo, которые содержат текущие и исторические настройки летнего времени и часового пояса для текущего часового пояса. Это также подвержено ошибкам, поскольку эти параметры зависят от политических решений, которые может быть отменено в будущем. Из-за этого код, основанный на данных zoneinfo, хрупкий и может сломаться, если система не обновляется регулярно.

Однако цель спецификаций даты и времени, совместимых с RFC822, состоит не в том, чтобы информировать другие серверы о настройках часового пояса «вашего» сервера, а в том, чтобы дать его представление о текущей дате и времени независимо от часового пояса. Вы можете сэкономить много циклов процессора (их можно измерить в выбросах CO2) как на отправляющей, так и на принимающей стороне, просто используя UTC вместо местного времени:

sub rfc822_gm {
    my ($epoch) = @_;

    my @time = gmtime $epoch;

    my @month_names = qw(Jan Feb Mar Apr May Jun
                         Jul Aug Sep Oct Nov Dec);
    my @day_names = qw(Sun Mon Tue Wed Thu Fri Sat Sun);

    return sprintf('%s, %02u %s %04u %02u:%02u:%02u +0000',
                   $day_names[$time[6]], $time[3], $month_names[$time[4]],
                   $time[5] + 1900, $time[2], $time[1], $time[0]);
}

Жестко запрограммировав часовой пояс на +0000, вы избегаете всех вышеупомянутых проблем, но при этом полностью соответствуете стандартам, не говоря уже о более быстром времени. Используйте это решение, когда производительность может быть для вас проблемой. Используйте первое решение, когда ваши пользователи жалуются на программное обеспечение, сообщающее о «неправильном» часовом поясе.

person Guido Flohr    schedule 12.10.2018

person    schedule
comment
О, здорово, я не знал, что в ядре есть что-то, что может сделать это. - person Adam Bellaire; 05.10.2008
comment
Спасибо, это именно то, что я искал, когда просил элегантный способ :) - person Tom Feiner; 05.10.2008
comment
Эм, я не думаю, что это больше работает. В ActivePerl 5.16 это strftime('%a, %d %b %Y %H:%M:%S %z', localtime(time())); дает Thu, 14 Nov 2013 09:46:00 E. Australia Standard Time, что определенно неверно. Пожалуйста, обратитесь к RFC 2822 3.3. Спецификация даты и времени. - person AlwaysLearning; 14.11.2013
comment
$ perl -v Это Perl 5, версия 16, Subversion 2 (v5.16.2), созданный для darwin-thread-multi-2level $ perl -e 'use POSIX qw(strftime); print strftime("%a, %d %b %Y %H:%M:%S %z", localtime(time())) . "\n";' Воскресенье, 24 ноября 2013 г., 20:02:53 -0500 - person njsf; 25.11.2013
comment
Я проверил это сейчас в Windows, и проблема на самом деле связана с реализацией strftime от Microsoft. msdn.microsoft.com/en-us/library/fe06s4ak.aspx говорит: %z, %Z Либо название часового пояса, либо аббревиатура часового пояса, в зависимости от настроек реестра; без символов, если часовой пояс неизвестен. Ниже приводится более подробная информация: -z-format?forum=vclanguage" rel="nofollow noreferrer">social.msdn.microsoft.com/Forums/vstudio/en-US/ Я проверил, что установка переменной среды TZ Active perl будет подчиняться этому . Я не проверял фактическую настройку реестра - person njsf; 25.11.2013
comment
Здесь Microsoft явно отклоняется от POSIX: pubs.opengroup.org/onlinepubs/000095399/functions /strftime.html %z Заменяется смещением от UTC в стандартном формате ISO 8601:2000 ( +hhmm или -hhmm ) или отсутствием символов, если невозможно определить часовой пояс. Например, -0430 означает отставание от UTC на 4 часа 30 минут (к западу от Гринвича). [CX] [Option Start] Если tm_isdst равно нулю, используется стандартное смещение времени. Если tm_isdst больше нуля, используется смещение летнего времени. Если tm_isdst имеет отрицательное значение, никакие символы не возвращаются. [Окончание опции] [tm_isdst] - person njsf; 25.11.2013
comment
Помимо избиения Microsoft, это не работает ни в одной локали, отличной от английского языка. - person Guido Flohr; 13.10.2018