reptile7's JavaScript blog
Thursday, June 28, 2007
Honey, I Shrunk the Script
Blog Entry #80

Today's post will conclude our guided tour of the Script Tips #56-59 Script. Having slogged through the deconstruction of the last few entries, I have a couple of bombshells for you:
(1) The custom chords object (see Blog Entry #78) is unnecessary - it is easily folded into the chord selection list of the second td element.
(2) The parser( ) function (see the previous post) is also unnecessary, and can be replaced with a single line of code.

Ciao, chords

When I first looked over the chords object and the chord menu, my intuition told me that these code blocks were redundant, and it subsequently occurred to me that the chords values could alternatively be assigned to value attributes added to the corresponding chord option elements:

<select name="chord" size="7" onchange="showChord( );">
<option value="1;5;14;15;16">A</option>
<option value="1;3;5;14;16">A7</option>
<option value="3;4;5;14;25;30">A9</option> <!-- etc. -->

Then, in the showChord( ) function, let's replace the parser( ) function call and the preceding Text variable statement with:

Value = document.guitar.chord.options[Item].value;
Frets = parser(Value);

Upon making these changes, the entire chords array can be removed.

Pack it in, parser( )

Given the format of the guitar chord property values - a series of numbers delimited with semicolons - we can substitute for the parser( ) function the following command:

Frets = document.guitar.chord.options[Item].value.split(";");

As its name implies, the split( ) method of the String object splits a string to give an array of substrings; the split(x) parameter x sets the original string's break-points but is not part of the resulting array.

At HTML Goodies, the "Arrays" section of the "JavaScript Basics Part 5" tutorial provides a split( ) method demo and points out that the split( ) method can be considered the inverse of the join( ) method of the Array object, which we utilized in the "Method arrays" section of Blog Entry #43.

Lagniappe: don't play that string

If you're a guitarist, you are familiar with chord diagrams with X's above unplayed strings:

Dm chord showing headstock X's for 5th/6th strings

My approach to modifying the Script Tips #56-59 Script to write headstock X's for chords that need them is outlined below.

The chord option elements

For the D minor chord, the 5th (A) and 6th (E) strings are unplayed*, so we want to write X's in place of the zeroth and first radio buttons of the guitar form. In the chord menu, code the Dm option as:

<option value="0X;1X;2;11;15;22">Dm</option>

(*If you play the D minor chord with an open 5th string, code the option as:
<option value="0X;1;2;11;15;22">Dm</option>
- the code to follow works with either option value.)

The radio button grid

Next, put each radio button element in a span element:

var spanIndex = 0, radioIndex = 0, inputIndex = 0;
for (Countx = 1; Countx < 8; Countx++)
for (Count = 1; Count < 7; Count++)
document.write("<span id='span" + spanIndex + "'>");
document.writeln("<input type='radio' name='r" + radioIndex + "' id='input" + inputIndex + "' onclick='this.checked=false;' />");
spanIndex++; radioIndex++; inputIndex++;
document.write("<br />");
if (Countx == 1) document.write("<hr class='nut' />");
else document.write("<hr class='fret' />"); }

The span and input elements are given 'ordinalized' id values (span0, span1, span2, ... input0, input1, input2, etc.) that can be exploited by the checking and unchecking loops, bringing us to...

The checking loop

for (Count = 0; Count < Frets.length; Count++)
if (/X$/.test(Frets[Count])) {
var dontplay = parseInt(Frets[Count],10);
document.getElementById("span" + dontplay).innerHTML = "X";
else document.getElementById("input" + Frets[Count]).checked = true; }

A six-iteration loop generates the display shown by the above image.

(1-2) For the first two iterations:

The 0X and 1X Frets values match the /X$/ regular expression and thus the if condition returns true.

The 0 and 1 characters of 0X and 1X are respectively extracted by the top-level parseInt( ) function and appended to "span" for a document.getElementById( ) command that will assign X to the value of the innerHTML property of the span0 and span1 elements, which would normally hold the 0th and 1st radio buttons in the first table cell.

For the parseInt( ) function to see the 0 of 0X as a decimal number, it is necessary to include a second parseInt( ) parameter specifying a radix of 10 for 0X. Without the 10, the parseInt( ) function perceives 0X as the beginning of a hexadecimal number; as a result,

var dontplay = parseInt("0X");
// is equivalent to
var dontplay = parseInt("");

In this case, the dontplay variable returns the value NaN (Not-a-Number). As you might expect, the browser promptly throws a runtime error when NaN is plugged into the document.getElementById( ) command.

(3-6) For the other four iterations:

The if condition returns false and the else statement kicks in. The 2, 11, 15, and 22 string values are respectively appended to "input" for a document.getElementById( ) command that will assign true to the value of the checked property of the input2, input11, input15, and input22 radio button elements.

The unchecking loop

