Patch in a 64 bit clean gmtime_r() and localtime_r() from the y2038 project. http...
Michael G Schwern [Sat, 13 Sep 2008 00:18:02 +0000 (17:18 -0700)]
Use Quad_t for our 64 bit time_t replacement.

Temporarily through out the "broken localtime work around".  That will
have to be integrated into localtime64_r().

Fix Time::Local to handle the new expanded date range.  "use integer" had
to go as it pegged scalars to 32 bit integers which aren't large enough
to hold the new time range.

There are probably portability issues.  timegm, for example, is not portable.
Also the assumption that "long" is 64 bits is probably wrong.

lib/Time/Local.pm
lib/Time/Local.t
localtime64.c [new file with mode: 0644]
localtime64.h [new file with mode: 0644]
pp_sys.c
t/op/time.t

index 4044cd9..b83bb1a 100644 (file)
@@ -4,7 +4,6 @@ require Exporter;
 use Carp;
 use Config;
 use strict;
-use integer;
 
 use vars qw( $VERSION @ISA @EXPORT @EXPORT_OK );
 $VERSION   = '1.18_01';
@@ -29,13 +28,8 @@ use constant SECS_PER_MINUTE => 60;
 use constant SECS_PER_HOUR   => 3600;
 use constant SECS_PER_DAY    => 86400;
 
-my $MaxInt = ( ( 1 << ( 8 * $Config{ivsize} - 2 ) ) - 1 ) * 2 + 1;
-my $MaxDay = int( ( $MaxInt - ( SECS_PER_DAY / 2 ) ) / SECS_PER_DAY ) - 1;
-
-if ( $^O eq 'MacOS' ) {
-    # time_t is unsigned...
-    $MaxInt = ( 1 << ( 8 * $Config{ivsize} ) ) - 1;
-}
+# localtime()'s limit is the year 2**31
+my $MaxDay = 365 * (2**31);
 
 # Determine the EPOC day for this machine
 my $Epoc = 0;
