Wednesday, November 18, 2020

[wczxrjhn] Time zones in GNU date

We experiment with time zones the GNU date command in coreutils version 8.30-3+b1.  In particular, we experiment with various combinations of setting the TZ variable, both in the environment and specifying it in the --date input argument, and with appending a suffix that indicates a time zone, e.g., -5:00.  We experiment with how the command behaves on the leap second that happened at 2015-06-30 18:59:60-05:00 (CDT), because this (maybe) helps illuminate what is going on.

The testcase below, specifying both TZ and suffix correctly, shows normal behavior:

1
$ TZ=right/America/Chicago date --debug --date='TZ="right/America/Chicago" 2015-06-30T18:59:60-05:00' --iso=second
date: parsed datetime part: (Y-M-D) 2015-06-30 18:59:60 UTC-05
date: input timezone: parsed date/time string (-05)
date: using specified time as starting value: '18:59:60'
date: starting date/time: '(Y-M-D) 2015-06-30 18:59:60 TZ=-05'
date: '(Y-M-D) 2015-06-30 18:59:60 TZ=-05' = 1435708825 epoch-seconds
date: timezone: TZ="right/America/Chicago" environment value
date: final: 1435708825.000000000 (epoch-seconds)
date: final: (Y-M-D) 2015-06-30 23:59:60 (UTC)
date: final: (Y-M-D) 2015-06-30 18:59:60 (UTC-05)
2015-06-30T18:59:60-05:00

The testcase below, omitting TZ in input, shows normal behavior:

2
$ TZ=right/America/Chicago date --debug --date='2015-06-30T18:59:60-05:00' --iso=second
date: parsed datetime part: (Y-M-D) 2015-06-30 18:59:60 UTC-05
date: input timezone: parsed date/time string (-05)
date: using specified time as starting value: '18:59:60'
date: starting date/time: '(Y-M-D) 2015-06-30 18:59:60 TZ=-05'
date: '(Y-M-D) 2015-06-30 18:59:60 TZ=-05' = 1435708825 epoch-seconds
date: timezone: TZ="right/America/Chicago" environment value
date: final: 1435708825.000000000 (epoch-seconds)
date: final: (Y-M-D) 2015-06-30 23:59:60 (UTC)
date: final: (Y-M-D) 2015-06-30 18:59:60 (UTC-05)
2015-06-30T18:59:60-05:00

The testcase below, omitting the suffix but specifying TZ in input, shows normal behavior:

3
$ TZ=right/America/Chicago date --debug --date='TZ="right/America/Chicago" 2015-06-30T18:59:60' --iso=second
date: parsed datetime part: (Y-M-D) 2015-06-30 18:59:60
date: input timezone: TZ="right/America/Chicago" in date string
date: using specified time as starting value: '18:59:60'
date: starting date/time: '(Y-M-D) 2015-06-30 18:59:60'
date: '(Y-M-D) 2015-06-30 18:59:60' = 1435708825 epoch-seconds
date: timezone: TZ="right/America/Chicago" environment value
date: final: 1435708825.000000000 (epoch-seconds)
date: final: (Y-M-D) 2015-06-30 23:59:60 (UTC)
date: final: (Y-M-D) 2015-06-30 18:59:60 (UTC-05)
2015-06-30T18:59:60-05:00

The testcase below is accepted, but we feel it should have been rejected for two reasons:

  1. -05:01 is incompatible with America/Chicago.
  2. A leap second did not occur at 2015-07-01 00:00:59 (UTC)

4
$ TZ=right/America/Chicago date --debug --date='TZ="right/America/Chicago" 2015-06-30T18:59:60-05:01' --iso=second
date: parsed datetime part: (Y-M-D) 2015-06-30 18:59:60 UTC-05:01
date: input timezone: parsed date/time string (-05:01)
date: using specified time as starting value: '18:59:60'
date: starting date/time: '(Y-M-D) 2015-06-30 18:59:60 TZ=-05:01'
date: '(Y-M-D) 2015-06-30 18:59:60 TZ=-05:01' = 1435708885 epoch-seconds
date: timezone: TZ="right/America/Chicago" environment value
date: final: 1435708885.000000000 (epoch-seconds)
date: final: (Y-M-D) 2015-07-01 00:00:59 (UTC)
date: final: (Y-M-D) 2015-06-30 19:00:59 (UTC-05)
2015-06-30T19:00:59-05:00

The testcase below gets rejected as "incorrect timezone", but its rejection is at odds with the previous example being accepted.

5
$ TZ=right/America/Chicago date --debug --date='TZ="right/America/Denver" 2015-06-30T18:59:60-05:00' --iso=second
xdate: parsed datetime part: (Y-M-D) 2015-06-30 18:59:60 UTC-05
date: input timezone: parsed date/time string (-05)
date: using specified time as starting value: '18:59:60'
date: error: invalid date/time value:
date:     user provided time: '(Y-M-D) 2015-06-30 18:59:60 TZ=-05'
date:        normalized time: '(Y-M-D) 2015-06-30 19:00:00 TZ=-05'
date:                                             -- -- --
date:      possible reasons:
date:        numeric values overflow;
date:        incorrect timezone
date: invalid date 'TZ="right/America/Denver" 2015-06-30T18:59:60-05:00'

