reptile7's JavaScript blog
Monday, December 09, 2019
 
More Tuition Check
Blog Entry #404

Let's get back now to our ongoing analysis of the JavaScript/Java Goodies College Tuition Chooser and its calc( )/validateData( )/pow( ) input-processing functionality.

Original processing issues, in execution order

The College Tuition Chooser was written during the JavaScript 1.2 → Navigator 4.0-4.05 era and the calc( ) and validateData( ) functions both feature an if (agentInfo == "Netscape 4.0") { ... } block. No features in those blocks were implemented in JavaScript 1.2 or are Netscape-specific, however. Upon switching the agentInfo == "Netscape 4.0" conditions to agentInfo != "Netscape 4.0" conditions, Navigator 3.04 and IE 4.5 run the blocks and then the computeForm( ) function without incident. Even Navigator 2.0, the first browser with a JavaScript engine, smoothly handles the validateData( ) for-Netscape 4.0 code although this version does throw a value is not a numeric literal error at the computeForm( ) stage, which we will discuss later.

Regarding non-digit ch characters, the validateData( ) for-Netscape 4.0 block allows the presence of decimal points and commas and $s whereas the following not-for-Netscape 4.0 else { ... } block allows the presence of only decimal points: this is fine for our $12,34r.67 example but we'll have to up our game if we want to stop inputs like ..1.23 and ,45,,6,,, and 7$8$9.

Every non-digit character in the initial input.value string is removed by the validateData( ) for-Netscape 4.0 block, which is fine for a correctly placed $ or comma or decimal point but definitely causes a problem vis-à-vis the $12,34r.67 value's mistyped r, whose removal prevents the remaining digits from reaching their original place-value positions - the 1 stops at the thousands place versus the ten-thousands place, the 2 stops at the hundreds place versus the thousands place, etc. - so that the validateData( ) correctedNum return, 1234.67, is smaller than the starting input by a factor of 10! We can minimize the damage in this case by mapping the r to a 0:

else if ((ch != ",") && (ch != "$")) {
    window.alert("The invalid character " + ch + " was detected and will be replaced by a 0.");
    if (! isDecimal) correctedNum = Number(correctedNum + "0");
    else { addOn += "0"; ++decimalPlace; } }
/* The Number( ) string-to-number conversion works for Netscape 3+; for Netscape 2, eval( ) would work. */


In contrast, the not-for-Netscape 4.0 code converts the initial input.value to 0 when it hits a not-OKed ch character, which would be fair enough for a hello world input but a heavy-handed response for a 1,000 input - I prefer in this case to return focus( ) to the input and to leave in place and select( ) the value:

/* At the end of the calc( ) function, after validateData( ) has returned false: */
else {
    window.alert("Your input may contain digits and a decimal point: please remove any other characters.");
    window.setTimeout(function ( ) { input.focus( ); input.select( ); }, 100); }
/* The loss-of-focus aspect of the onchange="calc(this);" trigger can interfere with the focus( ) and select( ) operations but that interference can be overcome by a short setTimeout( ) delay. */


The pow( ) function generates a 10decimalPlace power of 10 for the division operation that fractionizes the addOn part of the input; however, there's no need to create an ad hoc function when we can avail ourselves of the pow( ) method of the Math object for this purpose:

if (decimalPlace > 0) correctedNum += addOn / Math.pow(10, decimalPlace);

The validateData( )/pow( ) code places no limits on the number of post-decimal point digits: we could input 3.141592653589793 and the whole thing would be carried forward. However, it is simple enough to truncate such a too_long str string at the 'thousandths' place and penultimately round the final correctedNum number at the hundredths place:

var str = theNum;
var decimalpointindex = str.indexOf(".");
if (decimalpointindex != -1 && decimalpointindex + 2 < str.length) {
    var too_long = true;
    str = str.substring(0, str.indexOf(".") + 4); }
...
if (too_long) correctedNum = Math.round(correctedNum * 100) / 100;
// More modernly: if (too_long) correctedNum = correctedNum.toFixed(2);
return correctedNum;


One nonetheless wonders, "Why have different ways of dealing with the integer and fractional parts of an input? Just recast the str string as a new well-formed numeric str2 string containing only digits and a decimal point, and then Number( )-ize str2 at the end. What's the problem?"

Segue to regular expressions

If we do want to do something special for level 4 (and later) browsers versus their predecessors, then we can vet the cost inputs via a suitable regular expression: regular expressions were implemented in JavaScript 1.2.

// Begin the calc( ) body with:
var cost_pattern = /^\$?(\d{1,6}|\d{1,3},\d{3})(\.\d{0,2})?$/;
if (cost_pattern.test(input.value)) { ... computeForm(input.form); }
else {
    window.alert("Houston, we have a problem - please get the offending character(s) out of your input.");
    window.setTimeout(function ( ) { input.focus( ); input.select( ); }, 100);
    return; }


The cost_pattern pattern allows the presence of
a starting $ and
digits running from the hundreds-thousands place* to the hundredths place and
a comma between a thousands place digit and a hundreds place digit and
a decimal point between the ones place digit and a tenths place digit
in the input.value.
Everything else ist verboten, ja?
*The annual tuition cost for some dental schools clears six figures.

For arithmetic purposes the inputs cannot contain a $ or a comma, but we can easily track down these characters and replace( ) them with empty strings via the following lines of regular expression-based code:

var charOK = /[$,]/g;
input.value = input.value.replace(charOK, "");


We presented a similar approach to the removal of a document's HTML tags in the A non-iterative 'detagification' section of Blog Entry #98.

Other code possibilities that prevent Netscape 2.x-3.x errors, in the name of completeness

Navigator 3.04 and Navigator 2.0 throw a syntax error as soon as they hit the
var cost_pattern = /^\$?(\d{1,6}|\d{1,3},\d{3})(\.\d{0,2})?$/; line,
even if that line is in the body of a not-called function or in a not-operative if or else block: we can suppress this error if we formulate the cost_pattern pattern via the RegExp object constructor

