[tz] What's "right"?

Guy Harris gharris at sonic.net
Sat Nov 14 00:20:30 UTC 2020


On Nov 13, 2020, at 2:51 PM, Guy Harris <gharris at sonic.net> wrote:

> On Nov 13, 2020, at 5:17 AM, Michael H Deckers <michael.h.deckers at googlemail.com> wrote:
> 
>>   The computing interfaces for tzdb data typically allow for the
>>   conversion between datetimes of UTC and datetimes of any tzdb Zone
>>   time.
> 
> The computing interfaces, with the "posix" values, allow for the conversion of a value expressed as POSIX "seconds since the Epoch" - i.e., as a count of seconds, *excluding* leap seconds, that have elapsed since the Epoch - to a date and time, expressed as year/month/day/hour/minute/second, either as UTC (gmtime()) or as local time in any tzdb region (localtime()).

If you want to convert between year/month/day/hour/minute/second as UTC and year/month/day/hour/minute/second as local time, do:

	struct tm *
	utc_to_local(struct tm *utc)
	{
		time_t tmp;

		tmp = {do the calculation based on the values in *utc};
		return localtime(&tmp);
	}

to go from UTC to local time and

	struct tm *
	local_to_utc(struct tm *local)
	{
		time_t tmp;

		tmp = mktime(local);
		if (tmp == -1)
			return NULL;	/* no can do */
		return gmtime(&tmp);
	}

to go from local time to UTC.

Note that these will work correctly for UTC times with second = 60 - i.e., for leap seconds - only if 1) you are using the "right" tzdb files and 2) {do the calculation based on the values in *utc} involves using timegm(), if available, using the leap second information from the "right" tzdb files.  Otherwise, Weird Stuff may happen if you try to convert, for example, 2016-12-31 23:59:60 UTC to local time.

It *also* requires that

For example, on Ubuntu 20.04, which has the "right" values, the following program:

	/*
	 * Yes, I want all the APIs.
	 */
	#define _DEFAULT_SOURCE
	#define _XOPEN_SOURCE

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

	static struct tm *
	utc_to_local(struct tm *utc)
	{
		time_t tmp;

		tmp = timegm(utc);
		return localtime(&tmp);
	}

	static struct tm *
	local_to_utc(struct tm *local)
	{
		time_t tmp;

		tmp = mktime(local);
		if (tmp == -1)
			return NULL;	/* no can do */
		return gmtime(&tmp);
	}

	int
	main(int argc, char **argv)
	{
		struct tm input;
		struct tm *output;
		char bufin[1024+1], bufout[1024+1];

		if (argc != 2) {
			fprintf(stderr, "Usage: timetest <time>\n");
			return 1;
		}

		if (strptime(argv[1], "%Y-%m-%d %H:%M:%S", &input) == NULL) {
			fprintf(stderr, "timetest: \"%s\" is not a valid time\n",
			    argv[1]);
			return 2;
		}
		input.tm_isdst = -1;
		strftime(bufin, sizeof bufin, "%Y-%m-%d %H:%M:%S", &input);
		printf("%s is converted to %04d-%02d-%02d %02d:%02d:%02d\n",
		    argv[1],
		    input.tm_year + 1900,
		    input.tm_mon + 1,
		    input.tm_mday,
		    input.tm_hour,
		    input.tm_min,
		    input.tm_sec);
		output = utc_to_local(&input);
		strftime(bufin, sizeof bufin, "%Y-%m-%d %H:%M:%S", &input);
		strftime(bufout, sizeof bufout, "%Y-%m-%d %H:%M:%S", output);
		printf("%s in UTC is %s in local time\n", bufin, bufout);

		if (strptime(argv[1], "%Y-%m-%d %H:%M:%S", &input) == NULL) {
			fprintf(stderr, "timetest: \"%s\" is not a valid time\n",
			    argv[1]);
			return 2;
		}
		input.tm_isdst = -1;
		strftime(bufin, sizeof bufin, "%Y-%m-%d %H:%M:%S", &input);
		printf("%s is converted to %04d-%02d-%02d %02d:%02d:%02d\n",
		    argv[1],
		    input.tm_year + 1900,
		    input.tm_mon + 1,
		    input.tm_mday,
		    input.tm_hour,
		    input.tm_min,
		    input.tm_sec);
		output = local_to_utc(&input);
		strftime(bufin, sizeof bufin, "%Y-%m-%d %H:%M:%S", &input);
		strftime(bufout, sizeof bufout, "%Y-%m-%d %H:%M:%S", output);
		printf("%s in local time is %s in UTC\n", bufin, bufout);

		return 0;
	}

produces the following output with the "posix" files, in my time zone:

	$ ./timetest "2016-12-31 23:59:60"
	2016-12-31 23:59:60 is converted to 2016-12-31 23:59:60
	2017-01-01 00:00:00 in UTC is 2016-12-31 16:00:00 in local time
	2016-12-31 23:59:60 is converted to 2016-12-31 23:59:60
	2017-01-01 00:00:00 in local time is 2017-01-01 08:00:00 in UTC

and the following output with the "right" files, in my time zone:

	$ TZ=right/America/Los_Angeles ./timetest "2016-12-31 23:59:60"
	2016-12-31 23:59:60 is converted to 2016-12-31 23:59:60
	2016-12-31 23:59:60 in UTC is 2016-12-31 15:59:60 in local time
	2016-12-31 23:59:60 is converted to 2016-12-31 23:59:60
	2017-01-01 00:00:00 in local time is 2017-01-01 08:00:00 in UTC

Note that either gmtime() and localtime(), if the "posix" files are being used, will "fix" the members of the input struct tm, but will *not* do so if the "right" files are being used.



More information about the tz mailing list