reptile7's JavaScript blog
Friday, November 24, 2006

Random Numbers Again, and Other Stuff
Blog Entry #58

In HTML Goodies' JavaScript Primers #20, the core JavaScript Date object and its getSeconds( ) method along with the % modulus arithmetic operator were put to the service of creating random numbers. The random number methodology of Primer #20 was subsequently applied to:
(a) the display of random text strings in Primer #23;
(b) the display of random images in the Primer #23 Assignment;
(c) the coding of guessing games in Primers #22 and #26; and
(d) the coding of a random link generator in the Primer #26 Assignment.

However, in Blog Entry #36 we learned that the core JavaScript Math object and its random( ) method can also be used to create random numbers. In today's post, we revisit the Math.random( ) approach to random numbers as we discuss the script that spans Script Tips #16, #17, and #18. The Script Tips #16-18 Script is given below:

<script language="javascript"><!--
function RandomNumber(upper_limit) {
return Math.round(upper_limit * Math.random( )); }
//-->
</script>
<script language="javascript"><!--
var upper_limit = 50;
document.write("Here is a random number between 0 and " + upper_limit + ":<p><center>");
document.write(RandomNumber(upper_limit) + "</center>");
//-->
</script>

Deconstructing the Script Tips #16-18 Script

As shown above, the Script Tips #16-18 Script actually consists of two scripts, between which is a rough 'division of labor.'
(1) The first script comprises a function, RandomNumber( ), that generates a random integer ranging from 0 to 50, inclusive.
(2) The second script is, for lack of a better description, a user interface that provides the RandomNumber( ) function call, supplies RandomNumber( ) with a run-time parameter, and displays RandomNumber( )'s output.
The Script Tips #16-18 Script could just as easily be written as a single script, however, by subtracting its 4th, 5th, and 6th command lines.

Let's start our deconstruction with the second script. As you might guess, the var upper_limit = 50 statement will set 50 as the upper limit of the random number range. Joe notes in Script Tip #17 that the upper_limit value can be customized by the user via a prompt( ) command, for example:

// In place of the var upper_limit = 50 statement, substitute:
var upper_limit = window.prompt("Please set an upper limit for the random number range:","Enter a value here.");

Following the upper_limit declaration are two document.write( ) commands, the second of which triggers the RandomNumber( ) function in the first script:

document.write(RandomNumber(upper_limit) + "</center>");

At least a couple of times previously we've seen a basic function_name( ) function call occur within another JavaScript expression; we originally discussed a document.write(function_name( )) expression in the "Function Returns" section of Blog Entry #23; we subsequently discussed an if (!function_name(x)) statement in the "Social Security number validation" section of Blog Entry #49.

The RandomNumber(upper_limit) function call sends upper_limit to the RandomNumber( ) function; however, function parameterization is unnecessary in this case, given that upper_limit is a global variable and is not renamed when it is passed to RandomNumber( ).

A single statement composes the RandomNumber( ) function:

return Math.round(upper_limit * Math.random( ));

Lots of action in one command line! The script would read more straightforwardly, and have more pedagogical value, if we were to expand the RandomNumber( ) statement as follows:

var rn = Math.random( );
var noninteger0_50 = upper_limit * rn;
var integer0_50 = Math.round(noninteger0_50);
return integer0_50;

Here's what's happening:

(1) Math.random( ) returns a random number - more precisely, a pseudorandom number - between 0 and 1; this number, which on my computer runs at least 16 digits past the decimal point, is assigned to the variable rn. I don't know what to make of Joe's comments on the Math.random( ) return in Script Tip #16: Keep in mind that JavaScript plays in millisections [?] so it's not as silly as it first seems. There are 1000 choices the script can make.
(Netscape itself is not at all forthcoming as to how, algorithmically, the JavaScript engine produces the Math.random( ) number; Wikipedia suggests that a linear congruential generator is involved.)

(2) We then, using the * multiplication arithmetic operator, multiply rn by upper_limit (50), giving a product, noninteger0_50, that ranges between 0 and 50 and still runs many digits past the decimal point.

(3) The round( ) method of the Math object then rounds noninteger0_50 up or down to the nearest integer depending on whether noninteger0_50's tenths-place digit is 5-9 or 0-4, respectively.