@@ -65,13 +59,13 @@ sub _daygm {
     return $_[3] + (
         $Cheat{ pack( 'ss', @_[ 4, 5 ] ) } ||= do {
             my $month = ( $_[4] + 10 ) % 12;
-            my $year  = $_[5] + 1900 - $month / 10;
+            my $year  = $_[5] + 1900 - int($month / 10);
 
             ( ( 365 * $year )
-              + ( $year / 4 )
-              - ( $year / 100 )
-              + ( $year / 400 )
-              + ( ( ( $month * 306 ) + 5 ) / 10 )
+              + int( $year / 4 )
+              - int( $year / 100 )
+              + int( $year / 400 )
+              + int( ( ( $month * 306 ) + 5 ) / 10 )
             )
             - $Epoc;
         }
index 22138cf..ef32b40 100755 (executable)
@@ -25,10 +25,10 @@ my @time =
    # leap day
    [2020,  2, 29, 12, 59, 59],
    [2030,  7,  4, 17, 07, 06],
-# The following test fails on a surprising number of systems
-# so it is commented out. The end of the Epoch for a 32-bit signed
-# implementation of time_t should be Jan 19, 2038  03:14:07 UTC.
-#  [2038,  1, 17, 23, 59, 59],     # last full day in any tz
+   [2038,  1, 17, 23, 59, 59],     # last full day in any tz
+
+   # more than 2**31 time_t
+   [2258,  8, 11,  1, 49, 17],
   );
 
 my @bad_time =
@@ -88,7 +88,7 @@ for (@time, @neg_time) {
     $year -= 1900;
     $mon--;
 
- SKIP: {
+    SKIP: {
         skip '1970 test on VOS fails.', 12
             if $^O eq 'vos' && $year == 70;
         skip 'this platform does not support negative epochs.', 12
@@ -107,20 +107,21 @@ for (@time, @neg_time) {
             is($M, $mon, "timelocal month for @$_");
             is($Y, $year, "timelocal year for @$_");
         }
+    }
 
-        {
-            my $year_in = $year < 70 ? $year + 1900 : $year;
-            my $time = timegm($sec,$min,$hour,$mday,$mon,$year_in);
+    # Perl has its own gmtime()
+    {
+        my $year_in = $year < 70 ? $year + 1900 : $year;
+        my $time = timegm($sec,$min,$hour,$mday,$mon,$year_in);
 
-            my($s,$m,$h,$D,$M,$Y) = gmtime($time);
+        my($s,$m,$h,$D,$M,$Y) = gmtime($time);
 
-            is($s, $sec, "timegm second for @$_");
-            is($m, $min, "timegm minute for @$_");
-            is($h, $hour, "timegm hour for @$_");
-            is($D, $mday, "timegm day for @$_");
-            is($M, $mon, "timegm month for @$_");
-            is($Y, $year, "timegm year for @$_");
-        }
+        is($s, $sec, "timegm second for @$_");
+        is($m, $min, "timegm minute for @$_");
+        is($h, $hour, "timegm hour for @$_");
+        is($D, $mday, "timegm day for @$_");
+        is($M, $mon, "timegm month for @$_");
+        is($Y, $year, "timegm year for @$_");
     }
 }
 
@@ -166,11 +167,7 @@ for my $p (@years) {
         "$year $string a leap year" );
 }
 
-SKIP:
 {
-    skip 'this platform does not support negative epochs.', 6
-        unless $neg_epoch_ok;
-
     eval { timegm(0,0,0,29,1,1900) };
     like($@, qr/Day '29' out of range 1\.\.28/,
          'does not accept leap day in 1900');
diff --git a/localtime64.c b/localtime64.c
new file mode 100644 (file)
index 0000000..92372ad
--- /dev/null
@@ -0,0 +1,311 @@
+/*
+
+Copyright (c) 2007-2008  Michael G Schwern
+
+This software originally derived from Paul Sheer's pivotal_gmtime_r.c.
+
+The MIT License:
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+*/
+
+/*
+
+Programmers who have available to them 64-bit time values as a 'long
+long' type can use localtime64_r() and gmtime64_r() which correctly
+converts the time even on 32-bit systems. Whether you have 64-bit time
+values will depend on the operating system.
+
+localtime64_r() is a 64-bit equivalent of localtime_r().
+
+gmtime64_r() is a 64-bit equivalent of gmtime_r().
+
+*/
+
+#include <assert.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <time.h>
+#include <errno.h>
+#include "localtime64.h"
+
+static const int days_in_month[2][12] = {
+    {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
+    {31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
+};
+
+static const int julian_days_by_month[2][12] = {
+    {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334},
+    {0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335},
+};
+
+static const int length_of_year[2] = { 365, 366 };
+
+/* Number of days in a 400 year Gregorian cycle */
+static const int years_in_gregorian_cycle = 400;
+static const int days_in_gregorian_cycle  = (365 * 400) + 100 - 4 + 1;
+
+/* 28 year calendar cycle between 2010 and 2037 */
+static const int safe_years[28] = {
+    2016, 2017, 2018, 2019,
+    2020, 2021, 2022, 2023,
+    2024, 2025, 2026, 2027,
+    2028, 2029, 2030, 2031,
+    2032, 2033, 2034, 2035,
+    2036, 2037, 2010, 2011,
+    2012, 2013, 2014, 2015
+};
+
+static const int dow_year_start[28] = {
+    5, 0, 1, 2,     /* 2016 - 2019 */
+    3, 5, 6, 0,
+    1, 3, 4, 5,
+    6, 1, 2, 3,
+    4, 6, 0, 1,
+    2, 4, 5, 6,     /* 2036, 2037, 2010, 2011 */
+    0, 2, 3, 4      /* 2012, 2013, 2014, 2015 */
+};
+
+
+#define IS_LEAP(n)     ((!(((n) + 1900) % 400) || (!(((n) + 1900) % 4) && (((n) + 1900) % 100))) != 0)
+#define WRAP(a,b,m)    ((a) = ((a) <  0  ) ? ((b)--, (a) + (m)) : (a))
+
+int _is_exception_century(long year)
+{
+    int is_exception = ((year % 100 == 0) && !(year % 400 == 0));
+    /* printf("is_exception_century: %s\n", is_exception ? "yes" : "no"); */
+
+    return(is_exception);
+}
+
+void _check_tm(struct tm *tm)
+{
+    /* Don't forget leap seconds */
+    assert(tm->tm_sec  >= 0 && tm->tm_sec <= 61);
+    assert(tm->tm_min  >= 0 && tm->tm_min <= 59);
+    assert(tm->tm_hour >= 0 && tm->tm_hour <= 23);
+    assert(tm->tm_mday >= 1 && tm->tm_mday <= 31);
+    assert(tm->tm_mon  >= 0 && tm->tm_mon  <= 11);
+    assert(tm->tm_wday >= 0 && tm->tm_wday <= 6);
+    assert(tm->tm_yday >= 0 && tm->tm_yday <= 365);
+
+#ifdef TM_HAS_GMTOFF
+    assert(   tm->tm_gmtoff >= -24 * 60 * 60
+           && tm->tm_gmtoff <=  24 * 60 * 60);
+#endif
+
+    if( !IS_LEAP(tm->tm_year) ) {
+        /* no more than 365 days in a non_leap year */
+        assert( tm->tm_yday <= 364 );
+
+        /* and no more than 28 days in Feb */
+        if( tm->tm_mon == 1 ) {
+            assert( tm->tm_mday <= 28 );
+        }
+    }
+}
+
+/* The exceptional centuries without leap years cause the cycle to
+   shift by 16
+*/
+int _cycle_offset(long year)
+{
+    const long start_year = 2000;
+    long year_diff  = year - start_year - 1;
+    long exceptions  = year_diff / 100;
+    exceptions     -= year_diff / 400;
+
+    assert( year >= 2001 );
+
+    /* printf("year: %d, exceptions: %d\n", year, exceptions); */
+
+    return exceptions * 16;
+}
+
+/* For a given year after 2038, pick the latest possible matching
+   year in the 28 year calendar cycle.
+*/
+#define SOLAR_CYCLE_LENGTH 28
+int _safe_year(long year)
+{
+    int safe_year;
+    long year_cycle = year + _cycle_offset(year);
+
+    /* Change non-leap xx00 years to an equivalent */
+    if( _is_exception_century(year) )
+        year_cycle += 11;
+
+    year_cycle %= SOLAR_CYCLE_LENGTH;
+
+    safe_year = safe_years[year_cycle];
+
+    assert(safe_year <= 2037 && safe_year >= 2010);
+
+    /*
+    printf("year: %d, year_cycle: %d, safe_year: %d\n",
+           year, year_cycle, safe_year);
+    */
+
+    return safe_year;
+}
+
+struct tm *gmtime64_r (const Time64_T *in_time, struct tm *p)
+{
+    int v_tm_sec, v_tm_min, v_tm_hour, v_tm_mon, v_tm_wday;
+    Time64_T v_tm_tday;
+    int leap;
+    Time64_T m;
+    Time64_T time = *in_time;
+    Time64_T year;
+
+#ifdef TM_HAS_GMTOFF
+    p->tm_gmtoff = 0;
+#endif
+    p->tm_isdst  = 0;
+
+#ifdef TM_HAS_ZONE
+    p->tm_zone   = "UTC";
+#endif
+
+    v_tm_sec =  time % 60;
+    time /= 60;
+    v_tm_min =  time % 60;
+    time /= 60;
+    v_tm_hour = time % 24;
+    time /= 24;
+    v_tm_tday = time;
+    WRAP (v_tm_sec, v_tm_min, 60);
+    WRAP (v_tm_min, v_tm_hour, 60);
+    WRAP (v_tm_hour, v_tm_tday, 24);
+    if ((v_tm_wday = (v_tm_tday + 4) % 7) < 0)
+        v_tm_wday += 7;
+    m = v_tm_tday;
+    if (m >= 0) {
+        year = 70;
+
+        /* Gregorian cycles, this is huge optimization for distant times */
+        while (m >= (Time64_T) days_in_gregorian_cycle) {
+            m -= (Time64_T) days_in_gregorian_cycle;
+            year += years_in_gregorian_cycle;
+        }
+
+        /* Years */
+        leap = IS_LEAP (year);
+        while (m >= (Time64_T) length_of_year[leap]) {
+            m -= (Time64_T) length_of_year[leap];
+            year++;
+            leap = IS_LEAP (year);
+        }
+
+        /* Months */
+        v_tm_mon = 0;
+        while (m >= (Time64_T) days_in_month[leap][v_tm_mon]) {
+            m -= (Time64_T) days_in_month[leap][v_tm_mon];
+            v_tm_mon++;
+        }
+    } else {
+        year = 69;
+
+        /* Gregorian cycles */
+        while (m < (Time64_T) -days_in_gregorian_cycle) {
+            m += (Time64_T) days_in_gregorian_cycle;
+            year -= years_in_gregorian_cycle;
+        }
+
+        /* Years */
+        leap = IS_LEAP (year);
+        while (m < (Time64_T) -length_of_year[leap]) {
+            m += (Time64_T) length_of_year[leap];
+            year--;
+            leap = IS_LEAP (year);
+        }
+
+        /* Months */
+        v_tm_mon = 11;
+        while (m < (Time64_T) -days_in_month[leap][v_tm_mon]) {
+            m += (Time64_T) days_in_month[leap][v_tm_mon];
+            v_tm_mon--;
+        }
+        m += (Time64_T) days_in_month[leap][v_tm_mon];
+    }
+
+    p->tm_year = year;
+    if( p->tm_year != year ) {
+        errno = EOVERFLOW;
+        return NULL;
+    }
+
+    p->tm_mday = (int) m + 1;
+    p->tm_yday = julian_days_by_month[leap][v_tm_mon] + m;
+    p->tm_sec = v_tm_sec, p->tm_min = v_tm_min, p->tm_hour = v_tm_hour,
+        p->tm_mon = v_tm_mon, p->tm_wday = v_tm_wday;
+
+    _check_tm(p);
+
+    return p;
+}
+
+
+struct tm *localtime64_r (const Time64_T *time, struct tm *local_tm)
+{
+    time_t safe_time;
+    struct tm gm_tm;
+    long orig_year;
+    int month_diff;
+
+    gmtime64_r(time, &gm_tm);
+    orig_year = gm_tm.tm_year;
+
+    if (gm_tm.tm_year > (2037 - 1900))
+        gm_tm.tm_year = _safe_year(gm_tm.tm_year + 1900) - 1900;
+
+    safe_time = timegm(&gm_tm);
+    localtime_r(&safe_time, local_tm);
+
+    local_tm->tm_year = orig_year;
+    month_diff = local_tm->tm_mon - gm_tm.tm_mon;
+
+    /*  When localtime is Dec 31st previous year and
+        gmtime is Jan 1st next year.
+    */
+    if( month_diff == 11 ) {
+        local_tm->tm_year--;
+    }
+
+    /*  When localtime is Jan 1st, next year and
+        gmtime is Dec 31st, previous year.
+    */
+    if( month_diff == -11 ) {
+        local_tm->tm_year++;
+    }
+
+    /* GMT is Jan 1st, xx01 year, but localtime is still Dec 31st
+       in a non-leap xx00.  There is one point in the cycle
+       we can't account for which the safe xx00 year is a leap
+       year.  So we need to correct for Dec 31st comming out as
+       the 366th day of the year.
+    */
+    if( !IS_LEAP(local_tm->tm_year) && local_tm->tm_yday == 365 )
+        local_tm->tm_yday--;
+
+    _check_tm(local_tm);
+
+    return local_tm;
+}
diff --git a/localtime64.h b/localtime64.h
new file mode 100644 (file)
index 0000000..5ffeaa1
--- /dev/null
@@ -0,0 +1,11 @@
+#include <time.h>
+
+#ifndef LOCALTIME64_H
+#    define LOCALTIME64_H
+
+typedef Quad_t Time64_T;
+
+struct tm *gmtime64_r    (const Time64_T *in_time, struct tm *p);
+struct tm *localtime64_r (const Time64_T *time, struct tm *local_tm);
+
+#endif
index 481864b..1e445da 100644 (file)
--- a/pp_sys.c
+++ b/pp_sys.c
@@ -27,6 +27,8 @@
 #include "EXTERN.h"
 #define PERL_IN_PP_SYS_C
 #include "perl.h"
+#include "localtime64.h"
+#include "localtime64.c"
 
 #ifdef I_SHADOW
 /* Shadow password support for solaris - pdo@cs.umd.edu
@@ -4446,60 +4448,64 @@ PP(pp_gmtime)
 {
     dVAR;
     dSP;
-    Time_t when;
-    const struct tm *tmbuf;
+    Time64_T when;
+    struct tm tmbuf;
+    struct tm *err;
     static const char * const dayname[] =
        {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
     static const char * const monname[] =
        {"Jan", "Feb", "Mar", "Apr", "May", "Jun",
         "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
 
-    if (MAXARG < 1)
-       (void)time(&when);
+    if (MAXARG < 1) {
+       time_t now;
+       (void)time(&now);
+       when = (Time64_T)now;
+    }
     else
-#ifdef BIG_TIME
-       when = (Time_t)SvNVx(POPs);
-#else
-       when = (Time_t)SvIVx(POPs);
-#endif
+       when = (Time64_T)SvNVx(POPs);
 
     if (PL_op->op_type == OP_LOCALTIME)
-#ifdef LOCALTIME_EDGECASE_BROKEN
-       tmbuf = S_my_localtime(aTHX_ &when);
-#else
-       tmbuf = localtime(&when);
-#endif
+        err = localtime64_r(&when, &tmbuf);
     else
-       tmbuf = gmtime(&when);
+       err = gmtime64_r(&when, &tmbuf);
 
-    if (GIMME != G_ARRAY) {
+    if( (tmbuf.tm_year + 1900) < 0 )
+       Perl_warner(aTHX_ packWARN(WARN_OVERFLOW),
+                   "local/gmtime under/overflowed the year");
+
+    if (GIMME != G_ARRAY) {    /* scalar context */
        SV *tsv;
         EXTEND(SP, 1);
         EXTEND_MORTAL(1);
-       if (!tmbuf)
+       if (err == NULL)
            RETPUSHUNDEF;
+
        tsv = Perl_newSVpvf(aTHX_ "%s %s %2d %02d:%02d:%02d %d",
-                           dayname[tmbuf->tm_wday],
-                           monname[tmbuf->tm_mon],
-                           tmbuf->tm_mday,
-                           tmbuf->tm_hour,
-                           tmbuf->tm_min,
-                           tmbuf->tm_sec,
-                           tmbuf->tm_year + 1900);
+                           dayname[tmbuf.tm_wday],
+                           monname[tmbuf.tm_mon],
+                           tmbuf.tm_mday,
+                           tmbuf.tm_hour,
+                           tmbuf.tm_min,
+                           tmbuf.tm_sec,
+                           tmbuf.tm_year + 1900);
        mPUSHs(tsv);
     }
-    else if (tmbuf) {
+    else {                     /* list context */
+       if ( err == NULL )
+           RETURN;
+
         EXTEND(SP, 9);
         EXTEND_MORTAL(9);
-        mPUSHi(tmbuf->tm_sec);
-       mPUSHi(tmbuf->tm_min);
-       mPUSHi(tmbuf->tm_hour);
-       mPUSHi(tmbuf->tm_mday);
-       mPUSHi(tmbuf->tm_mon);
-       mPUSHi(tmbuf->tm_year);
-       mPUSHi(tmbuf->tm_wday);
-       mPUSHi(tmbuf->tm_yday);
-       mPUSHi(tmbuf->tm_isdst);
+        mPUSHi(tmbuf.tm_sec);
+       mPUSHi(tmbuf.tm_min);
+       mPUSHi(tmbuf.tm_hour);
+       mPUSHi(tmbuf.tm_mday);
+       mPUSHi(tmbuf.tm_mon);
+       mPUSHi(tmbuf.tm_year);
+       mPUSHi(tmbuf.tm_wday);
+       mPUSHi(tmbuf.tm_yday);
+       mPUSHi(tmbuf.tm_isdst);
     }
     RETURN;
 }
index 8b2f07d..a67dead 100755 (executable)
@@ -1,14 +1,12 @@
 #!./perl
 
-$does_gmtime = gmtime(time);
-
 BEGIN {
     chdir 't' if -d 't';
     @INC = '../lib';
     require './test.pl';
 }
 
-plan tests => 8;
+plan tests => 32;
 
 ($beguser,$begsys) = times;
 
@@ -32,7 +30,9 @@ ok($i >= 2_000_000, 'very basic times test');
 ($xsec,$foo) = localtime($now);
 $localyday = $yday;
 
-ok($sec != $xsec && $mday && $year,             'localtime() list context');
+isnt($sec, $xsec),      'localtime() list context';
+ok $mday,               '  month day';
+ok $year,               '  year';
 
 ok(localtime() =~ /^(Sun|Mon|Tue|Wed|Thu|Fri|Sat)[ ]
                     (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[ ]
@@ -56,13 +56,13 @@ $ENV{TZ} = "GMT+5";
 ok($hour != $hour2,                             'changes to $ENV{TZ} respected');
 }
 
-SKIP: {
-    skip "No gmtime()", 3 unless $does_gmtime;
 
 ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = gmtime($beg);
 ($xsec,$foo) = localtime($now);
 
-ok($sec != $xsec && $mday && $year,             'gmtime() list context');
+isnt($sec, $xsec),      'gmtime() list conext';
+ok $mday,               '  month day';
+ok $year,               '  year';
 
 my $day_diff = $localyday - $yday;
 ok( grep({ $day_diff == $_ } (0, 1, -1, 364, 365, -364, -365)),
@@ -76,4 +76,49 @@ ok(gmtime() =~ /^(Sun|Mon|Tue|Wed|Thu|Fri|Sat)[ ]
                /x,
    'gmtime(), scalar context'
   );
+
+
+
+# Test gmtime over a range of times.
+{
+    # gm/localtime is limited by the size of tm_year which might be as small as 16 bits
+    my %tests = (
+        # time_t         gmtime list                          scalar
+        -2**35        => [52, 13, 20, 7, 2, -1019, 5, 65, 0, "Fri Mar  7 20:13:52 881"],
+        -2**32        => [44, 31, 17, 24, 10, -67, 0, 327, 0, "Sun Nov 24 17:31:44 1833"],
+        -2**31        => [52, 45, 20, 13, 11, 1, 5, 346, 0, "Fri Dec 13 20:45:52 1901"],
+        0             => [0, 0, 0, 1, 0, 70, 4, 0, 0, "Thu Jan  1 00:00:00 1970"],
+        2**30         => [4, 37, 13, 10, 0, 104, 6, 9, 0, "Sat Jan 10 13:37:04 2004"],
+        2**31         => [8, 14, 3, 19, 0, 138, 2, 18, 0, "Tue Jan 19 03:14:08 2038"],
+        2**32         => [16, 28, 6, 7, 1, 206, 0, 37, 0, "Sun Feb  7 06:28:16 2106"],
+        2**39         => [8, 18, 12, 25, 0, 17491, 2, 24, 0, "Tue Jan 25 12:18:08 19391"],
+    );
+
+    for my $time (keys %tests) {
+        my @expected  = @{$tests{$time}};
+        my $scalar    = pop @expected;
+
+        ok eq_array([gmtime($time)], \@expected),  "gmtime($time) list context";
+        is scalar gmtime($time), $scalar,       "  scalar";
+    }
+}
+
+
+# Test localtime
+{
+    # We pick times which fall in the middle of a month, so the month and year should be
+    # the same regardless of the time zone.
+    my %tests = (
+        # time_t           month, year,  scalar
+        5000000000      => [5,  228,     qr/Jun \d+ .* 2128$/],
+        1163500000      => [10, 106,     qr/Nov \d+ .* 2006$/],
+    );
+
+    for my $time (keys %tests) {
+        my @expected  = @{$tests{$time}};
+        my $scalar    = pop @expected;
+
+        ok eq_array([(localtime($time))[4,5]], \@expected),  "localtime($time) list context";
+        like scalar localtime($time), $scalar,       "  scalar";
+    }
 }