for (Count = 0; Count < 42; Count++)
if (document.getElementById("span" + Count).innerHTML == "X")
document.getElementById("span" + Count).innerHTML =
"<input type='radio' name='r" + Count + "' id='input" + Count + "' onclick='this.checked=false;' />";

else document.getElementById("input" + Count).checked = false; }

I trust you can take it from here. For the loop's first two iterations, the if condition returns true and the X's in the span0 and span1 elements are converted to radio buttons. The loop's other forty iterations 'erase' (uncheck as necessary) the input2-input41 set of radio buttons.

(Regarding the checking and unchecking loops above, I found that document.getElementById( ) commands allowed a much more straightforward manipulation of the fretboard than did the original document.guitar.elements[ ] commands, which were accordingly 'put out to pasture'.)

And now for something completely different: in the next post, we'll take up the Script Tips #60-64 Script, which sets and reads a JavaScript cookie.


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.


Friday, June 08, 2007
An Associative Axe Array
Blog Entry #78

Back to the Script Tips #56-59 Script and the script element that composes the script's first td element. We've briefly mentioned a couple of times previously that, above and beyond the client-side and core objects we've heretofore been working with, JavaScript also allows you to create your own custom objects; today's post looks at a custom object that maps guitar chords onto their corresponding finger positions on the script's first-cell fretboard. The relevant code is loaded into the div below:

var chords = new Object( );
chords["A"] = "1;5;14;15;16";
chords["A7"] = "1;3;5;14;16";
chords["A9"] = "3;4;5;14;25;30";
chords["A13"] = "1;3;20;22;23";
chords["Am"] = "1;5;10;14;15";
chords["Am6"] = "0;1;10;14;15;17";
chords["Am7"] = "30;32;33;34";
chords["Bb"] = "7;20;21;22";
chords["B"] = "13;26;27;28";
chords["B7"] = "4;8;13;15;17";
chords["B9"] = "8;13;15;16";
chords["Bm"] = "13;22;26;27";
chords["Bm6"] = "2;4;9;13;17";
chords["Bm7"] = "2;4;13;15;17";
chords["C"] = "3;5;10;14;19";
chords["C6"] = "5;10;14;15;19";
chords["C7"] = "5;10;14;19;21";
chords["Cmaj7"] = "3;4;5;14;19";
chords["C9"] = "5;14;19;21;22";
chords["Csus4"] = "3;10;19;20";
chords["C7sus4"] = "4;19;20;21";
chords["Cdim7"] = "8;10;12;15";
chords["Cm"] = "19;28;32;33";
chords["Cm7"] = "19;21;28;32";
chords["D"] = "2;15;17;22";
chords["D6"] = "1;2;4;15;17";
chords["D7"] = "2;10;15;17";
chords["Dmaj7"] = "1;2;15;16;17";
chords["D9"] = "26;31;33;34;35";
chords["Dsus4"] = "1;2;15;22;23";
chords["D7sus4"] = "1;2;10;15;23";
chords["Ddim7"] = "2;4;9;11";
chords["Dm"] = "2;11;15;22";
chords["Dm7"] = "1;2;10;11;15";
chords["Eb9"] = "20;25;27;28;29";
chords["E"] = "0;4;5;9;13;14";
chords["E7"] = "0;2;4;5;9;13";
chords["E9"] = "0;2;9;13;17;22";
chords["E13"] = "0;2;5;9;13;16";
chords["Em"] = "0;3;4;5;13;14";
chords["Em6"] = "0;3;5;13;14;16";
chords["Em7"] = "0;3;5;13;14;22";
chords["F"] = "10;11;15;20";
chords["F5"] = "6;19";
chords["F6"] = "1;2;6;10;15";
chords["Fmaj7"] = "1;5;6;10;14;15";
chords["Fm"] = "6;9;10;11;19;20";
chords["Fm7"] = "6;8;9;10";
chords["F#"] = "12;16;17;21;25;26";
chords["F#7"] = "5;12;14;16;21";
chords["F#9"] = "7;9;12;14;16";
chords["F#m"] = "12;15;16;17;25;26";
chords["F#m7"] = "5;15;16;26";
chords["G"] = "2;3;4;13;18;23";
chords["G5"] = "24;37";
chords["G6"] = "14;18;22;27";
chords["G7"] = "2;3;4;11;13;18";
chords["Gmaj7"] = "18;22;26;27";
chords["G9"] = "4;15;18;20";
chords["G6/9"] = "14;15;22;23";
chords["Gsus4"] = "2;3;10;18;23";
chords["G7sus4"] = "3;10;18;20;23";
chords["Gdim7"] = "3;7;14;16;18";
chords["Gm"] = "2;3;7;18;22";
chords["Gm7"] = "18;20;21;22";