var cost_pattern2 = new RegExp("^\\$?(\\d{1,6}|\\d{1,3},\\d{3})(\\.\\d{0,2})?$");

and shield the regular expression code with an if (input.value.replace) { ... } gate.
Like regular expressions themselves, the replace( ) method of the String object was implemented in JavaScript 1.2.

I should also note that Navigator 3.04 and Navigator 2.0 similarly throw a syntax error as soon as they hit the window.setTimeout(function ( ) { input.focus( ); input.select( ); }, 100); line:
we can suppress this error if we
define a global var input2 = null; variable,
assign input2 = input; in the calc( ) body, and
recast the focus( )/select( ) action as window.setTimeout("input2.focus( ); input2.select( );", 100);.

Mozilla now discommends the setTimeout(codeString, delay) syntax, but again, we are accommodating late-1990s Netscape 2.x-3.x users here. I have no idea why the calc( ) and setTimeout( ) execution contexts converge in one case and diverge in the other.

Other inputs

For previous CCC applications we have generally intercepted blank, 0, and negative number Text.values.

A negative number is converted to its additive inverse by the for-Netscape 4.0 part of the validateData( ) function but is blocked - as it should be - by the not-for-Netscape 4.0 part and by my if (cost_pattern.test(input.value)) gate.

A 0 input is of course OK; a blank input is a 0-equivalent in an arithmetic context and is also OK, as it would be for a standard spreadsheet. The onchange="calc(this);" event handler is not triggered by leaving a field blank in the first place, although it would be triggered if we were to clear a non-blank field and then blur the field. The original chooser code handles the latter case satisfactorily:

(NN4.0) With Netscape 4.0x browsers, the now-empty input.value is mapped to 0 by the relevant parts of the validateData( ) and calc( ) functions, and we actually get a 0 in the field on the page when the dust has settled.

(!NN4.0) With non-Netscape 4.0x browsers, the input.value passes through the relevant parts of the calc( ) and validateData( ) functions without modification and is in almost all cases subsequently converted to 0 at the level of code but not on the page by the computeForm( ) function.

An empty input.value would be blocked by the if (cost_pattern.test(input.value)) gate: adding an || ! input.value.length subcondition to the gate will let it through.
We'll check over and retool the chooser's computeForm( ) and formClear( ) functions in the following entry.

Tuesday, November 12, 2019
 
Tuition Check
Blog Entry #403

We are at present discussing the JavaScript/Java Goodies College Tuition Chooser. We worked through the chooser's structure and presentational aspects in our last episode and are now ready to tackle the <script> code in the document head. The chooser JavaScript features five functions:
(1) A validateData( ) function vets the user's inputted cost values.
(2) For Netscape 4.0x users, a pow( ) function assists the validateData( ) function in dealing with the fractional part(s) of any cost values that have not been rounded to whole dollars.
(3) A computeForm( ) function adds up each column's cost values and displays the results in the Total Cost of Attendance fields in the tenth row.
(4) A calc( ) function serves as a gateway to, and coordinates, the validateData( )/computeForm( ) action.
(5) A formClear( ) function blanks all of the cost fields.
We'll cover the calc( ), validateData( ), and pow( ) functions in today's post.

So, suppose that Somewhere State University is on a shortlist of schools that Jane User is thinking of applying to. Jane calls up the College Tuition Chooser and attempts to type Somewhere State University into the first row's
<input type="text" name="title1" maxlength="20" size="15"> field
but only gets as far as the v because of the field's maxlength setting, so she uses an SSU abbreviation for the institution name. Jane knows from some online research that SSU's current annual tuition cost is $12,345.67, which she mistypes as $12,34r.67 in the second row's
<input type="text" name="a1" onchange="calc(this);" size="15"> field.
Jane clicks on the third row's
<input type="text" name="a2" onchange="calc(this);" size="15"> field,
thereby triggering the a1 field's onchange event handler, which calls the calc( ) function and passes thereto a this object reference to the a1 field.

function calc(input) { ... }

The a1 Text object is at the outset given an input identifier. What happens next depends on whether Jane is browsing with Netscape 4.0x or with some other browser.

if (agentInfo == "Netscape 4.0") { ... }

The agentInfo variable is defined prior to the five functions by:

var userAgent = navigator.appName + " " + navigator.appVersion;
var agentInfo = userAgent.substring(0, 12);


My SheepShaver browsers include Navigator 4.05, for which
navigator.appName returns Netscape and
navigator.appVersion returns 4.05 (Macintosh; I; 68K, Nav) and therefore
agentInfo is Netscape 4.0 - it checks out.

Let's assume, then, that it's October 1997 and that
Jane is as a matter of course browsing with Navigator 4.03 and therefore goes through the if (agentInfo == "Netscape 4.0") gate.
Subsequently, the calc( ) function calls the validateData( ) function and passes thereto the input.value, $12,34r.67.

input.value = validateData(input.value);

In this example, the validateData( ) function will convert the $12,34r.67 string to a 1234.67 number - not really a desired outcome, but that's what happens. The $12,34r.67 argument is given at first a theNum identifier and then a str identifier:

