Time Matters

I had a total lightbulb moment last Friday night and finally arrived at a clever programming feat. And by the time I finished posting this entry, I refined it even more, trimming 5 lines of coding from an already sparse function.

Calendar Snapshot

The above snapshot is that of an application I developed for work, but always with the intention of eventually distributing it. It looks very simple: it’s an online group scheduler. It easily allows a group of people, regardless of their time zone, to see what other members of the group are doing. If I, as a group member, cannot schedule myself for a particular appointment, I can schedule a co-member instead. Pretty straight-forward so far, right?

What you don’t see in this screenshot is that I can change the day view to another time zone. So, say I’m trying to schedule an appointment with someone who is in British Columbia, and the appointment should be at 10:00 am BC time. All I need to do is first switch the time zone pull-down (which you don’t see here) to BC, then click on the plus sign in my column corresponding to 10:00 am BC time to schedule the appointment. Then, when I’m done and I switch back to my time zone in Québec, the appointment will show as being at 1:00 pm, as BC is 3 hours earlier than Québec.

The underlying concept is as follows: Each day is divided into 96 slices, each representing 15 minutes. For me in Québec, Slice 41 represents 1:00 pm but corresponds to 10:00 am in BC. The Eastern time zone is designated as Zone 3 while the Pacific time zone is Zone 6. Most time zones in the northern hemisphere observe Daylight Saving Time at the same time — in North America, Mexico, as usual, is the odd man out in that it starts DST later and ends it sooner, as happens in Europe — so even when the time changes, the 3-hour difference between Zone 3 and Zone 6 stands.

However, there are places like Saskatchewan, where DST is never observed, or the far eastern reaches of Québec’s north shore, which is in the Atlantic time zone (Zone 2 in my scheme) but, since it doesn’t observe DST, is the same time as the rest of Québec when DST is being observed — in other words, Zone 3 in my scheme. In the case of Saskatchewan, the pull-down changes automatically so that this province is listed with Manitoba in winter (Zone 4) but Alberta in summer (Zone 5). As this post from April 2003 attests, I’ve been struggling with the notion of time zones, time differences, and observance and non-observance of DST from many, many years. (In fact, what I wrote 8 years ago about Indiana is no longer true.)

While most of my application relies on PHP’s inherent time zone controls (e.g., for Montréal, putenv(“TZ=America/Montreal”)), each user’s profile also includes the zone number per my group scheduling scheme. And it seemed totally unacceptable — not sufficiently user-centric — to require, as does WordPress (the blogging software I use for aMMusing), those who live and work in one of the unusual time zones to manually switch when we go to and from DST.

To add to the complexity: Most countries in the southern hemisphere do not observe DST, but when they do, they do so in our winter months because it’s summer in their hemisphere. If only it were as simple as, “On the day when we go to DST, they get off DST,” then the programming challenge, while considerable, would be relatively simple. But it’s not that simple.

Take Brazil as an example. Despite its width, most of that country is in one time zone and it observes DST. Right now (February 6, 2011), the time difference between Montréal and Rio is 3 hours, as in, Rio is 3 hours ahead of Montréal. However, towards the end of February, Brazil will cease observing DST and we still won’t be observing it here, so the difference between Montréal and Rio will be 2 hours until mid-March when we will start observing DST, at which point the difference between Montréal and Rio will drop to only 1 hour. On October 16, while we will still be observing DST, Brazil will start observing it as well, so difference will rebecome 2 hours. And finally, when we stop observing DST on November 6, the Montréal/Rio difference will rebecome 3 hours.

Okay, if you followed me this far and think you have it all figured out, I’ll throw Uruguay at you to burst your little bubble. Uruguay is essentially the same time zone as most of Brazil, complete with observance of DST; however, that country’s transition dates are different than Brazil. Instead of ending DST in late February, Uruguay only ends it on March 13, which coincidentally is when we start observing it. And instead of starting it again like Brazil on October 16, Uruguay restarts it October 2 …like the parts of Australia that do observe DST — Australia, which of course, is more than half a day ahead of Uruguay.

So, my challenge was that comparing time differences against Coordinated Universal Time (UTC) was as helpful as tits on a bull. In places where DST is observed, the difference for them is always their basic offset from UTC, that is, X hours or X+1 hours. In other words, in Montréal, we’re either 5 hours behind UTC in winter (i.e., X=–5) or 4 hours behind UTC in summer (i.e., X+1=–4). But for that far northeastern reach of Québec I mentioned earlier, which never observes DST, X=–4 year round, just as for Saskatchewan, X=–6 throughout the year.

