reptile7's JavaScript blog
Monday, June 18, 2007
The Saddest of All Keys
Blog Entry #79

We continue today with our analysis of the guitar chord chart script of Script Tips #56-59. This entry will take on the guts of the script: the script element's showChord( ) and parser( ) functions that process the values of the chords object to give chord diagram displays in the first table cell.

Joe variously describes the showChord( ) and parser( ) functions as "hairy", a "mess", a "monster", and "wicked" in Script Tips #58 and #59, and I won't disagree that this code could use some editing help, but we'll have the lot of it sorted out by the end of the post.

In the line-by-line dissection that follows, our test case will be the D minor chord:

chords["Dm"] = "2;11;15;22";

<select name="chord" size="7" onchange="showChord( );">

The user chooses the Dm option on the chord selection list in the second table cell, triggering via an onchange event handler the showChord( ) function.

function showChord( ) {
var Item, Ret, Count, Skip;

The variables Item, Ret, Count, and Skip are declared but not initialized; noteworthily, Ret and Skip do not appear subsequently in the showChord( ) function. The variable declaration expression can be commented out without any problems; if included, it should read:
var Count, Item, Text, Frets;

for (Count = 0; Count < 42; Count++) {
document.guitar[Count].checked = false; }

This for loop sets the checked property of each radio button in the first table cell to false. In this run of the script, nothing happens here because none of the radio buttons has been checked prior to the loop, but once the D minor chord is displayed, then these lines of code would clear the radio button grid if we selected another chord.
(N.B. Although it serves a resetting function, the loop cannot be replaced with document.guitar.reset( ); unless the radio buttons and the chord selection list are put in separate forms.)

The guitar[Count] radio button reference is not standard; the unchecking statement should read:
document.guitar.elements[Count].checked = false;

Item = document.guitar.chord.selectedIndex;

This statement assigns the value of the selectedIndex property of the chord menu to Item; for the Dm option, Item returns 32. Alternatively, this line can be removed if a parameterized function showChord(Item) is called with onchange="showChord(this.selectedIndex);".

if (Item != -1) {

Next we have an if declaration whose condition necessarily returns true: to call the showChord( ) function, the user must select a chord from the chord menu, and if the user has selected a chord, then Item cannot be equal to -1. Needless to say, an always-true if condition defeats the whole purpose of conditional programming - this line, and the concluding if } right brace, should be removed. (In Script Tip #58, Joe seems to misunderstand the != comparison operator, as his description of the if condition - this statement will always be wrong - has the situation reversed.)

Text = document.guitar.chord.options[Item].text;

This line plugs Item (32) as an index number into the options[ ] array/property of the chord menu and then assigns the value of the text property of the Itemth option to the variable Text, which returns Dm in this case.

Frets = parser(chords[Text]);

This line triggers the parser( ) function. Also, Text (Dm) is used as an index string vis-à-vis the chords[ ] associative array; chords[Text], which returns 2;11;15;22, is passed to parser( ). The parser( ) output will be assigned to the variable Frets.

function parser(InString) {

The chords[Text] string value (2;11;15;22) is given the identifier InString.

var Sep = ";", NumSeps = 1, Count, Start, ParseMark, parse;

This line declares six variables, initializing two of them; the Count, Start, ParseMark, and parse variables can be subtracted from the declaration, as they will be redeclared below.

for (Count = 1; Count < InString.length; Count++) { if (InString.charAt(Count) == Sep) NumSeps++; } Contra Script Tip #59, this for loop does not remove InString's semicolons. It does use the charAt( ) method of the String object (which Joe introduced in HTML Goodies' JavaScript Primers #29) to return InString's characters (excepting the zeroth character, because Count begins at 1), which are then compared to Sep, which, Joe notes, was set to represent a semicolon on the preceding line. Regarding the incrementing NumSeps variable, the loop would count the number of semicolons in InString had NumSeps began at 0; in practice, the loop counts the number of numbers in InString.

The NumSeps variable does not appear subsequently in the parser( ) function, and this loop can in fact be commented out; however, I'll suggest a use for NumSeps at the end of the post.

parse = new Array( );

This line creates a new instance of the Array object and assigns it to the variable parse. The parse array will be populated with the numbers in InString and then returned to the showChord( ) function.

var Start = 0, Count = 1, ParseMark = 0, LoopCtrl = 1;

This line initializes four variables that in the while loop below have the following 'division of labor':
(1) Start will be the starting character index of each number in InString.
(2) Count is incremented to give index numbers for the parse array.
(3) ParseMark will be the character index of each semicolon in InString.
(4) LoopCtrl appears in the while loop condition but is not the loop's counter variable (a role indirectly served by Start); rather, LoopCtrl is, for lack of a better description, a 'dummy variable' that allows the loop to run until all of InString has been processed.

while (LoopCtrl == 1) {
ParseMark = InString.indexOf(Sep, ParseMark);
TestMark = ParseMark;
if ((TestMark == 0) || (TestMark == -1)) {
parse[Count] = InString.substring(Start, InString.length);
LoopCtrl = 0;
break; }
parse[Count] = InString.substring(Start, ParseMark);
Start = ParseMark+1;
ParseMark = Start;
Count++; }
/* In the original script, the while loop's last three expressions are on one command line and are delimited not with semicolons but with commas, which somewhat surprisingly (to me at least) does not throw an error. */

The workhorse of the parser( ) function, this while block extracts InString's numbers and assigns them to elements of the parse array. Here's what happens when the loop acts on the 2;11;15;22 (InString) string:

(1) For the loop's first iteration:
(a) InString.indexOf(";", 0) returns 1 (the character index of InString's first semicolon), which is assigned to ParseMark. (The indexOf( ) method of the String object was also introduced in Primer #29.)
(b) The ParseMark value (1) is assigned to the variable TestMark; TestMark's role in the loop's last iteration will be clarified below.
(c) The condition of the following if ((TestMark == 0) || (TestMark == -1)) declaration returns false, so the browser skips over the three statements of the if block.
(d) InString.substring(0, 1) returns 2 (the first number in InString), which is assigned to parse[1]; data-type-wise, the 2 return is a string and not a number. (The substring( ) method of the String object is detailed here.)
(e) ParseMark+1 returns 2, which is assigned to Start.
(f) Start's value (2) is assigned to ParseMark.
(g) Count increments to 2.

(2) For the loop's second iteration:
(a) InString.indexOf(";", 2) returns 4 (the character index of InString's second semicolon), which is assigned to ParseMark.
(b) ParseMark's value (4) is assigned to TestMark.
(c) The if condition again returns false.
(d) InString.substring(2, 4) returns as a string 11 (the second number in InString), which is assigned to parse[2].
(e) ParseMark+1 returns 5, which is assigned to Start.
(f) Start's value (5) is assigned to ParseMark.
(g) Count increments to 3.

(3) For the loop's third iteration:
(a) InString.indexOf(";", 5) returns 7 (the character index of InString's third and last semicolon), which is assigned to ParseMark.
(b) ParseMark's value (7) is assigned to TestMark.
(c) The if condition again returns false.
(d) InString.substring(5, 7) returns as a string 15 (the third number in InString), which is assigned to parse[3].
(e) ParseMark+1 returns 8, which is assigned to Start.
(f) Start's value (8) is assigned to ParseMark.
(g) Count increments to 4.

(4) For the loop's fourth and final iteration:
(a) InString.indexOf(";", 8) returns -1, which is assigned to ParseMark.
(b) ParseMark's value (-1) is assigned to TestMark.
(c) The if condition now returns true. TestMark's -1 value signals that we have reached the last number in InString; TestMark is never 0, and the TestMark == 0 comparison in the if condition can be removed.
(d) InString.length returns 10, and InString.substring(8, 10) returns as a string 22, which is assigned to parse[4].
(e) Unnecessarily, 0 is assigned to LoopCtrl.
(f) The loop is terminated by a break statement.

parse[0] = Count;

Count's value, 4, is assigned to parse[0].

return (parse); // The parentheses here are unnecessary.

The entire parse array is returned to the showChord( ) function and given a new identifier, Frets:
Frets[0] = 4;
Frets[1] = "2";
Frets[2] = "11";
Frets[3] = "15";
Frets[4] = "22";
(Script Tip #59 insinuates that the numbers in InString are returned one at a time: not true.)

for (Count = 1; Count <= Frets[0]; Count++) {
document.guitar[parseInt(Frets[Count])].checked = true; }

Finally, a four-iteration for loop checks the 2nd, 11th, 15th, and 22nd radio buttons of the guitar form, giving the D minor chord display. The top-level parseInt( ) function extracts starting integers from strings that begin with integers; for example, parseInt("2X4") returns 2. It is not necessary to use the parseInt( ) function to convert the Frets element strings to numbers, because JavaScript will do this automatically; consequently, and per our earlier discussion of showChord( )'s unchecking statement, the for statement above should be recast as:

document.guitar.elements[Frets[Count]].checked = true;

Parse it, take 2

We can streamline both the parser( ) and showChord( ) functions by replacing the parser( ) while loop with the following for loop, which:
(a) eliminates the LoopCtrl and TestMark variables and also the parse/Frets array;
(b) suitably employs the Count and NumSeps variables as the loop's counter and upper boundary, respectively; and
(c) carries out the showChord( ) checking action via a meaningful use of the parseInt( ) function;
its deconstruction is left to you:

var Start=0, ParseMark=0;
for (Count = 0; Count < NumSeps; Count++) { /* You might want to rechristen NumSepsNumNumbers. */

RadioIndex = parseInt(InString.substring(Start, InString.length));
document.guitar.elements[RadioIndex].checked = true;
/* The showChord( ) checking loop can now be removed. */

ParseMark = InString.indexOf(Sep, Start);
Start = ParseMark + 1; }

However, more dramatic simplifications of the script are possible, and we'll discuss them in the next entry.


Comments: Post a Comment

<< Home

Powered by Blogger

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