reptile7's JavaScript blog
Friday, February 29, 2008
 
Around the World in 80 Milliseconds, Act IV
Blog Entry #105

Not-on-the-hour time zones

In his discussion of the "World Clock" script of HTML Goodies' JavaScript Script Tips #87-90, Joe makes no mention of the fact that some of the world's time zones are 'not on the hour' - by this I mean that the offsets in minutes of these time zones from UTC are not multiples of 60 - which is rather odd given that one of the script's time/date display locations is New Delhi, which observes UTC+5:30 throughout the year. Wikipedia's "List of time zones" entry lists thirteen not-on-the-hour time zones: three UTC-x:30 time zones, seven UTC+x:30 time zones, and three UTC+x:45 time zones. The script's checkDateTime( ) function accommodates the various UTC±x:30 time zones via its half variable:

var half = ourDifference % 60;
ourDifference = Math.round(ourDifference / 60);
hour = hour - ourDifference;
if (half == -30 || half == 30) minute += 30;

For an initial Central Time time/date display, a UTC-9:30 user location (our Point A for the previous entry's deconstruction) gives a half value of -30, whereas any other UTC±x:30 user location will give a half value of 30. For all of the UTC±x:30 locations, the user's minute (new Date( ).getMinutes( )) value is increased by 30 so as to either
(a) push the Central Time minute further away from the user minute, or
(b) bring the Central Time minute closer to the user minute,
depending on whether the Math.round( ) command gives an apparent user/Central Time time difference (ourDifference) that is, respectively, smaller (as in the UTC-9:30 case) or larger (as in the other UTC±x:30 cases) than the actual user/Central Time time difference.

Your assignment: Work through the script from a starting location of Newfoundland, which observes UTC-3:30 during standard time, and a login time/date of your choice.

But what about the folks who live in UTC+x:45 time zones - in Nepal, on the Chatham Islands, and in the southeastern corner of Western Australia? These users will see a Central Time time that is 15 minutes earlier (in the past) than it should be. To see why, let's run through a relevant mini-deconstruction from a starting location of Nepal, which observes UTC+5:45 throughout the year.

Let's suppose that you are about to scale Mount Everest, and that shortly before you and your climbing party depart from Kathmandu and begin your ascent, you decide to do a bit of Web surfing. As a matter of course, your online session includes a visit to the Script Tips #87-90 Script demo page, which you access at exactly 6:30AM on 24 February. What happens? We turn to the end of the checkDateTime( ) DST block:

yourOffset = new Date( ).getTimezoneOffset( );
ourDifference = gmtOffset - yourOffset;

For Nepal, new Date( ).getTimezoneOffset( ) returns -345, which is assigned to yourOffset. Subsequently, yourOffset is subtracted from gmtOffset, 360 as set at the top of the script element, to give 705, which is assigned to ourDifference.

var half = ourDifference % 60;

705 % 60 gives 45, which is assigned to half.

ourDifference = Math.round(ourDifference / 60);

705 / 60 gives 11.75, which is rounded up by the Math.round( ) function to give 12, which is assigned to ourDifference.

hour = hour - ourDifference;

ourDifference is subtracted from the current Nepal hour, 6, to give -6, which is assigned to hour.

if (half == -30 || half == 30) minute += 30;

Uh-oh! The if condition returns false, so the browser moves to...

if (minute > 59) { minute -= 60; hour++; } // Condition returns false
if (minute < 0) { minute += 60; hour--; } // Condition returns false
if (hour > 23) { hour -= 24; date += 1; } // Condition returns false
if (hour < 0) { hour += 24; date -= 1; } // Condition returns true

The last of these conditionals increases hour to 18 and decrements the current Nepal date, 24, to 23. The time/date in New Orleans should therefore be 18:30 (6:30PM) on 23 February given our initial conditions, right?

Not quite. During standard time, New Orleans is 11 hours and 45 minutes 'behind' Nepal, so the time/date in New Orleans is actually 18:45 (6:45PM) on 23 February. As for the UTC±x:30 time zones, to obtain the correct New Orleans time in this case we'll need to adjust the user minute value. The short-term fix for this situation is to add the following conditional to the checkDateTime( ) function:

if (half == 45) minute += 15;

In effect, this conditional corrects for the above Math.round( ) command, which gives an apparent Nepal/New Orleans time difference (12 hours) that is larger than the actual Nepal/New Orleans time difference, by bringing the time in New Orleans 15 minutes closer to the time in Nepal.

The preceding conditional would be inadequate for 'going the other way', however. Let's suppose that the script's location button table has a Kathmandu button that triggers a function that feeds to checkDateTime( ) a gmtOffset value of -345, and that I in New Orleans am the user. Regarding the mini-deconstruction above, here are the relevant returns upon clicking the Kathmandu button at 18:45 on 23 February:
(1) yourOffset will be 360.
(2) The initial ourDifference value will be -705.
(3) half will be -45.
(4) -705 / 60 gives -11.75, which is rounded up by Math.round( ) to give -11, which is assigned to ourDifference.
(5) hour - ourDifference gives 29, which is assigned to hour.
(6) The if (hour > 23) { hour -= 24; date += 1; } conditional decreases hour to 5 and increments date to 24.

The time/date in Nepal would therefore seem to be 5:45AM on 24 February given our initial conditions; however, we know from the Nepal-to-New Orleans case that the time/date in Nepal is actually 6:30AM on 24 February. Note that in going from Nepal to New Orleans, the Math.round( ) command adds 15 minutes (0.25 hours) to the actual Nepal/New Orleans time difference, whereas in going from New Orleans to Nepal, the Math.round( ) command subtracts 45 minutes (0.75 hours) from the actual Nepal/New Orleans time difference. In going from New Orleans to Nepal, the following conditional will correct for the Math.round( ) command by pushing the Nepal minute 45 minutes 'into the future':

if (half == -45) minute += 45;

Other UTC+x:45 situations are possible. Let's suppose that you are a user in Japan, for which yourOffset would be -540 (UTC+9), and that you click the Kathmandu button described above. It is left to you to verify that in this case half will be 15 and that a

if (half == 15) minute -= 15;

conditional will be needed to correct for the Math.round( ) command.

What a nuisance all of this is, huh? Fortunately, by replacing the Math.round( ) function with the parseInt( ) function, we can deal with the UTC+x:45 time zones and also the UTC±x:30 time zones via a single minute-adjusting conditional, as follows:

yourOffset = new Date( ).getTimezoneOffset( );
ourDifference = gmtOffset - yourOffset;
var half = ourDifference % 60;
ourDifference = parseInt(ourDifference / 60);
hour = hour - ourDifference;
if (half != 0) minute -= half;
if (minute > 59) { minute -= 60; hour++; }
if (minute < 0) { minute += 60; hour--; } /* This last conditional was unnecessary in the original script, but you'll need it here. */

The above code successfully converts the user's minute to the Point B minute for half values of 30, -30, 45, -45, 15, and -15. For all of these cases, the parseInt( ) command outputs an apparent user/Point B time difference that is smaller than the actual user/Point B time difference; the if (half != 0) minute -= half conditional then pushes the Point B minute into the future or into the past, as appropriate*.

*Generally, the negative half values (-30, -45, -15) apply to not-on-the-hour time differences for which Point B is to the east of the user, requiring the Point B minute to be pushed into the future, whereas the positive half values (30, 45, 15) apply to not-on-the-hour time differences for which Point B is to the west of the user, requiring the Point B minute to be pushed into the past. Apparent exceptions to this pattern arise when we go across the International Date Line. Let's suppose that the script's location button table has a Marquesas Islands button that triggers a function that feeds to checkDateTime( ) a gmtOffset value of 570, and that you are a user in the Chatham Islands, which observes UTC+12:45 during standard time and is a Point A location 'to the west' of the Marquesas Islands. What happens, deconstruction-wise, when you click the Marquesas Islands button? I encourage you to work through this case.

Let's go to Hong Kong

We have yet to discuss the script element code associated with the location buttons on the demo page - let's do that now, shall we?

Let's say that you are a user in Philadelphia, which observes UTC-5 during standard time, and that upon arriving at the demo page you click the Hong Kong button

<input type="button" name="reset" value="Hong Kong" onclick="checkHK( );" />

at exactly 11AM on 26 February; your click triggers the checkHK( ) function.

The checkHK( ) function is one of nine location-specific functions that precede the checkDateTime( ) function in the script element and that provide to checkDateTime( ) gmtOffset and zone values appropriate for a given display location. Here it is:

function checkHK( ) {
clearTimeout(checkDateTime);
gmtOffset = HK + adjust;
zone = " Hong Kong";
checkDateTime( ); }

Comments

(1) The clearTimeout(checkDateTime) command can be removed; there's no need to clear the setTimeout("checkDateTime( );", 900) timeout for the recursive function call that updates the clock display. And the clearTimeout( )/setTimeout( ) syntax is wrong anyway; if we did want to clear the timeout, we would variabilize the timeout

myTimeout = window.setTimeout("checkDateTime( );", 900);

and then plug the timeout variable into the clearTimeout( ) command:

window.clearTimeout(myTimeout);

(2) Hong Kong (indeed, all of China) observes UTC+8 throughout the year. Accordingly, -480, Hong Kong's new Date( ).getTimezoneOffset( ) return, is assigned to the variable HK at the top of the script element. Because Hong Kong does not observe daylight-saving time, HK should not be 'adjusted' by the adjust variable and should be assigned by itself to gmtOffset, i.e., gmtOffset = HK.

(3) For the demo page, Joe prefaces the zone Hong Kong string with six space characters, and not with one space character, in an attempt to approximately center it in the locationx box:

zone = "      Hong Kong";

To the best of my knowledge, a string cannot be centered in a text field via CSS.

On its last command line, checkHK( ) calls the checkDateTime( ) function. You can run through another deconstruction if you like, although it is simple enough to count forward 13 hours to determine that the time/date in Hong Kong will be 0:00 (12AM) on 27 February given our initial conditions.

The demo page's other location buttons work the same way; for example, the London button triggers a checkLD( ) function

function checkLD( ) {
clearTimeout(checkDateTime);
gmtOffset = LD + adjust;
zone = " London(GMT)";
checkDateTime( ); }

that (a) picks up a globally assigned LD = 0 getTimezoneOffset( ) UTC offset, (b) adjusts the offset for DST (London does observe DST, but as noted two entries ago, adjust should be subtracted from (not added to) LD), (c) sets a new zone location, and (d) calls checkDateTime( ), which in turn acts on the checkLD( ) gmtOffset and zone values. You get the idea.

In the following entry, we'll wrap up our discussion of the Script Tips #87-90 Script with a look at some other code possibilities and also a demo featuring an expanded location button table.

reptile7

Tuesday, February 19, 2008
 
On a Ship to the Crescent City
Blog Entry #104

We continue today our analysis of the "World Clock" script of HTML Goodies' JavaScript Script Tips #87-90. Upon accessing the script's demo page, a user anywhere in the world* will see the current time and date for the North American Central Time Zone in the face text field below the "World Clock" heading at the top of the page. In this post, we'll discuss how the script goes from 'Point A', the user's current time and date, to 'Point B', the Central Time time and date.
(*This isn't quite true; as originally written, the script stiffs those people who live in UTC+x:45 time zones - but we'll get to you guys later.)

Let's suppose that you are vacationing in the Marquesas Islands, which are located in the UTC-9:30 time zone and do not observe daylight-saving time (DST) - what better place for a bit of JavaScript study, eh? You fire up your laptop and surf to the Script Tips #87-90 Script demo page, accessing it at exactly 3:45PM (15:45).

Our deconstruction begins at the top of the script's script element:

<script type="text/javascript"> <!--
// var adjust = 0;
var gmtOffset = 360;
var zone = " New Orleans";

The variable gmtOffset is declared and given an initial value of 360, the time difference in minutes between Central Standard Time (UTC-6) and UTC/GMT. The variable zone is declared and given an initial value of  New Orleans. (For this post, we will ignore the highly problematic adjust DST adjustment.)

When the demo page document has loaded, the body element's onload="checkDateTime( );" attribute calls the script element's checkDateTime( ) function. As noted in the previous two entries, the checkDateTime( ) function initially
(a) variabilizes most of the parts of a new Date( ) return, and then
(b) attempts to determine if the user is on DST.
We've already covered this code, so let's move to the end of checkDateTime( )'s DST block:

yourOffset = (new Date( )).getTimezoneOffset( );
// yourOffset = yourOffset + adjust;

For the Marquesas Islands, new Date( ).getTimezoneOffset( ) returns 570, which is assigned to the variable yourOffset.

ourDifference = eval(gmtOffset - yourOffset);

yourOffset is subtracted from gmtOffset to give -210, which is assigned to the variable ourDifference. Regarding the -210 value, the 210 part is the time difference in minutes between New Orleans (on standard time) and the Marquesas Islands, whereas the - part indicates that the Marquesas Islands are 'further behind' UTC than is New Orleans. There's no need to use the eval( ) function to carry out the subtraction: gmtOffset and yourOffset are numbers, not strings, and their difference is also a number (even if gmtOffset and/or yourOffset were strings, JavaScript would automatically convert them to numbers for the gmtOffset - yourOffset expression). Indeed, Mozilla specifically states, Do not call eval( ) to evaluate an arithmetic expression; JavaScript evaluates arithmetic expressions automatically. These eval( ) considerations actually apply to all of the script element's eval( ) uses, which are unnecessary without exception.

Next up is a statement that indirectly flags UTC+x:30 and UTC-x:30 time zones:

var half = eval(ourDifference % 60);

-210 % 60 gives -30, which is assigned to the variable half; half would be 0 for the on-the-hour UTC+x and UTC-x time zones.
(FYI: For an a % b = c modulo operation, if the dividend a is negative, then the remainder c will also be negative, regardless of whether the divisor b is positive or negative.)

ourDifference = Math.round(ourDifference / 60);

-210 / 60 gives -3.5, which is rounded up by Math.round( ) to give -3, which is assigned to ourDifference.

hour = eval(hour - ourDifference);

If the user's current hour (today.getHours( )) value is 15, then hour - -3 gives 18, which is assigned to hour and is not quite the current hour in New Orleans, as we'll see shortly.

var m = new Array("mm", "Jan.", "Feb.", "Mar.", "Apr.", "May", "Jun.", "Jul.", "Aug.", "Sept.", "Oct.", "Nov.", "Dec.");

An array of month abbreviation strings - take the periods out if you like - is created and assigned to the variable m. Note that m has a 'dummy' zeroth element, mm, so that the month abbreviations have 'normal' indexes: Jan. is m[1], Feb. is m[2], etc.

We next set up a variable leap for checking later whether the current year is a leap year:

var leap = eval(year % 4);

If leap is 0, then year is almost always a leap year. You may know that years that are divisible by 100 but not divisible by 400 are not leap years, e.g., whereas 2000 was a leap year, 1900 was not a leap year. None of us know if JavaScript will still be in use in 2100, which will not be a leap year but will be flagged as a leap year by the above statement, and I can see how you might not care about correcting for this situation, but for the perfectionists out there, here's a more precise algorithm that you can use:

if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) var leap = 0;
else leap = 1;

