diff --git a/ext/POSIX/POSIX.xs b/ext/POSIX/POSIX.xs index d2b4167a4a96..a0faa37bb817 100644 --- a/ext/POSIX/POSIX.xs +++ b/ext/POSIX/POSIX.xs @@ -3589,7 +3589,7 @@ difftime(time1, time2) # sv_setpv(TARG, ...) could be used rather than # ST(0) = sv_2mortal(newSVpv(...)) void -strftime(fmt, sec, min, hour, mday, mon, year, wday = -1, yday = -1, isdst = 0) +strftime(fmt, sec, min, hour, mday, mon, year, wday = -1, yday = -1, isdst = -1) SV * fmt int sec int min @@ -3605,10 +3605,8 @@ strftime(fmt, sec, min, hour, mday, mon, year, wday = -1, yday = -1, isdst = 0) PERL_UNUSED_ARG(wday); PERL_UNUSED_ARG(yday); - /* -isdst triggers backwards compatibility mode for non-zero - * 'isdst' */ SV *sv = sv_strftime_ints(fmt, sec, min, hour, mday, mon, year, - -abs(isdst)); + isdst); if (sv) { sv = sv_2mortal(sv); } diff --git a/ext/POSIX/lib/POSIX.pm b/ext/POSIX/lib/POSIX.pm index 48abaf971d02..a49ac5501eaa 100644 --- a/ext/POSIX/lib/POSIX.pm +++ b/ext/POSIX/lib/POSIX.pm @@ -4,7 +4,7 @@ use warnings; our ($AUTOLOAD, %SIGRT); -our $VERSION = '2.24'; +our $VERSION = '2.25'; require XSLoader; diff --git a/ext/POSIX/lib/POSIX.pod b/ext/POSIX/lib/POSIX.pod index 696ef8a80a06..ae6a02ea837b 100644 --- a/ext/POSIX/lib/POSIX.pod +++ b/ext/POSIX/lib/POSIX.pod @@ -1866,49 +1866,17 @@ Identical to the string form of C<$!>, see L. =item C Convert date and time information to string based on the current -underlying locale of the program (except for any daylight savings time). -Returns the string. +underlying locale of the program. +Returns the string in a mortalized SV; set to an empty string on error. -Synopsis: - - strftime(fmt, sec, min, hour, mday, mon, year, - wday = -1, yday = -1, isdst = 0) - -The month (C) begins at zero, -I, January is 0, not 1. The -year (C) is given in years since 1900, I, the year 1995 is 95; the -year 2001 is 101. Consult your system's C manpage for details -about these and the other arguments. + my $sv = strftime(fmt, sec, min, hour, mday, mon, year, + wday = -1, yday = -1, isdst = -1) The C and C parameters are both ignored. Their values are always determinable from the other parameters. -C should be C<1> or C<0>, depending on whether or not daylight -savings time is in effect for the given time or not. - -If you want your code to be portable, your format (C) argument -should use only the conversion specifiers defined by the ANSI C -standard (C99, to play safe). These are C. -But even then, the B of some of the conversion specifiers are -non-portable. For example, the specifiers C change according -to the locale settings of the user, and both how to set locales (the -locale names) and what output to expect are non-standard. -The specifier C changes according to the timezone settings of the -user and the timezone computation rules of the operating system. -The C specifier is notoriously unportable since the names of -timezones are non-standard. Sticking to the numeric specifiers is the -safest route. - -The arguments, except for C, are made consistent as though by -calling C before calling your system's C function. -To get correct results, you must set C to be the proper value. -When omitted, the function assumes daylight savings is not in effect. - -The string for Tuesday, December 12, 1995 in the C locale. - - $str = POSIX::strftime( "%A, %B %d, %Y", - 0, 0, 0, 12, 11, 95, 2 ); - print "$str\n"; +More details on the behavior and the specification of the other +parameters are described in L. =item C diff --git a/ext/POSIX/t/posix.t b/ext/POSIX/t/posix.t index 999197aa916b..b4b82e7fb1e0 100644 --- a/ext/POSIX/t/posix.t +++ b/ext/POSIX/t/posix.t @@ -273,7 +273,12 @@ print POSIX::strftime("ok $test # %H:%M, on %m/%d/%y\n", localtime()); # input fields to strftime(). sub try_strftime { my $expect = shift; - my $got = POSIX::strftime("%a %b %d %H:%M:%S %Y %j", @_); + my @input = @_; + + # Add zeros to missing parameters. The final 0 is for isdst, and the zero + # forces use of mini_mktime (unless the code changes). + push @input, 0 while @input < 9; + my $got = POSIX::strftime("%a %b %d %H:%M:%S %Y %j", @input); is($got, $expect, "validating mini_mktime() and strftime(): $expect"); } diff --git a/ext/POSIX/t/time.t b/ext/POSIX/t/time.t index 0d52ac5b2481..324581bda9f2 100644 --- a/ext/POSIX/t/time.t +++ b/ext/POSIX/t/time.t @@ -9,7 +9,7 @@ use strict; use Config; use POSIX; -use Test::More tests => 31; +use Test::More tests => 49; # For the first go to UTC to avoid DST issues around the world when testing. SUS3 says that # null should get you UTC, but some environments want the explicit names. @@ -226,3 +226,71 @@ SKIP: { is(strftime(undef, CORE::localtime), '', "strftime() works if format is undef"); like($warnings, qr/^Use of uninitialized value in subroutine entry /, "strftime(undef, ...) produces expected warning"); } + +SKIP: { # GH #23878; test that dst fall back works properly + my $skip_count = 9; + skip "No mktime()", $skip_count if $Config{d_mktime} ne 'define'; + my $locale = "Europe/Paris"; + $ENV{TZ} = $locale; + my $t = 1761436800; # an hour before time should have changed + + # The time in the first test case is such that UTC gives a different day. + # If the locale above is unknown, libc is supposed to use UTC; so that's + # how we check if the system knows about the rules for Paris; which on + # some systems differ from plain CET-1CEST. + skip "'$locale' not understood", $skip_count if + POSIX::strftime("%F %T%z", localtime($t - 1)) !~ /2025-10-26/; + + # On Windows, the documentation says that it won't understand what our + # $locale is set to, but instead of falling back to UTC, it uses the + # system timezone, or U.S. Pacific time. The trick above therefore + # doesn't work, so just skip this batch of tests. + skip "'$locale' not understood", $skip_count if $^O eq "MSWin32"; + + my @fall = ( + [ -1, "2025-10-26 01:59:59+0200", "Chg -1 hr, 1 sec" ], + [ 0, "2025-10-26 02:00:00+0200", "Chg -1 hr, 0 sec" ], + [ 1, "2025-10-26 02:00:01+0200", "Chg -59 min, 59 sec" ], + [ 3599, "2025-10-26 02:59:59+0200", "Chg -1 sec" ], + [ 3600, "2025-10-26 02:00:00+0100", "At Paris DST fallback" ], + [ 3601, "2025-10-26 02:00:01+0100", "Chg +1 sec" ], + [ 7199, "2025-10-26 02:59:59+0100", "Chg +1 hr, 59m, 59s" ], + [ 7200, "2025-10-26 03:00:00+0100", "Chg +1 hr" ], + [ 7201, "2025-10-26 03:00:01+0100", "Chg +1 hr, 1 sec" ], + ); + for (my $i = 0; $i < @fall; $i++) { + is(POSIX::strftime("%F %T%z", localtime $t + $fall[$i][0]), + $fall[$i][1], $fall[$i][2]); + } +} + +SKIP: { # GH #23878: test that dst spring forward works properly; use a + # locale that MS narcissism should be able to handle + my $skip_count = 9; + skip "No mktime()", $skip_count if $Config{d_mktime} ne 'define'; + my $locale = "PST8PDT"; + $ENV{TZ} = $locale; + POSIX::tzset(); + my $t = 1741510800; # an hour before time should have changed + my $offset = POSIX::strftime("%z", localtime $t); + my $zone_name = POSIX::strftime("%Z", localtime $t); + skip "Platform doesn't recognize timezone '$locale'" + if $zone_name !~ /P[SD]T/ || $offset !~ /0[87]00/; + + my @spring = ( + [ -1, "2025-03-09 00:59:59-0800", "Chg -1 hr, 1 sec" ], + [ 0, "2025-03-09 01:00:00-0800", "Chg -1 hr, 0 sec" ], + [ 1, "2025-03-09 01:00:01-0800", "Chg -59 min, 59 sec" ], + [ 3599, "2025-03-09 01:59:59-0800", "Chg -1 sec" ], + [ 3600, "2025-03-09 03:00:00-0700", + "At Redmond DST spring forward" ], + [ 3601, "2025-03-09 03:00:01-0700", "Chg +1 sec" ], + [ 7199, "2025-03-09 03:59:59-0700", "Chg +1 hr, 59m, 59s" ], + [ 7200, "2025-03-09 04:00:00-0700", "Chg +1 hr" ], + [ 7201, "2025-03-09 04:00:01-0700", "Chg +1 hr, 1 sec" ], + ); + for (my $i = 0; $i < @spring; $i++) { + is(POSIX::strftime("%F %T%z", localtime $t + $spring[$i][0]), + $spring[$i][1], $spring[$i][2]); + } +} diff --git a/locale.c b/locale.c index 5773f9ebd48c..4f0a255f26ad 100644 --- a/locale.c +++ b/locale.c @@ -8154,11 +8154,12 @@ S_maybe_override_codeset(pTHX_ const char * codeset, /* =for apidoc_section $time -=for apidoc sv_strftime_tm -=for apidoc_item sv_strftime_ints +=for apidoc sv_strftime_ints +=for apidoc_item sv_strftime_tm =for apidoc_item my_strftime -These implement the libc strftime(). +These implement libc strftime(), overcoming various deficiencies it has; you +will come to regret sooner or later using it directly instead of these. On failure, they return NULL, and set C to C. @@ -8167,70 +8168,152 @@ handle the UTF-8ness of the current locale, the input C, and the returned result. Only if the current C locale is a UTF-8 one (and S> is not in effect) will the result be marked as UTF-8. +For these, the caller assumes ownership of the returned SV with a reference +count of 1. + C is kept for backwards compatibility. Knowing if its result should be considered UTF-8 or not requires significant extra logic. Note that all three functions are always executed in the underlying C locale of the program, giving results based on that locale. -The functions differ as follows: - -C takes a pointer to a filled-in S> parameter. It -ignores the values of the C and C fields in it. The other fields -give enough information to accurately calculate these values, and are used for -that purpose. +The stringified C parameter in all is the same as the system libc +C. The available conversion specifications vary by platform. These +days, every specification listed in the ANSI C99 standard should be usable +everywhere. These are C. -The caller assumes ownership of the returned SV with a reference count of 1. +But note that the B of some of the conversion specifiers are +non-portable. For example, the specifiers C change according +to the locale settings of the user, and both how to set locales (the +locale names) and what output to expect are not standardized. +The specifier C changes according to the timezone settings of the +user and the timezone computation rules of the operating system. +The C specifier is notoriously unportable since the names of +timezones are not standardized. Sticking to the numeric specifiers is the +safest route. -C takes a bunch of integer parameters that together -completely define a given time. It calculates the S> to pass to -libc strftime(), and calls that function. +At the time of this writing, for example, C<%s> is not available on +Windows-like systems. -The value of C is used as follows: +The functions differ as follows: =over -=item 0 +=item * -No daylight savings time is in effect +The C parameter and the return from C are S> +instead of the S> in the other two functions. This means the +UTF-8ness of the format and result are unspecified. The result MUST be +arranged to be FREED BY THE CALLER). -=item E0 +=item * -Check if daylight savings time is in effect, and adjust the results -accordingly. +C and C take a bunch of integer parameters that +together completely define a given time. They calculate the S> +to pass to libc strftime(), and call that function. See below for the meaning +of the parameters. -=item E0 +C takes a pointer to an already filled-in S> +parameter, so avoids that calculation. -This value is reserved for internal use by the L module for backwards -compatibility purposes. +=item * -=back +C takes two extra parameters that are ignored, being kept only +for historical reasons. These are C and C. -The caller assumes ownership of the returned SV with a reference count of 1. +=back -C is like C except that: +The C99 Standard calls for S> to contain at least these fields: + + int tm_sec; // seconds after the minute — [0, 60] + int tm_min; // minutes after the hour — [0, 59] + int tm_hour; // hours since midnight — [0, 23] + int tm_mday; // day of the month — [1, 31] + int tm_mon; // months since January — [0, 11] + int tm_year; // years since 1900 + int tm_wday; // days since Sunday — [0, 6] + int tm_yday; // days since January 1 — [0, 365] + int tm_isdst; // Daylight Saving Time flag + +C and C are output only; the other fields give enough +information to accurately calculate these, and are internally used for that +purpose. + +The numbers enclosed in the square brackets above give the maximum legal +ranges for values in the corresponding field. Those ranges are restricted for +some inputs. For example, not all months have 31 days, but all hours have 60 +minutes. If you set a number that is outside the corresponding range, perl +and the libc functions will automatically normalize it to be inside the range, +adjusting other values as necessary. For example, specifying February 29, is +the same as saying March 1 for non-leap years; and using a minute value of 60 +will instead change that to a 0, and increment the hour, which in turn, if the +hour was 23, will roll it over to 0 it and increment the day, and so on. + +Each parameter to C and C populates the +similarly-named field in this structure. + +A value of 60 is legal for C, but only for those moments when an +official leap second has been declared. It is undefined behavior to use them +otherwise, and the behavior does vary depending on the implementation. +Some implementations take your word for it that this is a leap second, leaving +it as the 61st second of the given minute; some roll it over to be the 0th +second of the following minute; some treat it as 0. Some non-conforming +implementations always roll it over to the next minute, regardless of whether +an actual leap second is occurring or not. (And yes, it is a real problem +that different computers have a different conception of what the current time +is; you can search the internet for details.) + +There is no limit (outside the size of C) for the value of C, +but sufficiently negative values (for earlier than 1900) may have different +results on different systems and locales. Some libc implementations may know +when a given locale adopted the Greorian calendar, and adjust for that. +Others will not. (And some countries didn't adopt the Gregorian calendar +until after 1900.) + +The treatment of the C field has varied over previous Perl versions, +and has been buggy (both by perl and by some libc implementations), but is now +aligned, as best we can, with the POSIX Standard, as follows: =over -=item The C parameter and the return are S> instead of -S>. +=item C is 0 -This means the UTF-8ness of the result is unspecified. The result MUST be -arranged to be FREED BY THE CALLER). +The function is to assume that daylight savings time is not in effect. This +should now always work properly, as perl uses its own implementation in this +case, avoiding non-conforming libc ones. -=item The C parameter is ignored. +=item C is E0 -Daylight savings time is never considered to be in effect. +The function is to assume that daylight savings time is in effect, though some +underlying libc implementations treat this as a hint instead of a mandate. -=item It has extra parameters C and C that are ignored. +=item C is E0 -These exist only for historical reasons; the values for the corresponding -fields in S> are calculated from the other arguments. +The function is to itself try to calculate if daylight savings time is in +effect. More recent libc implementations are better at this than earlier +ones. =back -Note that all three functions are always executed in the underlying C -locale of the program, giving results based on that locale. +Some libc implementations have extra fields in S>. The two that +perl handles are: + + int tm_gmtoff; // Seconds East of UTC [%z] + const char * tm_zone; // Timezone abbreviation [%Z] + +These are both output only. Using the respective conversion specifications +(enclosed in the square brackets) in the C parameter is a portable way to +gain access to these values, working both on systems that have and don't have +these fields. + +Example, in the C locale: + + my_strftime( "%A, %B %d, %Y", 0, 0, 0, 12, 11, 95, 0, 0, -1 ); + +returns + + "Tuesday, December 12, 1995" + =cut */ @@ -8242,7 +8325,6 @@ Perl_my_strftime(pTHX_ const char *fmt, int sec, int min, int hour, PERL_ARGS_ASSERT_MY_STRFTIME; PERL_UNUSED_ARG(wday); PERL_UNUSED_ARG(yday); - PERL_UNUSED_ARG(isdst); #ifdef USE_LOCALE_TIME const char * locale = querylocale_c(LC_TIME); @@ -8251,7 +8333,7 @@ Perl_my_strftime(pTHX_ const char *fmt, int sec, int min, int hour, #endif struct tm mytm; - ints_to_tm(&mytm, locale, sec, min, hour, mday, mon, year, 0); + ints_to_tm(&mytm, locale, sec, min, hour, mday, mon, year, isdst); if (! strftime_tm(fmt, PL_scratch_langinfo, locale, &mytm)) { return NULL; } @@ -8271,15 +8353,8 @@ Perl_sv_strftime_ints(pTHX_ SV * fmt, int sec, int min, int hour, const char * locale = "C"; #endif - /* A negative 'isdst' triggers backwards compatibility mode for - * POSIX::strftime(), in which 0 is always passed to ints_to_tm() so that - * the possibility of daylight savings time is never considered, But, a 1 - * is eventually passed to libc strftime() so that it returns the results - * it always has for a non-zero 'isdst'. See GH #22351 */ struct tm mytm; - ints_to_tm(&mytm, locale, sec, min, hour, mday, mon, year, - MAX(0, isdst)); - mytm.tm_isdst = MIN(1, abs(isdst)); + ints_to_tm(&mytm, locale, sec, min, hour, mday, mon, year, isdst); return sv_strftime_common(fmt, locale, &mytm); } @@ -8363,20 +8438,22 @@ S_ints_to_tm(pTHX_ struct tm * mytm, mytm->tm_year = year; struct tm * which_tm = mytm; - struct tm aux_tm; #ifndef HAS_MKTIME mini_mktime(mytm); #else +# if defined(HAS_TM_TM_GMTOFF) || defined(HAS_TM_TM_ZONE) +# define ALWAYS_RUN_MKTIME + + struct tm aux_tm; + +# endif /* On platforms that have either of these two fields, we have to run the * libc mktime() in order to set them, as mini_mktime() doesn't deal with * them. [perl #18238] */ -# if defined(HAS_TM_TM_GMTOFF) || defined(HAS_TM_TM_ZONE) -# define ALWAYS_RUN_MKTIME -# endif /* When isdst is 0, it means to consider daylight savings time as never * being in effect. Many libc implementations of mktime() do not allow @@ -8385,44 +8462,52 @@ S_ints_to_tm(pTHX_ struct tm * mytm, if (isdst == 0) { mini_mktime(mytm); -# ifndef ALWAYS_RUN_MKTIME - - /* When we don't always need libc mktime(), we call it only when we didn't - * call mini_mktime() */ - } else { +# ifdef ALWAYS_RUN_MKTIME -# else /* Here will have to run libc mktime() in order to get the values of * some fields that mini_mktime doesn't populate. We don't want - * mktime's side effect of looking for dst, so we have to have a - * separate tm structure from which we copy just those fields into the - * returned structure. Initialize its values. mytm should now be a - * normalized version of the input. */ + * mktime's side effect of looking for dst (because isdst==0), so we + * have to have a separate tm structure from which we copy just those + * fields into the structure we return. Initialize its values, which + * have now been normalized by mini_mktime. */ aux_tm = *mytm; - aux_tm.tm_isdst = isdst; which_tm = &aux_tm; +# endif + + } + +# ifndef ALWAYS_RUN_MKTIME + + else { /* Don't run libc mktime if both: + 1) we ran mini_mktime above; and + 2) we don't have to always run libc mktime */ + # endif /* Here, we need to run libc mktime(), either because we want to take * dst into consideration, or because it calculates one or two fields - * that we need that mini_mktime() doesn't handle. - * - * Unlike mini_mktime(), it does consider the locale, so have to switch + * that we need that mini_mktime() doesn't handle. */ + which_tm->tm_isdst = isdst; + + /* Unlike mini_mktime(), it does consider the locale, so have to switch * to the correct one. */ const char * orig_TIME_locale = toggle_locale_c(LC_TIME, locale); MKTIME_LOCK; - /* which_tm points to an auxiliary copy if we ran mini_mktime(). + /* 'which_tm' points to an auxiliary copy if we ran mini_mktime(). * Otherwise it points to the passed-in one which now gets populated * directly. */ (void) mktime(which_tm); MKTIME_UNLOCK; restore_toggled_locale_c(LC_TIME, orig_TIME_locale); + +# ifndef ALWAYS_RUN_MKTIME + } -# if defined(HAS_TM_TM_GMTOFF) || defined(HAS_TM_TM_ZONE) +# else /* And use the saved libc values for tm_gmtoff and tm_zone if we used an * auxiliary struct to get them */ diff --git a/pod/perldelta.pod b/pod/perldelta.pod index 6f2168f0ec52..1da388c93cd6 100644 --- a/pod/perldelta.pod +++ b/pod/perldelta.pod @@ -396,6 +396,16 @@ the first branch would correctly autovivify C<$h{foo}> to an array ref, but the second branch might incorrectly autovivify C<$h{bar}> to a hash ref. [GH #18669]. +=item * + +Perl 5.42.0 does not handle the transition to/from daylight savings time +properly. The time and/or timezone can be off by an hour in the +intervals surrounding such transitions. This is a regression from +earlier releases, and is now fixed. This bug was evident from perl +space in the L function, and in XS code with any of +L, L, or +L. + =back =head1 Known Problems