For example:
rn = 0.7614719611832943
noninteger0_50 = 38.073598059164716
integer0_50 = 38

Contra Script Tip #16, the round(x) method does not always round its argument x up; the ceil(x) method of the Math object does that. Complementarily, the floor(x) method of the Math object always rounds its argument x down to the nearest integer. In sum:
• Math.round(noninteger0_50) gives a random integer ranging from 0 to 50, inclusive.
• Math.ceil(noninteger0_50) would give a random integer ranging from 1 to 50, inclusive.
• Math.floor(noninteger0_50) would give a random integer ranging from 0 to 49, inclusive.

(4) Finally, the return integer0_50 statement sends the value of integer0_50 back to the second document.write( ) command in the second script.

We return now to the second script and its document.write( ) commands. You may be wondering, "Is it necessary to have two document.write( ) commands here? Can't we load the five document.write( ) arguments into a single document.write( ) command?" Yeah, actually, we could do that:

document.write("Here is a random number between 0 and " + upper_limit + ":<p><center>" + RandomNumber(upper_limit) + "</center>");

In his discussion of the document.write( ) commands in Script Tip #18, Joe displays a fuzzy understanding of the + string concatenation operator, saying:
"Those plus signs alert the browser that what is in between is not to be written to the page...The plus signs just act as triggers to tell the browser to look back. The text between the plus signs is only a representation of something else the script will provide."
In fact, it is the absence of quotation marks around upper_limit and RandomNumber(upper_limit) that alert[s] the browser that [these arguments are] not to be written to the page and are only a representation of something else the script will provide; if "upper_limit" and "RandomNumber(upper_limit)" were quoted, then they would indeed appear on the page. The + signs merely serve to hook together the various document.write( ) arguments, regardless of whether they are quoted or unquoted:

document.write("The " + "sky " + "is " + "blue.");
is fully equivalent to
document.write("The sky is blue.");

I was also initially taken aback by Joe's description of the RandomNumber( ) function call in the second document.write( ) command: It represents the entire function and thus represents the output of the function.

When I first read this, I thought, "The entire function?? No, Joe, this is just a function call, and it wouldn't even represent RandomNumber( )'s output without RandomNumber( )'s return statement*." But it then occurred to me that we could indeed substitute RandomNumber( )'s 'action' - its random number-generating expression, minus the return keyword - for the RandomNumber(upper_limit) document.write( ) argument and thus reduce the Script Tips #16-18 Script to:

<script type="text/javascript">
var upper_limit = 50;
document.write("Here is a random number between 0 and " + upper_limit + ":<p><center>" + Math.round(upper_limit * Math.random( )) + "</center>");
</script>

