reptile7's JavaScript blog
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.


Comments: Post a Comment

<< Home

Powered by Blogger

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