function validateData(theNum) {
    var str = theNum;


The rest of the validateData( ) function comprises an if...else statement whose if and else blocks serve Netscape 4.0x users and other browser users, respectively. The if block and its pow( ) function assistant are detailed below:

if (agentInfo == "Netscape 4.0") {
    var correctedNum = 0;
    var decimalPlace = 0;
    var addOn = "0";
    var isDecimal = false;
    for (var i = 0; i < str.length; i++) {
        var ch = str.substring(i, i + 1);
        if (((ch >= "0") && (ch <= "9")) && (! isDecimal)) {
            correctedNum *= 10;
            correctedNum += ch * 1; }
        else if (((ch >= "0") && (ch <= "9")) && (isDecimal)) {
            addOn += ch;
            ++decimalPlace; }
        else if (ch == ".") isDecimal = true;
        else if ((ch != ",") && (ch != "$")) window.alert("The invalid character " + ch + " was detected and ignored."); }
    if (decimalPlace > 0) correctedNum += addOn / pow(decimalPlace);
    return correctedNum; }

function pow(exp) {
    var returnTotal = 10;
    for (var j = 1; j < exp; j++) returnTotal *= 10;
    return returnTotal; }


Jane goes through the if (agentInfo == "Netscape 4.0") gate. Local correctedNum, decimalPlace, addOn, and isDecimal variables are initialized to the number 0, the number 0, a "0" string, and a boolean false, respectively. A for statement then checks over the str characters and transfers str's pre-decimal point digits to the correctedNum variable and its post-decimal point digits to the addOn variable - here's a rundown of what happens:

i = 0
The str.substring(i, i + 1) operation gets the zeroth str character, $, which is assigned to a ch variable. The ch $ doesn't go through any of the if...else if...else if...else if gates, and nothing is done with it. Regarding the ((ch >= "0") && (ch <= "9")) comparisons, note that $ lies outside of the 0-9 ASCII code position range; ditto for str's comma, r, and decimal point characters.

i = 1
The next str character, 1, is assigned to ch. The ch 1 goes through the if (((ch >= "0") && (ch <= "9")) && (! isDecimal)) gate and is then numberified by a * 1 multiplication and the resulting 1 number is added to correctedNum (01).

i = 2
The next str/ch character, 2, goes through the if (((ch >= "0") && (ch <= "9")) && (! isDecimal)) gate. The correctedNum *= 10; statement left correctedNum at 0 in the previous iteration; in this iteration, the statement's * 10 multiplication takes correctedNum's 1 to 10. The ch 2 is numberified and added to correctedNum (1012).

i = 3
The next str/ch character is the comma, which doesn't go through any of the if/else if gates, and nothing is done with it.

i = 4
The next str/ch character, 3, goes through the if (((ch >= "0") && (ch <= "9")) && (! isDecimal)) gate; the correctedNum *= 10; statement takes correctedNum's 12 to 120; the ch 3 is numberified and added to correctedNum (120123).

i = 5
The next str/ch character, 4, goes through the if (((ch >= "0") && (ch <= "9")) && (! isDecimal)) gate; the correctedNum *= 10; statement takes correctedNum's 123 to 1230; the ch 4 is numberified and added to correctedNum (12301234).

i = 6
The next str/ch character, r, goes through the else if ((ch != ",") && (ch != "$")) gate; Jane is alert( )ed that The invalid character r was detected and ignored., and she gives her .

i = 7
The next str/ch character is the decimal point, which goes through the else if (ch == ".") gate; the isDecimal boolean is toggled to true.

i = 8
The next str/ch character, 6, goes through the else if (((ch >= "0") && (ch <= "9")) && (isDecimal)) gate; the ch 6 is appended to addOn to give a 06 string; decimalPlace is incremented to 1.

i = 9
The last str/ch character, 7, goes through the else if (((ch >= "0") && (ch <= "9")) && (isDecimal)) gate; the ch 7 is appended to addOn to give a 067 string; decimalPlace is incremented to 2.

That's it for the loop. Control passes to the if (decimalPlace > 0) correctedNum += addOn / pow(decimalPlace); conditional, whose condition returns true and which pieces together the correctedNum and addOn data.

The if statement first calls the pow( ) function and passes thereto decimalPlace, which is given an exp identifier. The pow( ) function initializes a local returnTotal variable to 10 and then takes returnTotal to 100 via the * 10 multiplication of a one-iteration for (var j = 1; j < exp; j++) returnTotal *= 10; statement.

The exponentiated returnTotal is returned to the addOn / pow(decimalPlace) division, which gives the number 0.67, which is added to correctedNum to give 1234.67, which is returned to the calc( ) function, where it is assigned to the input.value.

The input.value = validateData(input.value); assignment is followed by a computeForm(input.form); call; we will discuss the computeForm( ) function in the following post.

Of course, Jane may not be using a Netscape 4.0x browser: she may be using Internet Explorer 4.0 or perhaps an earlier version of Netscape, or maybe it's not 1997 but it's 2019 and she's using a modern browser. In this case, she moves past the calc( ) function's if (agentInfo == "Netscape 4.0") block to a complementary else block, which itself initially calls and passes the input.value to the validateData( ) function:

else {
    if (validateData(input.value)) computeForm(input.form);


At the validateData( ) function, Jane moves past the if (agentInfo == "Netscape 4.0") block to a much more basic else block that also checks over the str characters but returns true if each character is either a digit or a decimal point or false if it runs into any other type of character.

else {
    for (var z = 0; z < str.length; z++) {
        var ch = str.substring(z, z + 1);
        if (((ch < "0") || (ch > "9")) && (ch != ".")) return false; }
    return true; } } // End of the validateData( ) function


A true return effects a computeForm(input.form) call. A false return pops up an "only numbers, please" alert( ) and sets the input.value to 0, which for $12,34r.67 happens as soon as we hit the $.

else {
    window.alert("Please do not use any characters besides numbers on this line. Your entry will be erased.");
    input.value = 0; } } // End of calc( )'s not-for-Netscape 4.0x else block

There are a number of things that we need to note about the above validation code, but those things can wait until the next entry.

Tuesday, October 22, 2019
 
Approaching Excel, Part 1
Blog Entry #402

All right, in today's post we'll begin a look at the CCC sector's College Tuition Chooser, which was created by Brian O'Keefe and went live in October 1997. The College Tuition Chooser codes a simple spreadsheet that stores and sums the various costs of attending 1-3 colleges or other institutions of learning.

