diff options
-rw-r--r-- | lib/Time/Local.pm | 20 | ||||
-rwxr-xr-x | lib/Time/Local.t | 37 | ||||
-rw-r--r-- | localtime64.c | 311 | ||||
-rw-r--r-- | localtime64.h | 11 | ||||
-rw-r--r-- | pp_sys.c | 74 | ||||
-rwxr-xr-x | t/op/time.t | 59 |
6 files changed, 438 insertions, 74 deletions
diff --git a/lib/Time/Local.pm b/lib/Time/Local.pm index 4044cd9c9a..b83bb1a7dd 100644 --- a/lib/Time/Local.pm +++ b/lib/Time/Local.pm @@ -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; } diff --git a/lib/Time/Local.t b/lib/Time/Local.t index 22138cfc0e..ef32b40405 100755 --- a/lib/Time/Local.t +++ b/lib/Time/Local.t @@ -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 index 0000000000..92372addd0 --- /dev/null +++ b/localtime64.c @@ -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 index 0000000000..5ffeaa138b --- /dev/null +++ b/localtime64.h @@ -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 @@ -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; } diff --git a/t/op/time.t b/t/op/time.t index 8b2f07d2d3..a67dead9b1 100755 --- a/t/op/time.t +++ b/t/op/time.t @@ -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"; + } } |