On the first line, we create a new instance of the core JavaScript Object object; our new Object object is given the identifier chords. The chords object is in effect an 'empty container' object that can possess whatever properties and/or methods we choose to give it. (This isn't quite true: as you can see from Netscape's Object object page, the Object object does have a small collection of predefined, relatively abstract properties and methods, none of which finds use in the Script Tips #56-59 Script.)

The next 65 lines define properties for the chords object. This code block is formulated as an associative array: an array whose elements are indexed with strings (and not with ordinal numbers). However, we could also use the more familiar objectName.propertyName=propertyValue syntax to set these properties, i.e.:

var chords = new Object( );
chords.A = "1;5;14;15;16";
chords.A7 = "1;3;5;14;16";
chords.A9 = "3;4;5;14;25;30"; // etc.

Is the chords object an Array object? I ask this question because there's no treatment at all of associative arrays in Netscape's Array object documentation*. For that matter, an associative array would seem to not quite fit Netscape's definition of an array - [a]n array is an ordered set of values associated with a single variable name - in the sense that an associative array's elements are not "ordered".
(*The "Backward Compatibility" section does hint indirectly that arrays can be indexed associatively: JavaScript 1.0. You must index an array by its ordinal number; for example myArray[0].)

On the one hand, the var chords = new Object( ); statement can be replaced with

var chords = new Array( );

without any problems. On the other hand, my attempts to use Array object methods (specifically, the join( ), pop( ), and shift( ) methods) with a subset of the chords object were singularly unsuccessful. At HTML Goodies, Array object methods are discussed in two recently posted tutorials: "JavaScript Basics Part 5" and "The JavaScript Diaries: Part 13"; in neither of these articles is an associative array used with an Array object method. (However, the former article relevantly notes that arrayObjectName.length returns 0 for an associative array.)

The script element's showChord( ) and parser( ) functions act on the strings that are assigned to the chords object's properties/elements but do not manipulate the chords object itself, and thus this is admittedly all somewhat of an academic discussion. Anyway, two more points before moving on:

(1) Chapter 7 ("Working with Objects") of the JavaScript 1.5 Core Guide outlines two other ways to create custom JavaScript objects:

(a) Via an 'object literal' syntax:

var chords = { A: "1;5;14;15;16", A7: "1;3;5;14;16", A9: "3;4;5;14;25;30", ... };

(b) Via an object constructor function methodology in which we first use a function to define an object blueprint

function chords(A, A7, A9, ...) {
this.A = A;
this.A7 = A7;
this.A9 = A9; ... }

and then use the new operator to create one or more instances of that blueprint:

var guitarChords = new chords("1;5;14;15;16", "1;3;5;14;16", "3;4;5;14;25;30", ...);

In this case, the chords collection of properties and statements that assign values to those properties can be reused by whateverChords object instances for other fretted instruments.

(2) The properties of client-side and core JavaScript objects can also be accessed via the associative array syntax, if for some reason you wanted to do that:

document["bgColor"] = "red";
document["title"] = "Welcome, Visitor";
// The above statements execute normally on my iMac when using either MSIE or Netscape.

Recommended reference: JavaScript Kit's "JavaScript and Object Oriented Programming (OOP)" tutorial contains a useful overview of custom JavaScript objects.

The chords property values

So now we have our custom chords object and an accompanying set of guitar chord properties. But what about those property values, huh?

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

Each property value is a string of 2-6 numbers delimited by semicolons; what do these numbers mean?

In the previous post, we coded the alternating rows of radio buttons and horizontal rules that collectively compose the first table cell's 'guitar'; in this assembly, the radio buttons are the first 42 members of the elements[ ] array/property of the guitar form. The numbers in each chords property value are the index numbers of the guitar radio button elements that are checked to give the corresponding finger positions of the chord in question:

Radio-button display of the Dm chord on the Script Tips #56-69 Script fretboard.

Recalling that each radio button column is a 'string', upon selecting a chord:
(a) When checked, the guitar.elements[0]-guitar.elements[5] radio buttons in the headstock signify open strings that are played but not fretted (e.g., the 4th (third-from-the-left) string of the Dm chord).
(b) If a given radio button column does not contain a checked radio button, then that string is not played (e.g., the 5th and 6th strings of the Dm chord).
(As to overhauling the script so that it places X's at the top of unplayed strings, well, I'm still working on that...)

Perhaps you are thinking, "Hey, I always play the 5th (A) string when I play a Dm chord"; regarding the script's chord diagrams, I myself would play additional strings for a number of these chords - I would fully barre the B chord, I would play an open 6th string for the C chord, etc. It is left to you to augment or otherwise modify the index numbers in the chords property values to suit your own chord preferences.

Joe for his part discusses the E (major) property value and its radio button display in Script Tip #57.

At this point, we have yet to detail how the script strips the semicolons out of the chords property values and then acts on those radio button index numbers. To accomplish these tasks, we turn to the script's showChord( ) and parser( ) functions, which we'll go through line by line in the next post.


Powered by Blogger

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