The thought of writing hundreds of lines of code upon looking up every exception and hope like hell none ever change brought me close to abandoning any hope of achieving a user-centric script. In fact, I found my first attempt at this fool’s errand didn’t work all the time; Saskatchewan always ended up as a Zone 5 instead of 4 in winter and 5 in summer. That was until it occurred to me that my mistake was to rely on UTC. Instead, I should arbitrarily pick what I consider a “normal” time zone — one in the northern hemisphere that observes DST — compare its offset on any given day against the “target” time zone (i.e., the one whose Zone number I’m trying to find), and adjust that Zone number accordingly.

The result: A 25-line function that I can call from anywhere in my suite of applications (including the group scheduler) and that works 100 per cent of the time! And to illustrate, I’m going to take Alberta (AB), Saskatchewan (SK), and Puerto Rico (PR) as examples of Zone whose number I’m trying to find.

— Regardless of its current offset to UTC, we know that Montréal (Mtl) is always Zone 3 in my calendar scheme.

Time Zone
Date Mtl
Feb 06/11 3
Mar 13/11 3

— Using this function like this, which returns the number of minutes offset between two time zones, start by calculating if Zone 3, synonymous with Montréal, is currently 4 or 5 hours behind UTC.

function get_timezone_offset($the_benchtime, $your_tz, $their_tz = null) {
if(
$their_tz === null) {
if(
!is_string($their_tz = date_default_timezone_get())) {
return
false;
}
}
if (
$the_benchtime == “”) {
$the_benchtime = “now”;
}
$origin_dtz = new DateTimeZone($their_tz);
$your_dtz = new DateTimeZone($your_tz);
$origin_dt = new DateTime($the_benchtime“, $origin_dtz);
$remote_dt = new DateTime($the_benchtime“, $your_dtz);
$offset = $origin_dtz->getOffset($origin_dt)$your_dtz->getOffset($remote_dt);
return
$offset;
}

Having determined that:

$whatdateandtime = “2011-02-06 00:00:00”;
$zone1 = “America/Montreal”;
$zone2 = “UTC”;

…calling the function is as simple as:

$TimeDifference = get_timezone_offset($whatdateandtime, $zone1, $zone2);

And since it gives the value in seconds, where 1 hour has 3600 seconds, convert to hours as follows:

$TimeDifference = $TimeDifference/3600;

So the result is:

Offset from UTC
Date Mtl
Feb 06/11 –5
Mar 13/11 –4

— Then, using the same function, calculate the number of hours the “target” zone (Alberta or Saskatchewan or Puerto Rico) is off compared to UTC.

Offset from UTC
Date Mtl AB SK PR
Feb 06/11 –5 –7 –6 –4
Mar 13/11 –4 –6 –6 –4

— Finally, calculate the target Zone number following this formula:

Base Zone Number — (Target OffsetBase Offset) = Target Zone Number

That formula is part of my “magic” function which refers to (i.e., uses) the function above to get the variable offset numbers.

And remember: Two negatives give one positive, so –7 — –5 is like saying –7 + 5, which is –2, and then 3 — –2 is like saying 3 + 2, which is 5.

Time Zone
Date Mtl AB SK PR
Feb 06/11 3 3 — (–7–5)
3 — –2 = Zone 5
3 — (–6–5)
3 — –1 = Zone 4
3 — (–4–5)
3 — 1 = Zone 2
Mar 13/11 3 3 — (–6–4)
3 — –2 = Zone 5
3 — (–6–4)
3 — –2 = Zone 5
3 — (–4–4)
3 — 0 = Zone 3

Invariably, the result is correct, per my time zone numbering scheme:

Time Zone
Date Mtl AB SK PR
Feb 06/11 3 5 4 2
Mar 13/11 3 5 5 3

Saskatchewan is indeed like Alberta in summer but like Manitoba in winter, and Puerto Rico is indeed like Montréal in summer but like Halifax in winter.

When I’m ready to distribute my script, the purchaser will have to choose one scheduling scheme from the following:

  1. The Americas
  2. Windy City to Kremlin (Americas East to Europe & Africa)
  3. Euroasia & Africa
  4. Asia & Oceania

Essentially, the east/west span is restricted to 10–11 hours. But the same function will always work, even if a scheme spans across the Greenwich line, with the only difference being the base city and its stable Zone number within that scheme:

  1. Montréal
  2. London
  3. London or Paris
  4. Some city in Russia whose DST resembles Europe’s

Or they could be based on Montréal, with only the stable Zone number changing to an improbable yet relational value within the scheme.

A spinoff of coming up with this function is that it brought to my attention that Newfoundland is not the only half-hour time zone in the Americas. Venezuela is the other, but it’s west of Newfoundland and, unlike Newfoundland, never observes DST. So, I was able to adjust the reference table for the Americas in my database and can now assert my application will always work. The cherry on the sundae would be to have a Spanish and Portuguese language file so that it could work in those languages in addition to the current English and French.