18

How can I elegantly print the date in RFC822 format in Perl?

brian d foy
  • 129,424
  • 31
  • 207
  • 592
Tom Feiner
  • 20,656
  • 20
  • 48
  • 51

4 Answers4

30
use POSIX qw(strftime);
print strftime("%a, %d %b %Y %H:%M:%S %z", localtime(time())) . "\n";
njsf
  • 2,729
  • 1
  • 21
  • 17
  • Oh, nice, I didn't know there was something in the core that would do this. – Adam Bellaire Oct 05 '08 at 15:15
  • Thank you ,this is exactly, what I was looking for when I asked for an elegant way :) – Tom Feiner Oct 05 '08 at 15:27
  • Umm, I don't think that works any more. With ActivePerl 5.16 this `strftime('%a, %d %b %Y %H:%M:%S %z', localtime(time()));` yields `Thu, 14 Nov 2013 09:46:00 E. Australia Standard Time` which is definitely wrong. Please refer to RFC 2822 3.3. Date and Time Specification. – AlwaysLearning Nov 13 '13 at 23:51
  • `$ perl -v` This is perl 5, version 16, subversion 2 (v5.16.2) built for darwin-thread-multi-2level `$ perl -e 'use POSIX qw(strftime); print strftime("%a, %d %b %Y %H:%M:%S %z", localtime(time())) . "\n";'` Sun, 24 Nov 2013 20:02:53 -0500 – njsf Nov 25 '13 at 01:02
  • I did test it now on Windows and the issue is actually with Microsoft implementation of strftime. http://msdn.microsoft.com/en-us/library/fe06s4ak.aspx says: %z, %Z Either the time-zone name or time zone abbreviation, depending on registry settings; no characters if time zone is unknown. The following shares a bit more details: http://social.msdn.microsoft.com/Forums/vstudio/en-US/f76b8164-a918-4b74-b6d0-b38590c0a537/how-to-control-strftime-z-format?forum=vclanguage I did verify that setting the environment variable TZ Active perl will obey it. I did not check the actual registry setting – njsf Nov 25 '13 at 01:14
  • Microsoft clearly deviates from POSIX here: http://pubs.opengroup.org/onlinepubs/000095399/functions/strftime.html %z Replaced by the offset from UTC in the ISO 8601:2000 standard format ( +hhmm or -hhmm ), or by no characters if no timezone is determinable. For example, "-0430" means 4 hours 30 minutes behind UTC (west of Greenwich). [CX] [Option Start] If tm_isdst is zero, the standard time offset is used. If tm_isdst is greater than zero, the daylight savings time offset is used. If tm_isdst is negative, no characters are returned. [Option End] [ tm_isdst] – njsf Nov 25 '13 at 01:34
  • Apart from Microsoft bashing this doesn't work in any non-English locale. – Guido Flohr Oct 12 '18 at 21:25
15

The DateTime suite gives you a number of different ways, e.g.:

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() );

Update: to give time for some particular timezone, add a time_zone argument to now():

DateTime->now( time_zone => $ENV{'TZ'}, ... )
ysth
  • 96,171
  • 6
  • 121
  • 214
  • @GuidoFlohr could you explain the thread safety issue? I don't know of any such – ysth Oct 12 '18 at 22:11
  • See my own answer. If the Perl interpreter runs inside a kernel thread, another thread may call `setlocale()`. Then DateTime::Format::Mail may use non-English month and day names. In general, temporarily switching locale is not thread-safe in that context. Often not relevant, sometimes it is. – Guido Flohr Oct 13 '18 at 06:12
5

It can be done with strftime, but its %a (day) and %b (month) are expressed in the language of the current locale.

From man strftime:

%a The abbreviated weekday name according to the current locale.
%b The abbreviated month name according to the current locale.

The Date field in mail must use only these names (from rfc2822 DATE AND TIME SPECIFICATION):

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

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

Therefore portable code should switch to the C locale:

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";
Community
  • 1
  • 1
Daniel Vérité
  • 58,074
  • 15
  • 129
  • 156
  • This answer is better than the highest ranking one. However the call to `time()` inside `localtime()` is unnecessary (it's the default argument). – neuhaus Jun 19 '17 at 07:06
0

Just using POSIX::strftime() has issues that have already been pointed out in other answers and comments on them:

  • It will not work with MS-DOS aka Windows which produces strings like "W. Europe Standard Time" instead of "+0200" as required by RFC822 for the %z conversion specification.
  • It will print the abbreviated month and day names in the current locale instead of English, again required by RFC822.

Switching the locale to "POSIX" resp. "C" fixes the latter problem but is potentially expensive, even more for well-behaving code that later switches back to the previous locale.

But it's also not completely thread-safe. While temporarily switching locale will work without issues inside Perl interpreter threads, there are races when the Perl interpreter itself runs inside a kernel thread. This can be the case, when the Perl interpreter is embedded into a server (for example mod_perl running in a threaded Apache MPM).

The following version doesn't suffer from any such limitations because it doesn't use any locale dependent functions:

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);
}

But it should be noted that converting from seconds since the epoch to a broken down time and vice versa are quite complex and expensive operations, even more when not dealing with GMT/UTC but local time. The latter requires the inspection of zoneinfo data that contains the current and historical DST and time zone settings for the current time zone. It's also error-prone because these parameters are subject to political decisions that may be reverted in the future. Because of that, code relying on the zoneinfo data is brittle and may break, when the system is not regulary updated.

However, the purpose of RFC822 compliant date and time specifications is not to inform other servers about the timezone settings of "your" server but to give its notion of the current date and time in a timezone indepent manner. You can save a lot of CPU cycles (they can be measured in CO2 emission) on both the sending and receiving end by simply using UTC instead of localtime:

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]);
}

By hard-coding the timezone to +0000 you avoid all of the above mentioned problems, while still being perfectly standards compliant, leave alone faster. Go with that solution, when performance could be an issue for you. Go with the first solution, when your users complain about the software reporting the "wrong" timezone.

Community
  • 1
  • 1
Guido Flohr
  • 1,871
  • 15
  • 28