The tuition.html page features a demo that works pretty well granted that
a large chunk of the chooser's JavaScript is meant for Netscape 4.0x and
the chooser's on-the-fly onchange addition action is fundamentally different from the =SUM(number1, number2, ...) formula input you'd use with a standard spreadsheet.
The chooser code may be accessed and saved at the corresponding tuition.txt page.

Layout and initial content

The chooser architecture commingles
a table having 11 rows and 41 cells and
a form holding 30 text inputs and a reset button.

<body>
<table cellspacing="0" cellpadding="0" width="100%" border="3"> ...
<form method="post" onsubmit="return false;"> ...
</form></table>


A form element can validly contain a table element but the converse is not true, and therefore the above form-in-table nesting should be inverted to a table-in-form nesting.

The cellspacing, cellpadding, and width attributes of the table element are entirely obsolete; the border attribute of the table element can be validly set to "" (the empty string) or 1 but is obsolete if set to any other value.

We won't be submitting any data to a processing agent, so the form element's method and onsubmit attributes should be removable. However, upon subtracting the onsubmit attribute and when using late-1990s versions of Internet Explorer,
clicking the enter/return key
after entering a value into a chooser text field
triggers a form submission attempt* that subsequently throws a The attempt to load 'pathname/tuition.html' has failed.-type error - at least that's what I see with IE 4.5 and IE 5.1.7 in the SheepShaver environment.
(*Our study of HTML Goodies' "Submit the Form Using Enter" tutorial made me suspect that something like this might happen.)
I have no idea if this occurs with Microsoft's recent Edge/IE browsers (I don't see it with Navigator 4.05, Communicator 4.61, or any other browser on my computer): in any case, I reckon there's no harm in keeping the submit cancelation to be on the safe side, but yes, we can at least lose the method setting.

At the top of the table we've got an Institutional Cost of Attendance Worksheet caption.

<table ...>
<caption><h3>Institutional Cost of Attendance Worksheet</h3></caption>


As shown, the caption text is also marked up with an h3 element, which would not have been valid in HTML 3.2 (the current version of HTML in October 1997) but is OK in HTML 5.

Each table row holds four cells except for the last row, which holds one cell. The table's first column contains a preset series of college cost headers (not marked up as such):
Name of Institution, Tuition, Fees, Books/Supplies, Board, Room, Personal/Recreation, Transportation, Other, Total Cost of Attendance

<tr><td>Name of Institution</td> ... </tr>
<tr><td>Tuition</td> ... </tr>
<tr><td>Fees</td> ... </tr> <!-- Etc. -->


The table's second, third, and fourth columns contain text boxes for entering values for the headers to their left.

<!-- 1st row, for the names of the institutions that you're thinking of applying to: -->
<td align="center">1.<input type="text" name="title1" maxlength="20" size="15"></td>
<td align="center">2.<input type="text" name="title2" maxlength="20" size="15"></td>
<td align="center">3.<input type="text" name="title3" maxlength="20" size="15"></td>
<!-- 2nd row, for the tuitions those institutions charge: -->
<td align="center">$<input type="text" name="a1" onchange="calc(this);" size="15"></td>
<td align="center">$<input type="text" name="b1" onchange="calc(this);" size="15"></td>
<td align="center">$<input type="text" name="c1" onchange="calc(this);" size="15"></td>
<!-- 3rd row, for the institutions' miscellaneous fees: -->
<td align="center"> &nbsp;&nbsp;<input type="text" name="a2" onchange="calc(this);" size="15"></td>
<td align="center"> &nbsp;&nbsp;<input type="text" name="b2" onchange="calc(this);" size="15"></td>
<td align="center"> &nbsp;&nbsp;<input type="text" name="c2" onchange="calc(this);" size="15"></td> <!-- Etc. -->


All of the input cells have an align="center" attribute that horizontally centers their contents and is now obsolete. All of the input text boxes have a size="15" attribute that effectively sets the width of the boxes and is still valid.

The table's last row holds the aforementioned reset button.

<tr><td colspan="4"><div align="center"><input type="button" value="RESET" onclick="formClear(this.form);" size="150"></div></td></tr>

Do we really need to put the button in a <div align="center"> in order to horizontally center it in the rows[10].cells[0] cell? I trust y'all know the answer to that one. The size="150" setting has no effect on the button width.

Style matters

My my, look at all of the W3C's recent CSS activity...

The table itself

The cellspacing="0" attribute maps onto a table { border-collapse: collapse; } style rule, which in the present case not only collapses td td and td table interspace but also shrinks the table/td border-widths by a pixel.

