reptile7's JavaScript blog
Tuesday, December 31, 2019
 
Adding and Clearing Columns of Numbers
Blog Entry #405

We've got two more College Tuition Chooser functions to put under the microscope, they being computeForm( ) and formClear( ). Before we get started, however, let's review a bit...

Calling the computeForm( ) function

Like the validateData( ) function, the computeForm( ) function is called from the calc( ) function, which is redetailed below in its entirety.

function calc(input) {
    if (agentInfo == "Netscape 4.0") {
        input.value = validateData(input.value);
        computeForm(input.form); }
    else {
        if (validateData(input.value)) computeForm(input.form);
        else {
            window.alert("Please do not use any characters besides numbers on this line. Your entry will be erased.");
            input.value = 0; } } }


For Netscape 4.0x users, every calc(this) call ultimately effects a computeForm(input.form) call. There are no input.values for which this does not happen: if the user's a1 input is hello world, then the validateData( ) function's correctedNum number stays at 0, which is returned to the calc( ) function and then passed on to the computeForm( ) function. The validateData( ) function does discard arithmetically unacceptable input characters but in no cases is the user alert( )ed that a given input itself may be unacceptable.

For non-Netscape 4.0x users, a calc(this) call effects a computeForm(input.form) call only if the input.value contains only digit and decimal point characters or is blank. When the not-for-Netscape 4.0 part of the validateData( ) function runs into an unapproved ch character, the not-for-Netscape 4.0 part of the calc( ) function alert( )s the user accordingly and sets the input.value to 0, and no further processing occurs.

Taking everything into account, the not-for-Netscape 4.0 approach is in my book better than the for-Netscape 4.0 approach: a problematic input should be blocked (if not erased) before any addition gets under way.

Adding it all up

Suppose we want to add a series of eight numbers via an OpenOffice spreadsheet.
We type the numbers individually into the spreadsheet's A1...A8 cells and then give focus to the A9 cell.
We access the spreadsheet's Insert menu → Function... Wizard.
We select the SUM option from the wizard's FunctionsFunction menu.
We click the button at the bottom of the wizard to input an =SUM( ) pre-formula into the wizard's Formula box.
We plug an A1:A8 parameter range into the =SUM( ) pre-formula.
We click the wizard's button to post the completed =SUM(A1:A8) formula and resulting sum to the A9 cell.

The College Tuition Chooser doesn't wait for a full column of costs to get summing, however. Per the above calc( ) code, each post-validation input.value fires the computeForm( ) function, which discretely sums the cost values for the a# and b# and c# columns and outputs the totals in the a9 and b9 and c9 fields, respectively.

function computeForm(form) {
    form.a9.value = form.a1.value * 1 + form.a2.value * 1 + form.a3.value * 1 + form.a4.value * 1 + form.a5.value * 1 + form.a6.value * 1 + form.a7.value * 1 + form.a8.value * 1;
    form.b9.value = form.b1.value * 1 + form.b2.value * 1 + form.b3.value * 1 + form.b4.value * 1 + form.b5.value * 1 + form.b6.value * 1 + form.b7.value * 1 + form.b8.value * 1;
    form.c9.value = form.c1.value * 1 + form.c2.value * 1 + form.c3.value * 1 + form.c4.value * 1 + form.c5.value * 1 + form.c6.value * 1 + form.c7.value * 1 + form.c8.value * 1; }