The testcase below shows the difference between a time zone suffix and the "relative items" time adjustment feature.

6
$ TZ=right/America/Chicago date --debug --date='TZ="right/America/Chicago" -5 hour -1 minute 2015-06-30T18:59:60' --iso=second
date: parsed relative part: -5 hour(s)
date: parsed relative part: -5 hour(s) -1 minutes
date: parsed datetime part: (Y-M-D) 2015-06-30 18:59:60
date: input timezone: TZ="right/America/Chicago" in date string
date: using specified time as starting value: '18:59:60'
date: starting date/time: '(Y-M-D) 2015-06-30 18:59:60'
date: '(Y-M-D) 2015-06-30 18:59:60' = 1435708825 epoch-seconds
date: after time adjustment (-5 hours, -1 minutes, +0 seconds, +0 ns),
date: new time = 1435690765 epoch-seconds
date: timezone: TZ="right/America/Chicago" environment value
date: final: 1435690765.000000000 (epoch-seconds)
date: final: (Y-M-D) 2015-06-30 18:59:00 (UTC)
date: final: (Y-M-D) 2015-06-30 13:59:00 (UTC-05)
2015-06-30T13:59:00-05:00

The testcase below is rejected, but we feel it should have been accepted because the date string unambiguously specifies a time zone.  We expect the answer "2015-06-30T17:59:60-06:00", that is, the Central time leap second translated to Mountain time.

7
$ TZ=right/America/Denver date --debug --date='2015-06-30T18:59:60-05:00' --iso=second
xdate: parsed datetime part: (Y-M-D) 2015-06-30 18:59:60 UTC-05
date: input timezone: parsed date/time string (-05)
date: using specified time as starting value: '18:59:60'
date: error: invalid date/time value:
date:     user provided time: '(Y-M-D) 2015-06-30 18:59:60 TZ=-05'
date:        normalized time: '(Y-M-D) 2015-06-30 19:00:00 TZ=-05'
date:                                             -- -- --
date:      possible reasons:
date:        numeric values overflow;
date:        incorrect timezone
date: invalid date '2015-06-30T18:59:60-05:00'

We do similar tests for 1 second later.  Nothing gets rejected.

8
$ TZ=right/America/Chicago date --debug --date='2015-06-30T19:00:00' --iso=second
date: parsed datetime part: (Y-M-D) 2015-06-30 19:00:00
date: input timezone: TZ="right/America/Chicago" environment value
date: using specified time as starting value: '19:00:00'
date: starting date/time: '(Y-M-D) 2015-06-30 19:00:00'
date: '(Y-M-D) 2015-06-30 19:00:00' = 1435708826 epoch-seconds
date: timezone: TZ="right/America/Chicago" environment value
date: final: 1435708826.000000000 (epoch-seconds)
date: final: (Y-M-D) 2015-07-01 00:00:00 (UTC)
date: final: (Y-M-D) 2015-06-30 19:00:00 (UTC-05)
2015-06-30T19:00:00-05:00

9
$ TZ=right/America/Chicago date --debug --date='TZ="right/America/Chicago" 2015-06-30T19:00:00' --iso=second
date: parsed datetime part: (Y-M-D) 2015-06-30 19:00:00
date: input timezone: TZ="right/America/Chicago" in date string
date: using specified time as starting value: '19:00:00'
date: starting date/time: '(Y-M-D) 2015-06-30 19:00:00'
date: '(Y-M-D) 2015-06-30 19:00:00' = 1435708826 epoch-seconds
date: timezone: TZ="right/America/Chicago" environment value
date: final: 1435708826.000000000 (epoch-seconds)
date: final: (Y-M-D) 2015-07-01 00:00:00 (UTC)
date: final: (Y-M-D) 2015-06-30 19:00:00 (UTC-05)
2015-06-30T19:00:00-05:00

10
$ TZ=right/America/Chicago date --debug --date='TZ="right/America/Chicago" 2015-06-30T19:00:00-05:00' --iso=second
date: parsed datetime part: (Y-M-D) 2015-06-30 19:00:00 UTC-05
date: input timezone: parsed date/time string (-05)
date: using specified time as starting value: '19:00:00'
date: starting date/time: '(Y-M-D) 2015-06-30 19:00:00 TZ=-05'
date: '(Y-M-D) 2015-06-30 19:00:00 TZ=-05' = 1435708826 epoch-seconds
date: timezone: TZ="right/America/Chicago" environment value
date: final: 1435708826.000000000 (epoch-seconds)
date: final: (Y-M-D) 2015-07-01 00:00:00 (UTC)
date: final: (Y-M-D) 2015-06-30 19:00:00 (UTC-05)
2015-06-30T19:00:00-05:00