And now perhaps I am guilty of packing too much functionality into too small a space.
(*BTW, if we delete RandomNumber( )'s return keyword, then the second document.write( ) command writes undefined to the page.)

We wrap up with two more points:

• As the center element is deprecated, the <p><center> markup in the first document.write( ) command should be replaced with <p style='text-align:center;'> and the closing </center> tag in the second document.write( ) command should be either replaced with a closing </p> tag or removed.

• According to this section of Appendix B of the W3C's HTML 4.01 Specification, the occurrence of closing HTML tags in the arguments of document.write( ) commands in script elements is "illegal," and the slashes in these tags should thus be escaped with backslashes, e.g.:

document.write("Here is a random number between 0 and " + upper_limit + ":<p style='text-align:center;'>" + Math.round(upper_limit * Math.random( )) + "<\/p>");

However, there's no mention of this in the "Scripts" section of Netscape's HTML Guide for Netscape Navigator 4.x - check the Example 2. Using SCRIPT tags in the Document Body and the source code of its functioning demo page.

We'll move on to Script Tips #19-20 in the next entry and examine a script that allows the user to customize a document's background and text colors.

reptile7

Tuesday, November 14, 2006

Browser Olfaction II
Blog Entry #57

In our last episode, we looked at a simple browser detection script authored by Joe himself. We turn our attention today to a more elaborate browser sniffer crafted by "Iceman" and discussed in Script Tips #13, #14, and #15.

But just where is Iceman's script, huh? The "Click to See the Script" links in Script Tips #13 and #15 take the reader to an unrelated-to-Script-Tips-#13-15, sprawling "Browser Detect Script" originating from WebReference.com; in Script Tip #14, this link leads to a "404 - File not found" page. Fortunately, only a modicum of detective work was necessary to track down the correct location of the Script Tips #13-15 Script, which is:
http://www.htmlgoodies.com/legacy/beyond/javascript/stips/browserpage.html

For your edification, Iceman's browser sniffer is unfurled below in its entirety:

<script language="javascript">
<!--
/* Script by Iceman, dciceman@usa.net. You can use this script providing this header stays in. Copyright 1997 Iceman. The parent.location.href statements can be changed to get different redirections for different browsers. */

var r; var browser = navigator.appName;
var version = "4.0 (compatible; MSIE 4.0b1; Windows 95)";
// if statement #1
if (browser == "Microsoft Internet Explorer" && navigator.appVersion == version) {
// If Browser is MSIE 4.0b1 then go to MSIE Page.
parent.location.href = "msiepage.html"; r = 2; }

// start of the else block
else { var ua = "Mozilla/2.0 (compatible; MSIE 3.02; Windows 95)";
// if statement #2
if (navigator.userAgent == ua) {
// If Browser is MSIE 3.02 go to MSIE Page.
parent.location.href = "msiepage.html"; r = 2; }

var version = "4.0 (compatible; MSIE 4.0b2; Windows 95)";
// if statement #3
if (browser == "Microsoft Internet Explorer" && navigator.appVersion == version) {
//If Browser is MSIE 4.0b2 then go to MSIE Page.
parent.location.href = "msiepage.html"; r = 2; }

var version = "2.01 (Win95; I)";
// if statement #4
if (navigator.appVersion == version && navigator.appCodeName == "Mozilla") {
//If Browser is Netscape 2.01 go to Netscape page.
parent.location.href = "nspage.html"; r = 1; }

var version = "2.0 (Win95; I; 16bit)";
// if statement #5
if (navigator.appVersion == version && navigator.appCodeName == "Mozilla") {
//If Browser is Netscape 2.0 16 bit go to Netscape page.
parent.location.href = "nspage.html"; r = 1; }

var version = "4.01 [en] (Win95; I)";
// if statement #6
if (navigator.appVersion == version && navigator.appCodeName == "Mozilla") {
//If Browser is Netscape 4.01 go to Netscape page.
parent.location.href = "nspage.html"; r = 1; }

var version = "3.0 (Win95; I)";
// if statement #7
if (browser == "Netscape" && navigator.appVersion == version) {
// If browser is Netscape 3.0 for Win95 go to Netscape Page.
parent.location.href = "nspage.html"; r = 1; } } // end of the else block

// if statement #8
if (browser == "Netscape") {
// If browser is Netscape and supports JavaScript go to Netscape Page.
parent.location.href = "nspage.html"; r = 1; }

// if statement #9
if (r != 1 && r != 2) {
/* If browser is not Netscape or MSIE known versions go to text-based page (caters for any other JavaScript browsers). */
parent.location.href = "textpage.html"; }
// -->
</script>

We're not going to go through this script line by line but will summarize its key features below.

The script comprises basically a series of if statements, nine in all. If statements #2-7 are enveloped in an else statement, although this is unnecessary; the else keyword, the opening else brace {, and the closing else brace } can be removed.

If statements #1-3 use the navigator.appName, navigator.appVersion, and/or navigator.userAgent properties to test for the 4.0b1, 3.02, and 4.0b2 versions of Internet Explorer, respectively. A parent.location.href = "msiepage.html" command, which can be shortened to location = "msiepage.html", sends users of these browsers to a msiepage.html Web page tailored to Internet Explorer.

If statements #4-7 use the navigator.appName, navigator.appVersion, and/or navigator.appCodeName properties to test for the 2.01, 2.0 16 bit, 4.01, and 3.0 for Win95 versions of Netscape, respectively. A parent.location.href = "nspage.html" command sends users of these browsers to a nspage.html Web page tailored to Netscape.

If statements #1-7 all target browsers for the Windows platform; consider, for example, if statement #1, which checks for a navigator.appVersion value of 4.0 (compatible; MSIE 4.0b1; Windows 95). I am tempted to say at this point that application developers who are not serious about the Macintosh platform are not serious, period, but you knew that, of course.

The script concludes with two platform-independent 'catch-alls.' If statement #8 sends users of JavaScript-supporting versions of Netscape not detected by if statements #4-7 to the nspage.html Web page. (FYI: Netscape Navigator's support for JavaScript began with Netscape 2.0.) Finally, if statement #9 sends users of all browsers not detected by if statements #1-8 to a textpage.html Web page, which is meant to be, quoting Joe, a more text-based version of the [msiepage.html/nspage.html] page.

The != and && operators, revisited

Joe briefly discusses the != comparison operator and the && logical operator in Script Tip #14; his comments are on the right track, but a bit of clarification is in order. It's true that the != operator reads "is not equal to" in the context of an if statement condition, but what != actually does, in contrast to the == equal comparison operator, is compare two operands as to their inequality, and then return a Boolean true if the operands are indeed not equal; the != operands do not have to be unequal:

var bool2 = (5 != 6); // returns true
var bool3 = (5 != (10/2)); // returns false

In turn, it's true that the && operator means "and" in a logical sense; more specifically, && usually takes two Boolean/logical operands (hence its categorization as a logical operator), determines whether they are true or false, and then returns true if both operands are true. Somewhat oddly, && can also take non-Boolean operands and return a non-Boolean value:

var bool4 = (5 && 6);
// returns 6, the second operand, if the first operand does not evaluate to false

Simplifying the Script Tips #13-15 Script

A question arises: "Is all this stuff in Iceman's script really necessary?"

One suspects that a massive simplification of the Script Tips #13-15 Script is indeed possible. (Having said this, I concede that I don't have any of the ancient versions of Internet Explorer or Netscape detected by the script on my computer. Maybe these browsers are downloadable somewhere on the Web, but even if I knew where to find them, then I still wouldn't be interested in larding my hard disk with them, even temporarily, for the purpose of checking their returns for the various navigator object properties - I'm just a killjoy, I guess.)

This larger script is better than the dinky one I wrote...it looks not only at the 'navigator.appName', but also at the navigator version number, Joe states in Script Tip #13. Well, so what? Given that MSIE 4.0b1/3.02/4.0b2 users are all sent to the same msiepage.html page, and that Netscape ≥2.0 users are all sent to the same nspage.html page, then what is the point of testing for this information? Joe himself paves the way for some major housecleaning a little later in Script Tip #13, inadvertently letting the cat out of the bag when he notes:
"You already know that [a navigator.appName value of] 'Netscape' denotes the Navigator browser. Well, now you can see that 'Microsoft Internet Explorer' denotes the MSIE browser."
If this is true for all versions of Netscape and MSIE, then it follows that Iceman's script should be reducible to:

<script type="text/javascript">
<!--
if (navigator.appName == "Microsoft Internet Explorer")
window.location = "msiepage.html";
if (navigator.appName == "Netscape")
window.location = "nspage.html";
if (navigator.appName != "Microsoft Internet Explorer" && navigator.appName != "Netscape")
window.location = "textpage.html";
//-->
</script>

So, is Iceman's browser sniffer, with its mountain of superfluous code, really "better" than Joe's? Not in my book - "less is more" here, I would argue. One more point before moving on: see those navigator.appCodeName == "Mozilla" expressions in the if statements #4-6 Netscape tests? The Microsoft Developer Network (MSDN) notes here that Mozilla is the default navigator.appCodeName value for both Internet Explorer and Netscape.

Detecting more recent browsers

As we all know, Netscape lost the 'first browser war' - this graph says it all. And things haven't gotten any better for Netscape since then - Netscape's usage share is now below that of the Firefox, Opera, and Safari browsers.

Internet Explorer still holds a commanding lead in the browser sweepstakes, of course, but over the last couple of years, Mozilla and Firefox have been slowly but surely chipping away at MSIE's position, reaching a ≅10% usage share. How might we test for Firefox?

The "Additional Browsers' Navigator Information" section of JavaScript Kit's navigator object page notes that recent Windows, Mac, and Linux flavors of Firefox all give a navigator.appName return of Netscape. However, the string Firefox does appear in the navigator.userAgent return for these browsers, and we can fish it out via the indexOf( ) method of the core JavaScript String object as follows:

if (navigator.userAgent.indexOf("Firefox") != -1)
window.location = "firefoxpage.html";

The userAgent property and the indexOf( ) method are generally useful in this regard; they can analogously detect the AOL, Konqueror, Mozilla, MSIE, Netscape, Opera, and Safari browsers, and probably other browsers as well. They can also be used to distinguish Windows, Mac, and Linux users. And in some (but not all) cases, they can, in conjunction with the substr( ) method of the String object and the top-level parseFloat( ) function, be used to determine a browser's version number, as illustrated below:

// this code borrows from the aforelinked WebReference.com browser sniffer
var ua = navigator.userAgent;
var appnamestart = ua.indexOf("MSIE");
var appversionstart = appnamestart + 5;
var appversionstring = ua.substr(appversionstart);
var appversion = parseFloat(appversionstring);

Here's what's happening:
(1) The ua.indexOf("MSIE") expression returns the character index at which the "M" of "MSIE" appears in the navigator.userAgent string; this index is assigned to appnamestart.
(2) The MSIE version number begins at the character index appversionstart, five characters later than appnamestart in the userAgent string.
(3) The ua.substr(appversionstart) expression returns the portion of the userAgent string beginning with appversionstart and running to the end of the userAgent string; this substring is assigned to appversionstring.
(4) The parseFloat(appversionstring) expression returns the number at the beginning of appversionstring, i.e., the browser version number.

For example, MSIE 3.02 would give an appversion value of 3.02, as can be seen from the var ua = "Mozilla/2.0 (compatible; MSIE 3.02; Windows 95)" statement preceding if statement #2 in Iceman's script. This method for obtaining a browser version number gives results that are spot on in some cases, approximately correct in some cases, and wildly off in some cases - use it with caution.

As for the future...

I've recently been reading through HTML Goodies' collection of XHTML articles, which are curiously subdivided between the Beyond HTML : XML section and the HTML Primers section, and the following remarks in the "Following the XHTML Path" article caught my eye:
"The one aspect that every Web page designer has wanted was cross browser compatibility. By current standards, when you code HTML you must either code separately for each browser you wish to support or you must code in such a fashion that all browsers will accept the HTML that you create...With XML and XHTML, if the browser can parse the W3C XHTML 1.0 DTD correctly, then your page should display nicely...The DTD will define the data for [a] specific device and the XML parser contained in the device will process the information correctly and prepare it for presentation to the user."
Go take a good look at the WebReference.com browser sniffer if you didn't do so earlier; my own response to this script: "This is nuts." Perhaps we would all be better off tightening up our code.

I think we've had our fill of browser sniffing, don't you? In the next post, we'll take on Script Tips #16-18, which discuss a script that generates and writes to the page a random integer.

reptile7

Saturday, November 04, 2006

Scent of a Browser
Blog Entry #56

We continue the previous post's "torn between two locations" theme in this and the next entries as we examine two browser detection scripts that (a) attempt to determine which browser the user is using and then (b) route the user to alternate (≥2) locations that respectively cater to different browsers, a process sometimes termed "browser sniffing." I am hesitant to discuss these scripts almost as a matter of principle - I expressed my disdain for browser-specific Web design back in Blog Entry #12 - and yet the archivist in me demands that we talk about them to at least some extent, so we will.

I also point out that these scripts were both written during the 1996-1998 Internet Explorer vs. Netscape Navigator browser war, and are thus in a sense rather dated. But there's nothing stopping us from refreshing them a bit, is there?

The Script Tips #10-12 Script

Script Tips #10, #11, and #12 discuss the following 'browser sniffer':

<script language="javascript">
<!--Hide the Script
//This Script written by Joe Burns, Ph.D.
//Use it freely
if (navigator.appName == "Netscape")
{parent.location="nspage.html";}
else
{parent.location="msiepage.html";}
// End hiding the Script-->
</script>
<!--The appName property is incorrectly capitalized in Script Tips #10-12.-->

Let's briefly take stock of the script issues that Joe chooses to address.

Script Tip #10 discusses the use and syntax of JavaScript comments and also the use of HTML commenting to hide JavaScript scripts from non-JavaScript-capable browsers - we previously covered these topics in Blog Entry #6. For those in the audience who might want primary source references in this regard:
(1) JavaScript commenting is treated in Chapter 4 ("Comments") of the JavaScript 1.5 Core Reference.
(2) The syntax of HTML comments is documented by the W3C here.
(3) For using the <!-- and //--> markup to comment out JavaScript scripts, see the "Hiding Scripts Within Comment Tags" section of the JavaScript 1.3 Client-Side Guide.

Script Tip #11 discusses the == comparison operator and the = assignment operator. If I didn't know better, then I'd say that Joe sounds almost confused here about the difference between these operators, which are both "equal" operators but reflect totally different operations. We fleshed out the == and = operators in Blog Entries #37 and #30, respectively, but just to recap, consider the following code:

<body bgcolor="red" text="white">
<script type="text/javascript">
var bool = (document.bgColor == "#ff0000");
</script>

The <body> tag creates a document object with a red background and a white foreground. In the subsequent script, the (document.bgColor == "#ff0000") expression compares document.bgColor with the red hex value #ff0000 as to their equality, returning a Boolean value - true in this case - that is assigned to the variable bool, which is then displayed on an alert( ) box. The == operator does not assign #ff0000 to document.bgColor; the bgcolor="red" attribute in the <body> tag did that. We'll return to these operators when we deconstruct the Script Tips #10-12 Script in just a bit.

Script Tip #12 mostly discusses if...else statements (recall that the Script Tips #5-9 Script contained an if statement without an else statement) and provides a brief script deconstruction; I trust we don't need to go over if...else statements at this point.

Deconstructing the Script Tips #10-12 Script

As shown above, the Script Tips #10-12 Script consists of several comment lines and a single if...else statement. The if statement's condition, navigator.appName == "Netscape", compares the value of the appName property of the navigator object to Netscape; if they're equal, then the if condition returns true, so nspage.html is assigned to the location property of the window object, setting up a link to the nspage.html Web page, which presumably is tailor-made for the Netscape browser. (We've previously noted that window.location=URL, parent.location=URL, and even just location=URL are all interchangeable outside of a frames situation.)

If navigator.appName and Netscape are not equal, then the if condition returns false, so the else "catch-all" kicks in: msiepage.html is assigned to parent.location, setting up a link to the msiepage.html Web page, which is meant for Internet Explorer or other non-Netscape browsers.

The "Additional Browsers' Navigator Information" section of JavaScript Kit's navigator object page reveals that navigator.appName == "Netscape" would return true for recent versions of the AOL, Firefox, Mozilla, Netscape, and Safari browsers - perhaps the Script Tips #10-12 Script is not so dated after all.

The script's design implies the creation of three Web pages: the nspage.html page, the msiepage.html page, and a separate page for the script itself. Clearly, it'd be more efficient to just code the nspage.html and msiepage.html pages and then use as an entry point the msiepage.html page containing the condensed script below:

<script type="text/javascript">
if (navigator.appName == "Netscape") window.location="nspage.html";
</script>

The navigator object and the DOM

The navigator object is created automatically by the browser's JavaScript engine and without reference to a loading document. In the classic JavaScript Navigator object hierarchy, the navigator object is not part of the window object 'tree'; rather, the navigator object is a 'peer' of the window object and heads its own mini-hierarchy with mimeTypes[ ] and plugins[ ] arrays as 'children.'

However, at some point Netscape/Mozilla evidently decided that the navigator object should be a property of the window object, and Mozilla's DOM window Reference accordingly sports a window.navigator page.

The "Specification" section of Mozilla's window.navigator page states that the navigator object is "DOM Level 0. Not part of any standard," which is correct as of this writing. The W3C's Window Object 1.0 working draft does not include "Client information ('window.navigator')."

In the next post, we'll check over the more complicated browser sniffer of Script Tips #13-15 and perhaps see what we can do to bring these scripts into the modern era.

reptile7