The cellpadding="0" attribute maps onto a td { padding: 0px; } style rule. If the cellpadding setting is removed and not replaced by a style rule (the initial padding value is 0), then some browsers will add 1-2px of padding to the table cells (the default cellpadding value is #IMPLIED), which you may be OK with.

The width="100%" attribute maps onto a table { width: 100%; } style rule - straightforward enough, eh? If the width setting is removed and not replaced by a style rule, then the table will have a shrink-to-fit width and much more closely resemble a standard spreadsheet.

As regards the border="3" attribute, a detailed, down-to-the-pixel discussion of my ultimately unsuccessful attempts to exactly replicate the original chooser borders with CSS would take up more space than it's worth, so let me cut to the chase and alternatively say that I like my borders solid and fully black versus the ridge-like and not fully black borders that we initially get, and therefore I'd go with table { border: solid 3px black; } and td { border: solid 2px black; } style rules for this one.

The table caption

The h3 markup bolds the table caption and increases the caption's font size by a few pixels
and it also puts an empty line box between the caption and the table's first row;
if you feel that the h3 markup is redundant or otherwise inappropriate but like its effects, then you can reproduce those effects with a
caption { font-weight: bold; font-size: 19px; margin-bottom: 18px; } style rule.

Header cells

If we mark up the header cells as th elements versus td elements, then the text in those cells will be bolded and horizontally centered: a th { font-weight: normal; text-align: left; } style rule returns us to the original rendering.

Data cells

We don't want all those align="center"s cluttering up the code, do we? Give the last row's cell an id="resetTd" and replace the align attributes with a td, #resetTd { text-align: center; } style rule. (If you keep the first-column headers in td cells, then you can use a tr > td:first-child { text-align: left; } rule to exclude those cells from the preceding rule.)

Something that didn't work:
The author didn't have access to the colgroup and col elements but we do, and I decided to check if a
<colgroup span="1"></colgroup>
<colgroup span="3" style="text-align:center;"></colgroup>

or
<colgroup><col span="1">
<col span="3" style="text-align:center;"></colgroup>

snippet would horizontally center the data cell content.
In the event(s), neither snippet had any effect at all, as could have been predicted.

We also have a relatively minor vertical alignment issue to sort out.
The first row's text boxes are preceded by 1., 2., and 3. labels, respectively.
The text boxes in the second and tenth rows are preceded by a $ symbol.
The text boxes in the remaining rows are preceded by an ASCII space** (which is ignored by the browser, per the last ¶ of this HTML 4.01 section) followed by two &nbsp; non-breaking spaces that shift the boxes slightly rightward so that they vertically line up with the $-marked boxes*** or not, depending on the browser's display font - for Times and Times New Roman, Arial, and Helvetica.
To the extent that you want to be a perfectionist about this, there are two fail-safe ways to vertically line up all of the text boxes exactly in the second, third, and fourth columns:
(1) Prepend a &nbsp; to each $ symbol so that each box is preceded by two renderable characters, and then set the table's font-family to a monospace font like Courier.
(2) Delete all of the pre-box characters.

**The ASCII space is not present in the rows[3].cells[1] cell.
***They don't quite line up with the #.-labeled boxes, which are 2-3 pixels to the right.

Input value alignment

Modern browsers will apply the text-align property to relevant input elements although this support is deemed experimental - see the last ¶ of this CSS 2.2 section.
Would you like to horizontally center the Name of Institution values and to right-justify the various cost values?
We can effect the former with a tbody > tr:first-child input { text-align: center; } style rule whereas
an input[type="text"] { text-align: right; } rule will handle the latter.

• As intimated above, the first-in-source-order tr element is the first child of an implicit tbody element; it's not a sibling of the caption element and consequently cannot be selected via a caption + tr next-sibling selector.
• I include an attribute selector in the latter rule so as to exclude the resetting push button in the last row.

The reset button

Per the preceding subsection, an input[type="button"] { width: 150px; } styling can be applied to the reset button if you do in fact want it to have a width of 150px for whatever reason.
We'll take on the chooser's JavaScript in the following entry.

Sunday, September 22, 2019
 
Approaching Quizmo, Part 3
Blog Entry #401

Today's post continues, and concludes, our Math Check discourse by presenting alternative approaches to
the generation of the numA and numB operands and
the prompt( )/confirm( )/alert( ) calculation interface.

Better difficulty levels

The A, B, and C difficulty levels should be mutually exclusive, but they're not.
As detailed in the Add it section of Blog Entry #399,
the
Level A
numA and numB range from 0 to 9,
the
Level B
numA and numB range from 0 to 29, and
the
Level C
numA and numB range from 0 to 59.

For all three levels, a multiply( ) run could serve up to the user a 0 * 0 = or 1 * 1 = problem, and that just wouldn't be right, would it? Clearly, it behooves us to equip Levels B and C with some minValues as well as maxValues. While we're at it, perhaps we should rejigger the maxValues somewhat as well - how many of you can carry out 59 * 58 = in your heads without recourse to pencil and paper?

Sticking with random( ) and ranom( ), getTime( ), and %

Suppose that for a Level B multiplication we want
numA to range from 11 to 30 and
numB to range from 2 to 10.
For our consolidated operate( ) function we can straightforwardly write:

if (document.rekenen.arithmetic[1].checked && opIndex == 2) {
    numA = random(20) + 11;
    numB = ranom(9) + 2; }


• The size of the numA range is 20. The numA range is initially 0-19: the + 11 shifts it to 11-30.
• The size of the numB range is 9. The numB range is initially 0-8: the + 2 shifts it to 2-10.
• We could specify and make use of maxValues and minValues here if we wanted to but it's not necessary.

Math.random( ) it

A new Date( ).getTime( ) return increases rapidly as time elapses, and modulo-ing that return does generate reasonably random numbers. That said, I prefer to generate random numbers with the Math object's random( ) method, whose raison d'être, after all, is to return a random number. Moreover, Math.random( ) was implemented for all platforms in JavaScript 1.1 and the Math Check authors could themselves have made use of it.

With help from the Math object's floor( ) method, we can smoothly Math.random( ) to the numA and numB ranges of the Level B multiplication example given earlier as follows:

if (document.rekenen.arithmetic[1].checked && opIndex == 2) {
    numA = Math.floor(Math.random( ) * 20) + 11;
    numB = Math.floor(Math.random( ) * 9) + 2; }


For the record, we've worked with such code once before: see the Animation #4 section of Blog Entry #210 vis-à-vis a discussion of HTML Goodies' "How to Create a JavaScript Animation" tutorial.

My preferred numA and numB ranges for the remaining levels and operations are specified in the Demo section at the end of the post.

A new dialogue

Several CCC sector scripts that we've covered previously feature prompt( ) dialogs;
when I provided new demos for these scripts, I recast the prompt( ) calls
as <label>ed <input type="text">s.
In crafting a new Math Check demo I similarly planned to convert
the Answer = window.prompt(numA + " " + opArray[opIndex] + " " + numB + " = ", 0); statement
to a <span id="equationSpan"></span> <input id="userInput" name="userInput" size="10" />
but then thought, "Why don't we do the equivalent with the confirm( ) and alert( ) calls? Just get it all to the page."

Structure

I divide all of the script information that the user deals with
after clicking the , , , or button
into discrete units of content and allocate those units to separate <div>s.

<div id="dialogDiv">
<div id="roundDiv">Please round your answer...</div><br />
<div id="equationDiv"><span id="equationSpan"></span> <input id="userInput" name="userInput" size="10" /></div><br />
<div id="promptButtonsDiv"><button id="promptButton1" type="button" onclick="cancel1( );">Cancel</button> <button id="promptButton2" type="button" onclick="checkInput( );">OK</button></div><br />
<div id="checkInputDiv"></div><br />
<div id="confirmButtonsDiv"><button id="confirmButton1" type="button" onclick="cancel2( );">Cancel</button> <button id="confirmButton2" type="button" onclick="checkAnswer( );">OK</button></div><br />
<div id="checkAnswerDiv"></div><br />
<div id="clearDiv"><button type="button" onclick="clearIt( );">Clear It</button></div><br />
<div id="scoreDiv"><button type="button" onclick="check( );">Check Score</button> <button type="button" onclick="score( );">Reset Score</button></div>
</div>


The and prompt( ) buttons are placed with a promptButtonsDiv div and the and confirm( ) buttons are placed with a confirmButtonsDiv div; the onclick cancel1( ), checkInput( ), cancel2( ), and checkAnswer( ) functions will be discussed shortly.

The Do you want your browser to check your answer... confirm( ) string will be housed in the checkInputDiv div and the ...check response (answer is correct|wrong, try again later)... alert( ) string will be housed in the checkAnswerDiv div.

I insert <br>s between the divs in order to double-space the units on the page.
I put the whole shebang in a dialogDiv div so I can collectively access the inner divs or brs via an Element.getElementsByTagName( ) command.

Clear it

I add a clearIt( ) function

<!-- I prefer to put this code at the very end of the document body, although you can window.addEventListener("load", function ( ) { ... }) it elsewhere in the document if you'd rather do that. -->
<script type="text/javascript">
var dialogDivs = document.getElementById("dialogDiv").getElementsByTagName("div");
var dialogBrs = document.getElementById("dialogDiv").getElementsByTagName("br");
function clearIt( ) {
    for (i = 0; i < dialogDivs.length - 1; i++) dialogDivs[i].style.display = "none";
    for (i = 0; i < dialogBrs.length; i++) dialogBrs[i].style.display = "none";
    document.getElementById("userInput").value = ""; }
clearIt( );
</script>
</body>


that clears all of the dialog content dynamically (after all, the alert( )/prompt( )/confirm( ) box content 'goes away' when a button on those boxes is clicked).
The clearIt( ) function is called
(1) when the page has finished loading,
(2) at the beginning of the operate( ) function, and
(3) by clicking a button that sits right above the and buttons.
Note that the button and its clearDiv div container are themselves cleared by the clearIt( ) function.

Initial dialog display

I visualize
the roundDiv div and its Please round your answer... alert string for a division,
the equationDiv div holding the equationSpan span and the userInput text box, and
the promptButtonsDiv and clearDiv divs and their buttons,
plus the divs' br nextSiblings,
with the operate( ) function.

...clearIt( ) call, numA and numB generation, numC calculation...
if (opIndex == 3) {
    numC = Math.round(numC);
    document.getElementById("roundDiv").style.display = "block";
    dialogBrs[0].style.display = "inline"; }
document.getElementById("equationDiv").style.display = "block";
dialogBrs[1].style.display = "inline";
document.getElementById("equationSpan").textContent = numA + " " + opArray[opIndex] + " " + numB + " " + "=";
...


Post-prompt responses

Clicking the promptButton2 button calls a checkInput( ) function, which
visualizes the checkInputDiv and confirmButtonsDiv divs and their br nextSiblings and
loads the Do you want your browser to check your answer... confirm string into the checkInputDiv div.

Clicking the promptButton1 button calls a cancel1( ) function, which
sets the userInput.value to "null", as befits a prompt( ) cancelation, and then
also runs the checkInput( ) function.

Post-confirm responses

Clicking the confirmButton2 button calls a checkAnswer( ) function that takes the place of the original ans( ) function. The checkAnswer( ) function
gets the userInput.value and assigns it to Answer,
visualizes the checkAnswerDiv div and its br nextSibling,
compares the Answer and numC values,
increments the correct or wrong tally as appropriate, and
writes the ...answer is correct|wrong... alert string to the checkAnswerDiv div.

Clicking the confirmButton1 button calls a cancel2( ) function, which
visualizes the checkAnswerDiv div and its br nextSibling and
loads the Please try again later! alert string into the checkAnswerDiv div.

A bell that whistles

Lastly, I display a or image to the right of the userInput field if the Answer and numC values are equal or not equal, respectively.

<div id="equationDiv">... <img id="check_or_x" src="" alt="" style="vertical-align:middle;" /></div>

// In the checkAnswer( ) function:
document.getElementById("check_or_x").style.display = "inline";
document.getElementById("check_or_x").src = Answer == numC ? "pathname/check.gif" : "pathname/x.gif";
/* In practice in the demo below, the check.gif and x.gif assignments are in separate if (Answer == numC) and else clauses, although you can certainly go the ?: route if you'd rather do that. */

// In the clearIt( ) function:
document.getElementById("check_or_x").style.display = "none";


Provenance-wise the images are part of the liveforms/ package that supplements WebReference.com's "Bring Your Forms to Life With JavaScript" tutorial, which we previously dissected in Blog Entries #202, #203, #204, #205, #206, #207, and #208. (The www.webreference.com links in these entries are dead; if you are interested in this tutorial, then go to the aforelinked www.webreference.com resource and take it from there.)

Demo

The preceding sections are distilled into the demo below - check the page source for the full coding.

IMPROVE YOUR ARITHMETIC SKILLS

USEFUL FOR THE AGES 3 TO 103



Level A Level B Level C

Please round your answer to the nearest integer (round up for x.5-or-higher, round down for x.4-or-lower).








Difficulty level operand ranges
Level A

add, subtract, and multiply: numA and numB run from 2 to 10
divide: numA runs from 11 to 30, numB runs from 2 to 10
Level B

add and subtract: numA and numB run from 11 to 30
multiply: numA runs from 11 to 30, numB runs from 2 to 10
divide: numA runs from 31 to 60, numB runs from 2 to 10
Level C

add and subtract: numA and numB run from 31 to 60
multiply: numA and numB run from 11 to 30
divide: numA runs from 61 to 100, numB runs from 11 to 30
I trust you are up to the task of 'upping the ante' if my levels are too easy for your taste.

Your assignment
I've left the original score( ) and check( ) functions alone. Write the check( ) function's alert( ) message, with its running correct and wrong counts, to a <span id="scoreSpan"></span> on the page.

Wednesday, June 19, 2019
 
Approaching Quizmo, Part 2
Blog Entry #400

Welcome back to our ongoing deconstruction of the Java/JavaScript Goodies Math Check. In the previous post we walked through the checker's addition functionality and we now turn our attention to its subtraction, multiplication, and division code.

Subtract, multiply, divide

Clicking the rekenen form's , , and buttons calls subtract( ), multiply( ), and divide( ) functions, respectively. The subtract( ) and multiply( ) functions are exactly like the add( ) function except they've got - and * operators in place of the + operator; the divide( ) function largely parallels the add( ) function although it does feature some noteworthy differences.

Recall that the Math Check metatext includes a This page is best viewed with Netscape 3.0 line. With Navigator 3.04 on my iMac, x / 0 for x 0 and 0 / 0 both return NaN; the former division returns Infinity with Netscape 4+ and all modern browsers. These divisions are legitimate but their usefulness leaves something to be desired vis-à-vis testing the user's division skills: the divide( ) function staves them off by adding one to the random( ) and ranom( ) % remainders.

numA = random(maxValue) + 1;
numB = ranom(maxValue) + 1;


The divide( ) function subsequently carries out a numA / numB division for which the numA dividend is greater than or equal to the numB divisor; if a first numA = random(maxValue) + 1; is less than a first numB = ranom(maxValue) + 1;, then divide( ) is re-called as many times as needed until numAnumB.

if (numA < numB) { divide( ); }
numC = numA / numB;


The numC quotient is round( )-ed to the nearest integer; a following alert( ) asks the user to follow suit.

numC = Math.round(numC);
window.alert("Please round your answer off ?\n.5 or higher one number up\n.4 or lower one number down");


Each divide( )/ans( ) run therefore displays four dialogs:

(1) window.alert("Please round your answer...");
(2) window.prompt(numA + "/" + numB + " = ", 0);
(3) window.confirm("Do you want your browser to check your answer...");
(4) window.alert(...check response (answer is correct|wrong, try again later)...);

If it takes three divide( ) calls to obtain a numAnumB, then the user will be subjected to twelve dialogs for the same, ultimate numA / numB division, and we don't want that; this situation is easily cleaned up by placing a return statement after the divide( ) re-call so as to end the current divide( ) run.

if (numA < numB) { divide( ); return; }

Alternatively and preferably, we can lose the divide( ) re-call and just exchange the numA and numB values via:

if (numA < numB) {
    var divisor = numA;
    numA = numB;
    numB = divisor; }


Function consolidation

Do we really need separate add( ), subtract( ), multiply( ), and divide( ) functions? Nah. Upon setting up a

var opArray = ["+", "-", "*", "/"];

operator string Array (N.B. a var opArray = [+, -, *, /]; Array of non-stringified operators throws an expected expression, got ',' SyntaxError) the four functions can be formulated as a single operate( ) function that is operator-differentiated by an opIndex parameter (0 for addition, 1 for subtraction, etc.) and whose operator characters are fetched by an opArray[opIndex] expression:

function operate(opIndex) {
    ...beginning maxValue, numA, and numB assignments...
    if (opIndex == 3) { numA++; numB++; if (numA < numB) { ...numAnumB exchange... } }
    numC = eval(numA + opArray[opIndex] + numB);
    if (opIndex == 3) { numC = Math.round(numC); window.alert("Please round your answer..."); }
    Answer = window.prompt(numA + " " + opArray[opIndex] + " " + numB + " = ", 0);
    if (window.confirm("Do you want your browser to check your answer to the problem " + numA + " " + opArray[opIndex] + " " + numB + "?")) ans( );
    else window.alert("Please try again later!"); }


Note that the numC = numA +|-|*|/ numB operations are merged via the somewhat-notorious eval( ) function; if the use of eval( ) here bothers you for whatever reason, you can write out the operations 'in longhand' instead per the original code:

if (opIndex == 0) numC = numA + numB;
if (opIndex == 1) numC = numA - numB;
if (opIndex == 2) numC = numA * numB;
if (opIndex == 3) numC = numA / numB;


One last function

Clicking the button calls a check( ) function that displays the user's correct and wrong tallies on an alert( ) box.

function check( ) {
    window.alert("YOUR SCORE\n : " + correct + " correct\n : " + wrong + " incorrect"); }


The user can problem-wise flit freely between
the add, subtract, multiply, and divide operations and
the A, B, and C difficulty levels
and check( ) will score-wise keep track of it all.
In the next entry we'll revisit the random generation of numA and numB and then wrap things up with a refurbished demo.

Saturday, May 25, 2019
 
Approaching Quizmo
Blog Entry #399

Let's move on now to the next Calculators, Calendars, and Clocks sector item, that being Math Check, which went live in October 1997 and comes to us courtesy of Binoculars V.O.F.

Math Check assembles and presents to the user arithmetic problems having difficulty levels A < B < C and whose integer operands are themselves randomly generated;
per its title, it can check the user's answers to these problems against its own answers
and keep a running tally of correct and wrong user answers.
The /JSBook/mathcheck.html page does not feature a Grab the Script link although the Math Check code can be accessed at the corresponding mathcheck.txt page. The mathcheck.html demo works OK for the most part: its division module is somewhat buggy for a reason we will detail below.

Initial display

Before we do any arithmetic, we initially see on the mathcheck.html page
some metatext

IMPROVE YOUR ARITHMETIC SKILLS
USEFUL FOR THE AGES 3 TO 103
This page is best viewed with Netscape 3.0


<h4><center>IMPROVE YOUR ARITHMETIC SKILLS</center></h4>
<h4><center>USEFUL FOR THE AGES 3 TO 103</center></h4>
<center><h4>This page is best viewed with Netscape 3.0</center></h4>


and an series of push buttons

<center><form name="rekenen">
<input type="button" value="add" onclick="add( );">
<input type="button" value="subtract" onclick="subtract( );">
<input type="button" value="multiply" onclick="multiply( );">
<input type="button" value="divide" onclick="divide( );">


and a
Level A Level B Level C
set of radio buttons

<input type="radio" name="arithmetic">Level A
<input type="radio" name="arithmetic" checked>Level B
<input type="radio" name="arithmetic">Level C


and a push button.

<input type="button" value="Check Score" onclick="check( );">

We should also see a push button to the right of the button but it doesn't show up because of a coding typo.

<inputtype="button" value="Reset Score" onclick="score( );">

HTML notes

• As detailed above, the metatext segments are structurally marked up as <h4> headings. Are those lines headings? They're not headings. Code them as a <br>-newlining <strong>, good enough.

• The authors used four separate <center>s to horizontally center the metatext and the line boxes of the rekenen form (FYI, the h# elements can't validly have block-level children); just one <center> would have sufficed.

<body onload="score( );" bgcolor="#ffffff" text="#000000">
<center> ...metatext + form code... </center>
</body>


Our baseline

A score( ) function sets correct and wrong variables to 0 when the mathcheck.html page has loaded.

function score( ) {
    correct = 0;
    wrong = 0; }


Add it

It's time to get the math checking under way and put our arithmetic skills to the test, yes? We click the button, thereby calling an add( ) function, which will create and display an addition problem having two operands. The add( ) function's first order of business is to set some maxValue upper boundaries for the operands:

function add( ) {
    if (document.rekenen.arithmetic[0].checked) maxValue = 10;
    else {
        if (document.rekenen.arithmetic[1].checked) maxValue = 30;
        else { maxValue = 60; } } ...


For a
Level A
problem, the operands will be less than 10;
for a
Level B
problem, they'll be less than 30;
for a
Level C
problem, they'll be less than 60.
In all three cases, the operand range runs up to but does not reach the maxValue value, per the following code.

The operands themselves are separately generated by external random( ) and ranom( ) functions.

numA = random(maxValue);
numB = ranom(maxValue);

function random(maxValue) {
    day = new Date( );
    hour = day.getHours( );
    min = day.getMinutes( );
    sec = day.getSeconds( );
    mili = day.getTime( );
    return(((hour * 3600) + (min * 60) + (sec) + mili) % maxValue); }

function ranom(maxValue) {
    day = new Date( );
    mil = day.getTime( );
    return((mil) % maxValue); }


Both functions use a new Date( )-data % maxValue division/remainder operation to give a random (pseudorandom?) number in the range 0 to maxValue - 1, inclusive; regarding these operations,
the random( ) dividend is the number of seconds that have elapsed since the beginning of the current day plus the getTime( ) number of milliseconds that have elapsed since 1970-01-01T00:00:00.000Z whereas
the ranom( ) dividend is just the getTime( ) millisecond count.
The % remainders are returned to the add( ) function and respectively assigned to numA and numB variables.
(We previously encountered a var num = new Date( ).getSeconds( ) % 10 creation of random numbers in HTML Goodies' JavaScript Primers #20, which was discussed in Blog Entry #36.)

Back at the add( ) function,
numA and numB are added
and the resulting sum is stored in a numC variable.

numC = numA + numB;

Next, add( ) concatenates numA, +, numB, and  =  and displays the resulting addition problem string on a prompt( ) box; the prompt( )'s default input value is initially set to 0.

Answer = window.prompt(numA + "+" + numB + " = ", 0);

The user does or does not enter a value into the box's input field and then clicks the box's or button; the prompt( ) output is assigned to an Answer variable.

Is the Answer correct? Up pops an if-conditioned confirm( ) box whose message asks
Do you want your browser
 to check the answer you gave to the problem numA + numB
?

if (window.confirm("Do you want your browser\n to check the answer you gave to the problem " + numA + " + " + numB))
    ans( );
else { window.alert("Please try again later!"); } } // End of add( ) function


Clicking the box's button displays a Please try again later! alert( ). Clicking the box's button calls an external ans( ) function that compares the Answer and numC values.

function ans( ) {
    if (Answer == numC) {
        correct == correct++;
        window.alert("Congratulations your answer is correct."); }
    else {
        wrong == wrong++;
        window.alert("The answer " + Answer + " that you gave is wrong. The correct answer = " + numC); } }


If Answer and numC are equal, then the correct count is increased by one and the user gets a Congratulations your answer is correct. alert( ); if they're not equal, then the wrong count is increased by one and the user gets a The answer  Answer  that you gave is wrong. The correct answer = numC alert( ).

The correct == correct++; and wrong == wrong++; lines are kind of weird - if I understand the postfix increment operator correctly, they initially compare correct and wrong with themselves (thereby returning true, although nothing is done with those trues) and then increment correct and wrong - clearly, correct++; and wrong++; are all we need here.
We'll get to the rest of the mathcheck.html JavaScript in the following entry.


Powered by Blogger

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