11
$ TZ=right/America/Chicago date --debug --date='TZ="right/America/Chicago" 2015-06-30T19:00:00-05:01' --iso=second
date: parsed datetime part: (Y-M-D) 2015-06-30 19:00:00 UTC-05:01
date: input timezone: parsed date/time string (-05:01)
date: using specified time as starting value: '19:00:00'
date: starting date/time: '(Y-M-D) 2015-06-30 19:00:00 TZ=-05:01'
date: '(Y-M-D) 2015-06-30 19:00:00 TZ=-05:01' = 1435708886 epoch-seconds
date: timezone: TZ="right/America/Chicago" environment value
date: final: 1435708886.000000000 (epoch-seconds)
date: final: (Y-M-D) 2015-07-01 00:01:00 (UTC)
date: final: (Y-M-D) 2015-06-30 19:01:00 (UTC-05)
2015-06-30T19:01:00-05:00

12
$ TZ=right/America/Chicago date --debug --date='TZ="right/America/Denver" 2015-06-30T19:00:00-05:00' --iso=second
date: parsed datetime part: (Y-M-D) 2015-06-30 19:00:00 UTC-05
date: input timezone: parsed date/time string (-05)
date: using specified time as starting value: '19:00:00'
date: starting date/time: '(Y-M-D) 2015-06-30 19:00:00 TZ=-05'
date: '(Y-M-D) 2015-06-30 19:00:00 TZ=-05' = 1435708826 epoch-seconds
date: timezone: TZ="right/America/Denver" environment value
date: final: 1435708826.000000000 (epoch-seconds)
date: final: (Y-M-D) 2015-07-01 00:00:00 (UTC)
date: final: (Y-M-D) 2015-06-30 18:00:00 (UTC-06)
2015-06-30T19:00:00-05:00

13
$ TZ=right/America/Denver date --debug --date='2015-06-30T19:00:00-05:00' --iso=second
date: parsed datetime part: (Y-M-D) 2015-06-30 19:00:00 UTC-05
date: input timezone: parsed date/time string (-05)
date: using specified time as starting value: '19:00:00'
date: starting date/time: '(Y-M-D) 2015-06-30 19:00:00 TZ=-05'
date: '(Y-M-D) 2015-06-30 19:00:00 TZ=-05' = 1435708826 epoch-seconds
date: timezone: TZ="right/America/Denver" environment value
date: final: 1435708826.000000000 (epoch-seconds)
date: final: (Y-M-D) 2015-07-01 00:00:00 (UTC)
date: final: (Y-M-D) 2015-06-30 18:00:00 (UTC-06)
2015-06-30T18:00:00-06:00

Contradiction between input TZ and suffix can cause weird results.  (Input TZ, in the --date argument, and output TZ, in the environment variable, are different, but that is normal operation: date can be used to convert times between time zones.)

14
$ TZ=right/America/Chicago date --debug --date='TZ="right/America/New_York" 2015-06-30T19:00:00-05:00' --iso=second
date: parsed datetime part: (Y-M-D) 2015-06-30 19:00:00 UTC-05
date: input timezone: parsed date/time string (-05)
date: using specified time as starting value: '19:00:00'
date: starting date/time: '(Y-M-D) 2015-06-30 19:00:00 TZ=-05'
date: '(Y-M-D) 2015-06-30 19:00:00 TZ=-05' = 1435708825 epoch-seconds
date: timezone: TZ="right/America/New_York" environment value
date: final: 1435708825.000000000 (epoch-seconds)
date: final: (Y-M-D) 2015-06-30 23:59:60 (UTC)
date: final: (Y-M-D) 2015-06-30 19:59:60 (UTC-04)
2015-06-30T18:59:60-05:00

Yet another example of contradiction between input TZ and suffix:

15
$ TZ=right/America/New_York date --debug --date='2015-06-30T19:00:00-05:00' --iso=second
date: parsed datetime part: (Y-M-D) 2015-06-30 19:00:00 UTC-05
date: input timezone: parsed date/time string (-05)
date: using specified time as starting value: '19:00:00'
date: starting date/time: '(Y-M-D) 2015-06-30 19:00:00 TZ=-05'
date: '(Y-M-D) 2015-06-30 19:00:00 TZ=-05' = 1435708825 epoch-seconds
date: timezone: TZ="right/America/New_York" environment value
date: final: 1435708825.000000000 (epoch-seconds)
date: final: (Y-M-D) 2015-06-30 23:59:60 (UTC)
date: final: (Y-M-D) 2015-06-30 19:59:60 (UTC-04)
2015-06-30T19:59:60-04:00

We hesitate to call these behaviors bugs because it is a complicated situation.  Determining whether a suffix is incompatible with TZ seems formidable, because a given time zone (e.g., America/Chicago) can have many different suffixes depending on the date because of Daylight Saving Time and history.

The software might also be supporting specifying unofficial time zones not in the tzdata database.  For such a situation, the user might be able to specify TZ as a first approximation then apply a suffix to modify it.

That said, what is the best way to convert to and from a user-defined time zone, not requiring superuser privileges?  Can such a user-defined time zone then be used by all user applications that work with time, for example by setting the TZ variable in ~/.bashrc to point to a user tzfile?  We imagine users wanting to use a time zone that the local government or the maintainers of tzdata oppose.

No comments :