The Time::Local Trap

The localtime and gmtime built-in functions are interfaces to the POSIX functions for turning a Unix epoch timestamp into date-time components in either UTC or the system local time. When you want to go the other way, there's Time::Local.

Well, almost.

The localtime and gmtime functions have two quirks as compared to the date-time components humans might expect. The value it returns for the month is 0-indexed, ostensibly so that it can be used directly in a 0-indexed array of 12 month names, and the value it returns for the year is the number of years since the year 1900.

Historically, Time::Local's timelocal and timegm functions have not used this interpretation for the year value; they use a heuristic to attempt to handle the common two- and four-digit human representations of years, in addition to the localtime year value format. Unfortunately, this means that quite often, the year value as returned by localtime or similar functions will not be interpreted correctly. Of note, starting in 2020, the value of 70 returned for the year of the Unix epoch 1970-01-01 will start to be interpreted as 2070.

my $epoch = timegm(gmtime(0)); # 3155760000 since 2020

Recent versions of Time::Local (1.27+, or the version that comes with Perl 5.30+) introduce timelocal_modern and timegm_modern variants with a more consistent interpretation. While it is a reliable interpretation unlike the legacy functions, it interprets it as a full year value, rather than the years since 1900 as returned by localtime and similar functions. This means you unfortunately still cannot use it directly as the reverse of localtime/gmtime, but at least if you are interpreting values from humans, you can consistently pass in the real year.

There is hope however. If you have access to the new modern functions, you can simply add 1900 to the localtime year value and it will always be interpreted correctly. If you don't, adding 1900 to the year value if it is zero or positive will always avoid the two-digit heuristic and thus make the interpretation consistent.

# modern functions
my @localtime = localtime($epoch);
$localtime[5] += 1900;
my $epoch = timelocal_modern(@localtime);

# legacy functions
my @gmtime = gmtime($epoch);
$gmtime[5] += 1900 if $gmtime[5] >= 0;
my $epoch = timegm(@gmtime);

UPDATE: Time::Local version 1.30 has had two new functions added called timelocal_posix and timegm_posix, these will treat the year value as the number of years since 1900 and so be consistently the reverse of the core functions. Thus if you can depend on this version of Time::Local you can simply use these functions instead for consistent reversal.

my @localtime = localtime($epoch);
my $epoch = timelocal_posix(@localtime);

Reddit comments

2 Comments

Thanks for sharing this. I think that it would be appropriate to note that years greater than 999 are interpreted as being the actual year, rather than the offset from 1900. Although indirectly this follows from your post.

Leave a comment

About Grinnz

user-pic I blog about Perl.