Two if conditionals now give the current New Orleans hour and minute:

if ((half == -30) || (half == 30)) minute += 30;
if (minute > 59) minute -= 60, hour++;
// if (minute < 0) minute += 60, hour--; /* As originally written, the script does not need this conditional, but we'll make use of it later. */

(The syntax of the second if conditional would seem to be non-standard - ordinarily, the two assignment statements would be separated by a semicolon and surrounded by braces - but perhaps this formulation is a legitimate application of the JavaScript comma operator. Moreover, this isn't the first time we've seen colinear assignment statements delimited with commas and not semicolons - see the parser( ) function in the guitar chord chart script of Script Tips #56-59.)

(1) Because half is -30, the first of these conditionals increases minute, 45, to 75.
(2) The second of these conditionals then
(a) decreases minute to 15, and
(b) increments hour to 19.
And the time in New Orleans is indeed 19:15 (7:15PM) if we access the script's demo page in the Marquesas Islands at 15:45.

A time change will often require us to change the current date. Sticking with the Marquesas Islands, suppose we had surfed to the script's demo page at 9:45PM (21:45) on 15 February. The following conditional comes into play:

if (hour > 23) hour -= 24, date += 1;

After the starting hour is increased to 25 and minute is adjusted to 15 via the commands above, this conditional decreases hour to 1 and increments date to 16, and the time/date in New Orleans would therefore be 1:15 (AM) on 16 February given our initial conditions.

The checkDateTime( ) function also has a corresponding

if (hour < 0) hour += 24, date -= 1;

conditional that 'goes backward' in this respect. Suppose now that, for whatever reason, you're in Iceland, which observes UTC throughout the year, and you access the script's demo page at 4AM on 15 February. In this case, yourOffset is 0 and the starting hour will be decreased by ourDifference (6) to -2; minute stays at 0 because half is 0. The preceding conditional then increases hour to 22 and decrements date to 14, and the time/date in New Orleans would be 22:00 (10PM) on 14 February given our initial conditions.

On occasion, a time change will require us to change the current month or even the current year. For the UTC-12 to UTC-7 time zones to the west of Central Time, the following five script conditionals will at the end of a month take us forward into a new month:

if (((month == 4) || (month == 6) || (month == 9) || (month == 11)) && (date == 31))
date = 1, month += 1;

Returning to the Marquesas Islands, let's say we surf to the script's demo page at 9:45PM on 30 April. The starting date will be incremented to 31 as described earlier and then this conditional sets date to 1 and increments month to 5 (May). A corresponding 30 June, 30 September, or 30 November login date would be converted to 1 July, 1 October, or 1 December, respectively. Analogously:

if (((month == 2) && (date > 28)) && (leap != 0)) date = 1, month += 1;
// A 28 February login date in a non-leap year would be converted to 1 March.

if ((month == 2) && (date > 29)) date = 1, month += 1;
// A 29 February login date in a leap year would also be converted to 1 March.

if ((date == 32) && (month == 12)) month = m[1], date = 1, year += 1;
// A 31 December 2008 login date would be converted to 1 January 2009.

if (date == 32) date = 1, month += 1;
/* This catchall converts 31 January to 1 February, 31 March to 1 April, 31 May to 1 June, 31 July to 1 August, 31 August to 1 September, and 31 October to 1 November. */

And yes, the checkDateTime( ) function has another five conditionals that, for the UTC-5 to UTC+14 time zones to the east of Central Time, will take us backward into the prior month if need be:

if ((date < 1) && (month == 1)) month = m[12], date = 31, year -= 1;
// This line would convert 1 January 2009 to 31 December 2008.

if (date < 1) date = 31, month -= 1;
/* Pushes back all non-January months; handles 1 February to 31 January, 1 April to 31 March, 1 June to 31 May, 1 August to 31 July, 1 September to 31 August, and 1 November to 31 October; however, it also converts 1 March to a '31 February', 1 May to a '31 April', 1 July to a '31 June', 1 October to a '31 September', and 1 December to a '31 November' */

if (((month == 4) || (month == 6) || (month == 9) || (month == 11)) && (date == 31)) date = 30;
/* For converting 1 May (in practice, '31 April') to 30 April, 1 July ('31 June') to 30 June, 1 October ('31 September') to 30 September, and 1 December ('31 November') to 30 November */

if ((month == 2) && (date > 28)) date = 29;
// For converting 1 March ('31 February') to 29 February in a leap year

if (((month == 2) && (date > 28)) && (leap != 0)) date = 28;
/* For converting 1 March ('29 February' per the prior conditional) to 28 February in a non-leap year */

Next, a for loop maps the month value onto its corresponding string in the m array:

for (i = 1; i < 13; i++) {
if (month == i) { month = m[i]; break; } }

Let's return to our 15 February login date, for which month (today.getMonth( ) + 1) is 2. In the loop's second iteration (i = 2), the if condition returns true, so m[2], Feb., is assigned to month and then the loop is terminated by a break statement, which can be removed because the if condition will return false for all other values of i.

Use of a loop here is overkill, however. If we recast the above 31 December to/from 1 January conditionals** as

if (date == 32 && month == 12) { month = 1; date = 1; year += 1; } // and
if (date < 1 && month == 1) { month = 12; date = 31; year -= 1; }

then the loop can be replaced with:

month = m[month];

(**The original conditionals directly assign to month Jan. and Dec. (not 1 and 12), respectively; for these month values, the loop's if condition returns false for all twelve iterations.)

We're at long last ready to cobble together the Central Time time/date string that will be displayed in the face field; this string is represented in the checkDateTime( ) function by the variable dateTime.

var dateTime = hour;

dateTime is declared and given the current Central Time hour value.

dateTime = ((dateTime < 10) ? "0" : "") + dateTime;
dateTime = " " + dateTime;

The first of these statements prepends a 0 tens-place digit to dateTime/hour values that are less than 10 (nothing (an empty string) is prepended to them if they're greater than or equal to 10); we previously discussed the ?: conditional operator in the "clock( ) function" sub-section of Blog Entry #101. Now a two-digit string literal, dateTime is prepended with a space character by the second of these statements.

dateTime += ((minute < 10) ? ":0" : ":") + minute;
dateTime += ((second < 10) ? ":0" : ":") + second;

We next append to dateTime
(a) the clock's hour:minute colon and
(b) the current Central Time minute for minute values greater than or equal to 10; for minute values less than 10, a 0 tens-place digit is also added.
The clock's minute:second colon and the current Central Time second are tacked on in the same way; a 0 tens-place digit is again added for second values less than 10.

dateTime += " " + month + " " + date + ", " + year;
document.clock.face.value = dateTime;
document.clock.locationx.value = zone;

With the hour:minute:second part of the clock now set, we append to dateTime (a) a space, (b) the current Central Time month, (c) another space, (d) the current Central Time date, (e) a two-character comma-space string, and (f) the current Central Time year. The completed dateTime string - e.g.,  19:15:00 Feb. 15, 2008 for our initial Marquesas Islands login - is then assigned to the value value of the face field. Subsequently, the zone location string, set to  New Orleans at the beginning of the post, is assigned to the value value of the locationx ("Current Time Zone") field.

Finally, a setTimeout( ) command recursively calls the checkDateTime( ) function after a 900-millisecond delay, updating the clock display:

window.setTimeout("checkDateTime( );", 900);

Now, what happens when we click the location buttons in the table on the script's demo page? And what about those script users in UTC+x:45 time zones, huh? We'll thrash out these questions in our next episode.

reptile7

Saturday, February 09, 2008
 
Shifting Light from Morning to Afternoon
Blog Entry #103

We return now to our deconstruction of the "World Clock" script of HTML Goodies' JavaScript Script Tips #87-90. We pick up the conversation at the checkDateTime( ) function's daylight-saving time (DST) block:

/* This next tidbit gets the last Saturday in the month of Oct, for daylight-saving time purposes */
var lastSat;
lastSat = date - (day + 1);
while (lastSat < 32) {
lastSat += 7; }
if (lastSat > 31) lastSat += -7;

// This bit grabs the first Saturday in April for the start of daylight time
var firstSat;
firstSat = date - (day + 1);
while (firstSat > 0) {
firstSat += -7; }
if (firstSat < 1) firstSat += 7;

// Adjust for Windows95 daylight-saving time changes
if ((((month == 4) && (date >= firstSat)) || month > 4) && (month < 11 || ((month == 10) && day <= lastSat))) { adjust += 60; }

yourOffset = (new Date( )).getTimezoneOffset( );
yourOffset = yourOffset + adjust;

References
(1) date is the new Date( ).getDate( ) return
(2) day is the new Date( ).getDay( ) return
(3) The while statement
(4) Comparison operators (<, >, ==, >=, <=)
(5) Assignment operators (=, +=)
(6) Logical operators (&&, ||)

Validation note: Scripts containing one or more < and/or & characters should be externalized for XHTML-compliance.

The Script Tips #87-90 Script was written in the 1987-2006 period during which DST in the United States began on the first Sunday of April and ended on the last Sunday of October. Today, however, DST in the U.S. runs from the second Sunday of March to the first Sunday of November. (I should mention at this point that Hawaii and most of Arizona do not observe DST.) Accordingly, we'll first go over the script's DST code and then I'll give you my own code that brings it up to date.

The above code's first "tidbit" is meant to determine the date of the last Saturday in October for the current year, and it would do that if the current month were October, but in practice this code block determines the date of the last Saturday of the current month regardless of what month it is, at least for those months that have 31 days; this date will be assigned to the variable lastSat. Here's what happens:

var lastSat;
lastSat = date - (day + 1);

We first declare lastSat. Subsequently, a date - (day + 1) expression determines the date of the Saturday prior to the current date; this Saturday date is assigned to lastSat. For example, I'm writing today on 3 February 2008, a Sunday whose day return is 0 and whose prior Saturday was 3 - (0 + 1) = 2 February (yesterday).
(It might not be intuitively obvious to you that the date - (day + 1) value would necessarily correspond to a Saturday - it wasn't to me initially - but if you substitute various date/day values into this expression, you'll see that it checks out.)

while (lastSat < 32) lastSat += 7;

A while loop adds multiples of 7 to lastSat until lastSat is greater than 31. If lastSat is initially 2:
(1-5) For the loop's first, second, third, fourth, and fifth iterations, the loop condition returns true, and lastSat is increased from 2 to 9 to 16 to 23 to 30 to 37;
(6) At the beginning of what would be the loop's sixth iteration, the loop condition returns false, so the browser moves on to...

if (lastSat > 31) lastSat += -7;

The if condition returns true and lastSat is decreased to 30, its final value. Relatedly, if this year's April, June, September, or November were to begin on a Thursday (none of them do), then the final lastSat value for these months would be 31. However, we'll see in a moment that these seemingly erroneous values don't cause any problems vis-à-vis determining whether the user (at least an American user, for whom the script is clearly intended) is or is not on DST.

The DST code's next sub-block similarly determines the date of the first Saturday of the current month regardless of what month it is, for all twelve months.

var firstSat; firstSat = date - (day + 1);

À la the previous sub-block, we first declare the variable firstSat and initially assign thereto the date of the Saturday prior to the current date.

while (firstSat > 0) firstSat += -7;
if (firstSat < 1) firstSat += 7;

A while loop subtracts multiples of 7 from firstSat until the firstSat value falls below 1, at which point the loop stops and the subsequent if statement adds 7 to firstSat. If date - (day + 1) is 2 per the discussion above, then the while loop drops firstSat to -5 and the if statement brings firstSat back up to 2.

Next we have a conditional that tests if the user (again, an American user) is on DST:

// Adjust for Windows95 daylight-saving time changes
if ((((month == 4) && (date >= firstSat)) || month > 4) && (month < 11 || ((month == 10) && day <= lastSat))) { adjust += 60; }

If the if condition returns true, then the variable adjust, set to 0 at the beginning of the checkDateTime( ) function, is increased by 60. In plain English, the if conditional says,
"If the current month is May, June, July, August, September, or October, or
if it's April and the current date is or is later than the first Saturday thereof, or
if it's October and the current date is or is earlier than the last Saturday thereof, then
add 60 to adjust because it's currently DST."
The if condition would without incident return true for an April 31, June 31, or September 31, and false for a February 30 or November 31.

Even if the U.S. DST period had not changed in 2007, however, the preceding if condition would be problematic in several respects:
(1) The first Saturday of April and also the first two hours of the first Sunday of April, which formerly belonged to standard time, would be mistaken for DST.
(2) The month < 11 mini-condition should be month < 10.
(3) If October's lastSat is 31, then DST would actually end on 25 October (the prior Sunday) and almost an entire week's worth of standard time would be mistaken for DST.
(4) If October's lastSat is not 31, then the first two hours of the following Sunday, which formerly belonged and still currently belong to DST, would be mistaken for standard time.
We'll sort out these problems later, but let's address another matter for the time being: Why would we want to add 60 to adjust? We move to the DST block's last two lines:

yourOffset = (new Date( )).getTimezoneOffset( );
/* Instead of 'reconstructing' the Date object, you can use the today Date object here, i.e.: yourOffset = today.getTimezoneOffset( ); */
yourOffset = yourOffset + adjust;

The getTimezoneOffset( ) method of the Date object returns the number of minutes between the user's local time and GMT (Greenwich Mean Time, a.k.a. UTC - OK, GMT and UTC are not exactly the same, but what's a few leap seconds among friends?). The getTimezoneOffset( ) return is a positive number for the UTC-x time zones of the Western hemisphere and is a negative number for the UTC+x time zones that mostly lie in the Eastern hemisphere. For example, I live in New Orleans, which is located in the UTC-6 time zone, and thus new Date( ).getTimezoneOffset( ) returns 360 (60 minutes/hour × a 6-hour offset from UTC) on my computer during standard time.

But now things get a bit confusing. The getTimezoneOffset( ) return, assigned here to the variable yourOffset, is, according to Mozilla's documentation, supposed to take DST into account, which evidently requires the user's operating system to have some sort of 'set daylight-saving time automatically' capability. The // Adjust for Windows95 daylight-saving time changes comment preceding the above DST conditional suggests that the Windows 95 operating system does not have such a capability, i.e., the user must manually adjust the Windows 95 clock for DST - I don't have access to, nor do I plan to track down, a computer running Windows 95, and it is left to those interested to verify whether this is true or not - so for the benefit of Windows 95 users (for whom the script is also apparently intended), adjust (60) is added to yourOffset. However, for those users on DST, DST decreases by an hour the time difference between UTC and the local time - e.g., during DST in New Orleans, the time is UTC-5 and the getTimezoneOffset( ) return should be 300 - and thus adjust should really be subtracted from yourOffset if yourOffset is meant to represent the user's actual offset from UTC.

The script also adds adjust to the standard time UTC offsets* of the script's various display locations that are set globally at the top of the script element, even for those locations that do not currently observe DST, e.g., Tokyo:

var TK = -540; // Japan is located in the UTC+9 time zone.

function checkTK( ) {
window.clearTimeout(checkDateTime);
gmtOffset = TK + adjust;
zone = " Tokyo";
checkDateTime( ); }

*This would seem to put the kibosh on my thought that maybe the Windows 95 getTimezoneOffset( ) return during DST is correct, and the script's author is using the yourOffset + adjust expression to give a standard time offset that can be properly compared with the gmtOffset = 360 standard time offset for the script's initial U.S. Central Time display.

Clearly, the script's DST code could use a good revamping, wouldn't you say?

DST to the millisecond

It occurred to me that DST 'goalposts' can be created more efficiently and more precisely via the
new Date(dateString); and
new Date(year, month, date [, hour, minute, second, millisecond ]);
syntaxes for the Date object, as follows:

var today, year, dstweek, secondSun, dstweek2, firstSun, dstbegins, dstends;

today = new Date( );
year = today.getFullYear( );

dstweek = new Array( );
for (i = 8; i < 15; i++) {
dstweek[i] = new Date("March " + i + ", " + year);
if (dstweek[i].getDay( ) == 0) secondSun = i; }

dstweek2 = new Array( );
for (i = 1; i < 8; i++) {
dstweek2[i] = new Date("November " + i + ", " + year);
if (dstweek2[i].getDay( ) == 0) firstSun = i; }

dstbegins = new Date(year, 2, secondSun, 2, 0, 0, 0);
dstends = new Date(year, 10, firstSun, 2, 0, 0, 0);

if (dstbegins < today && today < dstends) gmtOffset -= 60;

As noted earlier, the current U.S. DST period starts on the second Sunday of March, which is necessarily 8, 9, 10, 11, 12, 13, or 14 March. We begin by creating an array dstweek of these dates for the current year and then use the getDay( ) method to flag which of those dates is a Sunday; that Sunday date is assigned to the variable secondSun. The current U.S. DST period ends on the first Sunday of November, which is necessarily 1, 2, 3, 4, 5, 6, or 7 November. We next create an array dstweek2 of these dates for the current year and use the getDay( ) method to flag which of those dates is a Sunday; that Sunday date is assigned to the variable firstSun.

For the secondSun and firstSun dates, respectively, DST begins and ends at exactly 2AM local time, which can be set to the millisecond via:

dstbegins = new Date(year, 2, secondSun, 2, 0, 0, 0);
dstends = new Date(year, 10, firstSun, 2, 0, 0, 0);

Finally, because [a Date object] date is measured in milliseconds since midnight 01 January, 1970 UTC, we can use simple comparisons to determine if the current date/time, today, lies in the DST period:

if (dstbegins < today && today < dstends) gmtOffset -= 60;

The above approach is easily extended to the DST periods of other countries. For example, in New Zealand DST begins at 2AM local time on the last Sunday of September and ends at 3AM local time on the first Sunday of April, and thus the corresponding code is:

dstweek = new Array( );
for (i = 24; i < 31; i++) {
dstweek[i] = new Date("September " + i + ", " + year);
if (dstweek[i].getDay( ) == 0) lastSun = i; }

dstweek2 = new Array( );
for (i = 1; i < 8; i++) {
dstweek2[i] = new Date("April " + i + ", " + year);
if (dstweek2[i].getDay( ) == 0) firstSun = i; }

dstbegins = new Date(year, 8, lastSun, 2, 0, 0, 0);
dstends = new Date(year, 3, firstSun, 3, 0, 0, 0);

if (dstbegins < today || today < dstends) gmtOffset -= 60;
// Note the change in logical operators!

Regarding the script's other display locations:
In London (and in the rest of Europe except Iceland, for that matter), DST begins at 01:00 UTC on the last Sunday of March and ends at 01:00 UTC on the last Sunday of October.
Mexico's DST period is the same as that observed in the U.S. during 1987-2006, beginning at 2AM local time on the first Sunday of April and ending at 2AM local time on the last Sunday of October.
I trust y'all can write corresponding DST blocks for these locations.
• Tokyo, Hong Kong, Hawaii, and New Delhi do not observe DST.

In the following entry, we'll detail the generation of the U.S. Central Time time/date display that initially appears in the face text box.

reptile7


Powered by Blogger

Actually, reptile7's JavaScript blog is powered by Café La Llave. ;-)