The various values are numberized for arithmetic addition via * 1 multiplications (otherwise they'd be concatenated); importantly, blanks are mapped to 0s for those multiplications by almost all browsers. A 10000 a1 input therefore gives a 10000 a9 output but it also gives a 0 b9 output and a 0 c9 output.

Total Cost of Attendance$$$

The calc( )/computeForm( ) functionality updates the a9 and b9 and c9 values automatically as the user fills in the chooser form.

The computeForm( ) statements clash with my organizational bent. Why are we writing out all those terms? I like to do things iteratively whenever possible, so here's how I would carry out the addition:

var columnID = new Array("a", "b", "c"); /* Netscape 3.x doesn't support array literals. */
for (var i = 0; i < columnID.length; i++) {
    var runningfieldtotal = 0;
    for (var j = 1; j < 9; j++)
        runningfieldtotal += Number(form.elements[columnID[i] + j].value); /* Number("") returns 0. */
    form.elements[columnID[i] + 9].value = runningfieldtotal; }


Moreover, an array-loop approach would make it much easier to scale the addition if we were to add more columns and rows to the chooser.

Netscape 2.x and the computeForm( ) function

Navigator 2.0 does not map blanks to 0s in the computeForm( ) function but instead throws a value is not a numeric literal error as soon as it hits a "" * 1 multiplication - at least that's what I see on my iMac. I'm sure that some people were still using Netscape 2.x when the chooser went live, and those users could have been accommodated by the preceding for-based addition code upon making a few simple modifications thereto:
(1) Use an Object object to array the a and b and c column identifiers.
(2) Explicitly convert blanks to 0s before attempting to add them.
(3) Numberize the cost values with the eval( ) function.

var columnID = new Object( ); /* Netscape 2.x doesn't support the Array object at all. */
columnID[0] = "a";
columnID[1] = "b";
columnID[2] = "c";
columnID.length = 3;
for (var i = 0; i < columnID.length; i++) {
    var runningfieldtotal = 0;
    for (var j = 1; j < 9; j++) {
        if (! form.elements[columnID[i] + j].value.length) form.elements[columnID[i] + j].value = 0;
        runningfieldtotal += eval(form.elements[columnID[i] + j].value); } /* eval("") returns undefined. */
    form.elements[columnID[i] + 9].value = runningfieldtotal; }


Actually, there's no reason why we couldn't use this code with modern browsers; re the danger of using eval( ), malicious code inputted by a malicious user would be blocked at the validation stage.

The form.elements[columnID[i] + j].value = 0 assignments load 0s into the rows[1]-rows[8] non-input cost fields; if you don't like that (I don't care for it myself), then you can blank those fields with:

var form2 = null;
function computeForm(form) {
    ...addition code...
    form2 = form;
    window.setTimeout("for (var i = 3; i < 27; i++) if (form2.elements[i].value == '0') form2.elements[i].value = '';", 10); }


One column at a time

If you would prefer to add up just one column of costs at a time à la a standard spreadsheet, then that's pretty easy to arrange:

function computeForm(input) {
    var columnID = input.name.charAt(0);
    var runningfieldtotal = 0;
    for (var i = 1; i < 9; i++)
        runningfieldtotal += Number(input.form.elements[columnID + i].value);
    input.form.elements[columnID + 9].value = runningfieldtotal; }

/* In the calc( ) function, if the input.value passes a regexp validation or is blank: */
if (cost_pattern.test(input.value) || ! input.value.length)
    computeForm(input);


I trust you are up to the task of modifying this computeForm( ) version for all of the Netscape 2.x users out there. ;-)

Back to the beginning

The chooser's formClear( ) function empties all of the cost fields, including the Total Cost of Attendance fields, and is called by clicking the button at the bottom of the display.

function formClear(form) {
    form.a1.value = ""; form.a2.value = ""; form.a3.value = ""; form.a4.value = "";
    form.a5.value = ""; form.a6.value = ""; form.a7.value = ""; form.a8.value = "";
    form.a9.value = ""; form.b1.value = ""; form.b2.value = ""; form.b3.value = "";
    form.b4.value = ""; form.b5.value = ""; form.b6.value = ""; form.b7.value = "";
    form.b8.value = ""; form.b9.value = ""; form.c1.value = ""; form.c2.value = "";
    form.c3.value = ""; form.c4.value = ""; form.c5.value = ""; form.c6.value = "";
    form.c7.value = ""; form.c8.value = ""; form.c9.value = ""; }


Again, there is no need to process the fields individually when we can do so iteratively, in this case with just one line of code:

for (var i = 3; i < 30; i++) form.elements[i].value = "";

To clear the Name of Institution fields as well, we can supplement the button with a traditional reset button:

<tr><td id="resetTd" colspan="4">
<input type="button" value="Just Clear the Numbers" onclick="formClear(this.form);">
<input type="reset" value="Clear All Fields">
</td></tr>

We're almost ready to wrap up our College Tuition Chooser discourse. I would still like to add an =AVERAGE(number1, number2, ...) capability to the chooser and then provide a demo along with some summary commentary, and I'll do so in the following entry; depending on how long that takes, I may also discuss an alternative, JavaScript-based approach to the creation of a spreadsheet template that I've been working on.

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 integer 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 hundred-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.


Powered by Blogger

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