bug-gnulib@gnu.org mirror (unofficial)
 help / color / mirror / Atom feed
From: Bruno Haible <bruno@clisp.org>
To: bug-gnulib@gnu.org
Cc: Paul Eggert <eggert@cs.ucla.edu>
Subject: localtime on native Windows
Date: Sun, 11 Feb 2024 13:46:50 +0100	[thread overview]
Message-ID: <7117573.dE46n4Xy2H@nimes> (raw)
In-Reply-To: <2364117.OYXXYNVTWy@nimes>

[-- Attachment #1: Type: text/plain, Size: 1802 bytes --]

I wrote:
> For these modules, the next function to provide in an MT-safe way is
> localtime_r.

Our gmtime_r and localtime_r are MT-safe on native Windows. I ran the
test-gmtime_r-mt and test-localtime_r-mt tests for 2000 seconds each, and
they did not crash.

But the problem is that localtime() and localtime_r() on native Windows
produce nonsensical results:
  - They pretend that in France, in 2007, DST began on 2007-03-11. When in
    fact, it started on 2007-03-25.
  - The hour is wrong.
Witness: The attached program loc.c.

> On native Windows, when the 'localtime_s' function [1][2]
> is not available, such as on the older Windows versions that Emacs cares
> about, the solution is to use GetTimeZoneInformation [3].

None of the GetTimeZoneInformation APIs from the Windows DLLs works either.
They pretend that in German and French time zones, DST starts on March 5,
in all years. Witness: The attached program tzi.c.

So, there is no way around implementing a correct localtime_r, based on
tzdata, in Gnulib.

It will be useful
  - for localtime_r on native Windows,
  - for nstrftime, c_nstrftime, parse-datetime, which all take a timezone_t
    argument.

For reading tzdata: The first question is how to include tzdata in gnulib.
  - AFAICS, the main data file (without comments) is tzdata.zi and is about
    100 KB large. It can be upgraded simply by copying the newest tzdata.zi
    from a newer tzdata distribution. Including such a file in gnulib would
    be OK (re copyright, number of files, total size), right?
  - Whereas including all files from /usr/share/zoneinfo is probably not
    acceptable (> 1300 files, ca. 6 MB total size).
  - Access pattern: In a running program, very few among the time zones will
    be used. Therefore, caching in memory is essential.

Bruno

[-- Attachment #2: loc.c --]
[-- Type: text/x-csrc, Size: 1519 bytes --]

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>


/* Some common time zone name.  */

#if defined _WIN32 && !defined __CYGWIN__
/* Cf. <https://ss64.com/timezones.html>  */
# define FRENCH_TZ  "Romance Standard Time"
#else
/* Cf. <https://en.wikipedia.org/wiki/List_of_tz_database_time_zones>  */
//# define FRENCH_TZ  "Europe/Paris"
# define FRENCH_TZ  "Europe/Berlin"
#endif


static void
show_localtime_result (time_t t)
{
  struct tm *result = localtime (&t);
  printf ("%ld -> %04d-%02d-%02d %02d:%02d:%02d DST=%d\n",
          (long) t,
          result->tm_year + 1900, result->tm_mon + 1, result->tm_mday,
          result->tm_hour, result->tm_min, result->tm_sec,
          result->tm_isdst);
}

int
main (int argc, char *argv[])
{
#if defined _WIN32 && !defined __CYGWIN__
  _putenv ("TZ" "=" FRENCH_TZ);
#else
  setenv ("TZ", FRENCH_TZ, 1);
#endif

  show_localtime_result (1173578399); /* 2007-03-11 02:59:59 */
  show_localtime_result (1173578401); /* 2007-03-11 03:00:01 */

  show_localtime_result (1174784399); /* 2007-03-25 01:59:59 */
  show_localtime_result (1174784401); /* 2007-03-25 03:00:01 */

  return 0;
}
/*
glibc, Cygwin:
1173578399 -> 2007-03-11 02:59:59 DST=0
1173578401 -> 2007-03-11 03:00:01 DST=0
1174784399 -> 2007-03-25 01:59:59 DST=0
1174784401 -> 2007-03-25 03:00:01 DST=1

Native Windows:
1173578399 -> 2007-03-11 01:59:59 DST=0
1173578401 -> 2007-03-11 03:00:01 DST=1
1174784399 -> 2007-03-25 01:59:59 DST=1
1174784401 -> 2007-03-25 02:00:01 DST=1
*/

[-- Attachment #3: tzi.c --]
[-- Type: text/x-csrc, Size: 4372 bytes --]

#include <stdio.h>
#include <windows.h>
#include <timezoneapi.h>
#include <wchar.h>

/* Cf. <https://ss64.com/timezones.html>  */
# define FRENCH_TZ  "Romance Standard Time"

int main ()
{
  _putenv ("TZ=" FRENCH_TZ);

  DWORD ret;

  TIME_ZONE_INFORMATION info1;
  ret = GetTimeZoneInformation (&info1);
  printf ("GetTimeZoneInformation:\n"
          "ret = %lu  info1 =\n"
          "standard: |%ls| bias=%ld date=%4d-%02d-%02d %02d:%02d:%02d.%03d\n"
          "daylight: |%ls| bias=%ld date=%4d-%02d-%02d %02d:%02d:%02d.%03d\n",
          ret,
          info1.StandardName,
          info1.StandardBias,
          info1.StandardDate.wYear,
          info1.StandardDate.wMonth,
          info1.StandardDate.wDay,
          info1.StandardDate.wHour,
          info1.StandardDate.wMinute,
          info1.StandardDate.wSecond,
          info1.StandardDate.wMilliseconds,
          info1.DaylightName,
          info1.DaylightBias,
          info1.DaylightDate.wYear,
          info1.DaylightDate.wMonth,
          info1.DaylightDate.wDay,
          info1.DaylightDate.wHour,
          info1.DaylightDate.wMinute,
          info1.DaylightDate.wSecond,
          info1.DaylightDate.wMilliseconds);

  DYNAMIC_TIME_ZONE_INFORMATION info2;
  ret = GetDynamicTimeZoneInformation (&info2);
  printf ("GetDynamicTimeZoneInformation:\n"
          "ret = %lu  info2 = bias=%ld timezonekey=%ls dynamicdisabled=%d\n"
          "standard: |%ls| bias=%ld date=%4d-%02d-%02d %02d:%02d:%02d.%03d\n"
          "daylight: |%ls| bias=%ld date=%4d-%02d-%02d %02d:%02d:%02d.%03d\n",
          ret,
          info2.Bias,
          info2.TimeZoneKeyName,
          info2.DynamicDaylightTimeDisabled,
          info2.StandardName,
          info2.StandardBias,
          info2.StandardDate.wYear,
          info2.StandardDate.wMonth,
          info2.StandardDate.wDay,
          info2.StandardDate.wHour,
          info2.StandardDate.wMinute,
          info2.StandardDate.wSecond,
          info2.StandardDate.wMilliseconds,
          info2.DaylightName,
          info2.DaylightBias,
          info2.DaylightDate.wYear,
          info2.DaylightDate.wMonth,
          info2.DaylightDate.wDay,
          info2.DaylightDate.wHour,
          info2.DaylightDate.wMinute,
          info2.DaylightDate.wSecond,
          info2.DaylightDate.wMilliseconds);

  DYNAMIC_TIME_ZONE_INFORMATION info3i;
  TIME_ZONE_INFORMATION info3;
  {
    DWORD i;
    for (i = 0; ; i++)
      {
        if (EnumDynamicTimeZoneInformation (i, &info3i) == ERROR_SUCCESS // Link error in mingw, OK in MSVC.
            && wcscmp (info3i.TimeZoneKeyName, L"Romance Standard Time") == 0)
          break;
      }
  }
  ret = GetTimeZoneInformationForYear (2007, &info3i, &info3);
  printf ("GetTimeZoneInformationForYear(2007):\n"
          "ret = %lu  info3 =\n"
          "standard: |%ls| bias=%ld date=%4d-%02d-%02d %02d:%02d:%02d.%03d\n"
          "daylight: |%ls| bias=%ld date=%4d-%02d-%02d %02d:%02d:%02d.%03d\n",
          ret,
          info3.StandardName,
          info3.StandardBias,
          info3.StandardDate.wYear,
          info3.StandardDate.wMonth,
          info3.StandardDate.wDay,
          info3.StandardDate.wHour,
          info3.StandardDate.wMinute,
          info3.StandardDate.wSecond,
          info3.StandardDate.wMilliseconds,
          info3.DaylightName,
          info3.DaylightBias,
          info3.DaylightDate.wYear,
          info3.DaylightDate.wMonth,
          info3.DaylightDate.wDay,
          info3.DaylightDate.wHour,
          info3.DaylightDate.wMinute,
          info3.DaylightDate.wSecond,
          info3.DaylightDate.wMilliseconds);

}
/* Compile:
   $CC tzi.c -Wall -D_WIN32_WINNT=_WIN32_WINNT_WIN8 -ladvapi32
 */
/* Results:

GetTimeZoneInformation:
ret = 1  info1 =
standard: |W. Europe Standard Time| bias=0 date=   0-10-05 03:00:00.000
daylight: |W. Europe Daylight Time| bias=-60 date=   0-03-05 02:00:00.000
GetDynamicTimeZoneInformation:
ret = 1  info2 = bias=-60 timezonekey=W. Europe Standard Time dynamicdisabled=0
standard: |W. Europe Standard Time| bias=0 date=   0-10-05 03:00:00.000
daylight: |W. Europe Daylight Time| bias=-60 date=   0-03-05 02:00:00.000
GetTimeZoneInformationForYear(2007):
ret = 1  info3 =
standard: |Romance Standard Time| bias=0 date=   0-10-05 03:00:00.000
daylight: |Romance Daylight Time| bias=-60 date=   0-03-05 02:00:00.000

*/

  reply	other threads:[~2024-02-11 12:47 UTC|newest]

Thread overview: 13+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2024-02-09 17:43 MT-unsafe time modules Bruno Haible
2024-02-09 20:22 ` Bruno Haible
2024-02-10 11:10 ` Bruno Haible
2024-02-11 12:46   ` Bruno Haible [this message]
2024-02-13  2:02     ` localtime on native Windows Paul Eggert
2024-02-18  2:38       ` Bruno Haible
2024-02-18  4:50         ` Paul Eggert
2024-02-18 14:38           ` Bruno Haible
2024-02-18 19:05             ` Paul Eggert
2024-02-11 10:43 ` MT-unsafe time modules Bruno Haible
  -- strict thread matches above, loose matches on Subject: below --
2024-02-13 18:25 localtime on native Windows Brian Inglis
2024-02-18  2:14 ` Bruno Haible
2024-02-18  5:32   ` Brian.Inglis

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

  List information: https://lists.gnu.org/mailman/listinfo/bug-gnulib

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=7117573.dE46n4Xy2H@nimes \
    --to=bruno@clisp.org \
    --cc=bug-gnulib@gnu.org \
    --cc=eggert@cs.ucla.edu \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).