Sunday, May 17, 2015

Custom DST on Linux via the POSIX TZ environment variable

Fig 1: Geographical regions currently using DST (in 2013), in blue and orange.
Image credit:   TimeZonesBoy (Own work) [CC BY-SA 3.0 (http://creativecommons.org/licenses/by-sa/3.0)], via Wikimedia Commons


If you are reading this article, you are probably writing software for, or porting Linux on devices that other people will use.  People who use Linux on their general purpose Desktop machines have no interest in fiddling with custom timezones or custom DST.  Doing so will upset your web browser, among other things.

But for those of us who have to worry about custom timezones, here is a 'getting started' guide that may help.

First of all, disambiguation of DST terminology should help a lot (at least it did for me).  Below is a diagram of what happens/happened to the clocks in Melbourne, Australia, for the year 2015.

Fig 2: DST time in Melbourne, AU, 2015.

The blue part represents what we call "standard time" or STD; or in other words, where the clocks would be all-year-round if DST didn't exist (which it arguably shouldn't).  The yellow part represents "daylight savings time" or DST.

Different geographical locations have different names for their standard time and daylight savings time.  Stockholm (Sweden) is using so-called "CEST" (Central European Summer Time) during DST, and "CET" (Central European Time) during its STD period.  Important note: "Summer Time" is just another name for DST used in some European countries.

Yes, your head may be a bit confused right now.  It can be for many people.  For example;
  • "AEST" is "Australian Eastern Standard Time"; and
  • "CEST" is "Central European Summer Time".
One is referring to DST and the other one is referring to STD.  Careful about that.

So what happens when one crosses over the border from blue part to the yellow part (DST) shown above?  Well, by official definition, a geographical location always advances the clocks when crossing the border into DST.  That is, we lose sleep when it starts, and gain sleep when it ends.  And it's usually an hour of sleep that we lose/gain.  But using the TZ variable, it can be any number of minutes you desire; not necessarily 60 minutes.

Now to the 'nitty-gritty' of the POSIX 'TZ' variable.  This is how you can set set your own custom version of what you see in figure 2.  You can choose when the 'blue' and 'yellow' parts start and end, and how much time is lost or gained at the crossing of each border.
The POSIX 'TZ' variable is defined as so:
std offset dst [offset],start[/time],end[/time]
Fig 3: The POSIX 'TZ' variable.

Here it is.  The golden TZ variable.  You're going to want to refer back to it many times; here, or on the GNU manual page or elsewhere.  Rather than explain what each part of figure 3 means, maybe jumping straight into some examples is clearer:


Example 1:
TZ=CST6CDT,M3.2.0/2:00:00,M11.1.0/2:00:00
In English:

  • This would jump into DST (and lose an hour of sleeep) on the 2nd Sunday of the third month (March) at 2:00:00.  Eg. For 2015, we jump into DST on 8 MAR 2015 2:00:00.
  • This would jump out of DST (daylight savings time) on the 1st Sunday of the eleventh month (November) at 2:00:00 and we get an hour of sleep back.  Eg. For 2015, jump out of DST on 1 NOV 2015 2:00:00.
  • In this instance, the option DST [offset] omitted, and so defaults to an hour.


Example 2:
TZ=NZST-12:00:00NZDT-13:00:00,M10.1.0,M3.3.0
In English:
  • In this example, we have omitted the start and end [/time]'s, so it is assumed to be 2:00:00.
  • This would jump into DST (and lose an hour of sleep) on the 1st Sunday of October 2:00:00.   Eg. For 2015, we jump into DST on 4 OCT 2015 2:00:00.
  • This would jump out of DST (and gain and hour of sleep) on the 3rd Sunday of March 2:00:00.  Eg. For 2016, we jump out of DST on 20 MAR 2016 2:00:00.
  • Be careful if you were to manually set the time using the "date -s" command in the time period between 1:00:00 and 2:00:00 on 20 MAR 2016, as this is an "ambiguous" time period (it is experienced twice; once in the 'lead up' to exiting DST, and once again, when the clocks are set back an hour).
Example 3:
TZ=ABCST4ABCDT,J5/0,J100/22
In English:
  • This example uses an entirely fictitious "ABC" standard and daylight savings time. 
  • In this example, we aren't using the 'Mm.w.d' format shown in the above two examples.  We are using "Julian Days" from 0 to 365.
  • In this example we will use 2015 as the example year.
  • We jump into DST (lose an hour of sleep) at the stroke of midnight on the 4 JAN 2015.  To be clearer, we jump directly from 4 JAN 2015 23:59:59 to 5 JAN 2015 01:00:00.
  • We jump out of DST (gain an hour of sleep) on the 22nd hour of the 10th April 2015, i.e. 10 APR 2015 22:00:00.



Some extra things worth mentioning:

  1. Environment variable scope is an issue when using the 'TZ' variable.  Exporting the TZ variable will affect the current process; and any processes spawned from it (child processes) will get a copy.  Parent processes will not be affected.
  2. There's more than one way to cook an egg.  If the TZ variable is not suitable for you, you may also consider a timezone database (zic input files), and the zic compiler.
    This page is the best resource I can find about this method.  It includes good examples.