reptile7's JavaScript blog Friday, July 02, 2021

Reflected Through Zero
Blog Entry #415

So, what's left in our Super Calculator odyssey?
Some of the SC's remaining features - specifically, the key and the Memory keys - are normal calculator fare
although some of them - specifically, the Scientific Notation, Finding "x", and Fractions keys - are kind of odd;
my inclination is to discuss the former in detail
and skim through the latter.

Going from one side of the number line to the other

Nestled between the SC's and keys
is a key

<input type="button" value=" +/- " onclick="getinput(negpos);">

that is indirectly bound to a negpos( ) function

function getinput(func) {
var a = document.mainform.total; ...
var mode = document.mainform.mode.checked ? 1 : 0; ...
if (func == negpos) { return negpos(mode, a); } ... }

// Change the sign of any given number
function negpos(mode, obj) {
if (mode == 1) { window.alert("+/- allows the [sic] you to convert a negative(-) number to a positive(+) and a positive number to a negative. And then it puts it in the total text box with any other number that is already in there."); }
obj.value *= (-1); }

that converts the total field's value to its additive inverse by multiplying it by -1.

Unlike the SC's exponentiation and trigonometry functions, the negpos( ) function does not trouble the user with a "Do you want to use the current number in the total text box for this operation?"-type prompt( ),
nor does it call on the doit( ) and more( ) functions to load the new obj.value into the total field.

The negpos( ) help message's second sentence seems to say that the new total value is appended to the starting total value, which does not happen: the former replaces the latter.

If the initial obj.value can be converted to a number for the * (-1) multiplication, then all is well, but if not, then the negpos( ) output is NaN - more on this below.

Other inversions

Jamie Beyore's Another Great Science Calculator has a key but not a key.
Saries's Rainbow Calculator has a key and a key; the former is directly bound to a change( ) function, which I did not discuss in Blog Entry #74, so let's take a quick look at it now:

function change( ) {
var temp = document.calculator.text.value;
if (temp.substring(0, 1) == "-") {
document.calculator.list.value = "";
document.calculator.text.value = 0 - document.calculator.text.value * 1; }
if (temp.substring(0, 1) != "-") {
document.calculator.list.value = "";
document.calculator.text.value = "-" + temp.substring(0, temp.length); } }

<form name="calculator"> ...
<input name="list" type="hidden"> <!-- The list field serves no purpose. -->
<input type="text" name="text" value=""> ...
<input type="button" value=" +|- " name="sign" onclick="change( );"> ... </form>

If the starting input is a negative number,
then the text field's value is unnecessarily numberified via a * 1 operation
and the resulting number is subtracted from 0;
this approach would work with positive inputs as well.

If the starting input is a positive number,
then a - character is prepended to the text value;
this approach would be problematic for a negative input in that, e.g.,
-5 would be converted to --5
and eval("--5"), effected by clicking the key, would throw a
SyntaxError: invalid decrement operand.
N.B. The ++ and -- operators cannot be used with numeric literal operands - see Stack Overflow's "Why can't we increment (++) or decrement (--) number literals" page - there is no discussion of this in Mozilla's Increment (++) and Decrement (--) documentation.

The former approach is semantically consistent with what we are trying to do, and we can fairly recast the change( ) function as:

function change( ) {
document.calculator.text.value = 0 - document.calculator.text.value; }

Both change( ) functions and the negpos( ) function are adequate for a numeric input, but what if we want to +/- a more complex expression?

Parentheses and other interlopers, revisited

Back at the SC, suppose we type 2*(3+4) in the total field and it.
The negpos( ) function returns NaN in this case because, as alluded to earlier, the presence of even one nonnumber operator character (let alone four such characters) in the initial obj.value kills the * (-1) multiplication.
In contrast, Saries's calculator smoothly s 2*(3+4) to -2*(3+4) via the second if clause of the original change( ) function;
clicking the key then yields -14.
However, an attempted 0 - "2*(3+4)" subtraction with my revised change( ) function returns NaN.

Now suppose we want to +/- 3+4.
For this case, the negpos( ) and revised change( ) functions again return NaN
whereas the original change( ) function returns -3+4,
which is equally unacceptable as we want the return to eval( ) to -7 rather than 1.
We can circumvent these results in a hybrid way via:

/* For the change( ) function, var obj = document.calculator.text; */
if (isNaN(obj.value)) obj.value = "-(" + obj.value + ")";
else obj.value = 0 - obj.value;

The if clause effectively parenthesizes the 3+4 input before prefixing it with a -; numeric inputs go through the else clause.

Alternatively if not quite preferably, we can eval( ) this type of input on the spot and subsequently subtract the eval( )ed value from 0:

if (isNaN(obj.value)) obj.value = eval(obj.value);
obj.value = 0 - obj.value;

This code takes 2*(3+4) to -14 and 3+4 to -7; it'll handle most mathematical expressions containing nonnumber characters (it'll take an entered Math.log(1024)/Math.log(2) to -10, e.g.) although it balks at the number1(number2) expressions that are generated by the else clause of the more( ) function in collaboration with a number of other SC functions.

Lagniappe: number1(number2) multiplication

Suppose we type 2 in the SC's total field.
We click the key and suddenly decide that we want to raise 3 to the 3rd power;
our final return is 2(27) when the power( ) and more( ) functions have finished executing.

A user who is conversant with basic mathematical conventions would understand the 2(27) expression as the multiplication of 2 and 27, but JavaScript doesn't see it that way. Attempted eval( )uation of our 2(27) by clicking the key throws a
TypeError: 2 is not a function.
(A function identifier can have one or more digits in it, but it can't just be a number.)

The Google calculator widget and the Calculator.net calculator will both carry out a number1(number2) multiplication, however, so maybe we should do that too. The retooled calc( ) function below (cf. the original here) will get us there:

function calc(obj) {
var mult_expr, exprArray;
mult_expr = /^([+-]?\d*\.?\d*)\(([+-]?\d*\.?\d*)\)\$/;
if (! mult_expr.test(obj.value)) obj.value = eval(obj.value);
else {
exprArray = mult_expr.exec(obj.value);
if (exprArray) obj.value = exprArray * exprArray;
else obj.value = eval(obj.value); } }

<button type="button" onclick="calc(document.mainform.total);"> = </button>

The mult_expr regular expression

var mult_expr = /^([+-]?\d*\.?\d*)\(([+-]?\d*\.?\d*)\)\$/;

matches a number1(number2) expression whose number1 and number2 numbers
may be signed with a + or a - ([+-]?),
have 0 or more integer digits (\d*),
may contain a decimal point (\.?), and
have 0 or more fractional digits.
Noteworthily, the latter expression's decimal points, left parenthesis, and right parenthesis would normally be regexp metacharacters and must therefore be literalized with a backslash for the former expression.
Furthermore, the number1 and number2 zones in the mult_expr pattern are parenthesized so that we can remember them.

As detailed above, we can use the test( ) method of the RegExp object to separate a normal number1*number2 multiplication from a number1(number2) multiplication.

(f) For the former multiplication, the total value and the mult_expr pattern do not match and so ! mult_expr.test(obj.value) returns true, and the obj.value is accordingly eval( )ed as in the original calc( ) function.

(l) For the latter multiplication, the total value and the mult_expr pattern do match and so ! mult_expr.test(obj.value) returns false; toward the end of getting our hands on the number1 and number2 numbers, the match is subsequently parsed via the exec( ) method of the RegExp object.

Although we haven't made use of it previously, the exec( ) method is analogous to the match( ) method of the String object, which we discussed in Blog Entry #222.
The mult_expr.exec(obj.value) call returns an array - let's call it exprArray - whose exprArray and exprArray elements
hold the parenthesized substring matches for the number1 and number2 zones respectively and
can be multiplied to give us the product we want -
there, that wasn't so hard, was it, weekend silicon warriors?

The (3.141592653589793) string outputted by the SC's key also matches the mult_expr pattern; for this match, exprArray holds the empty string, so the exprArray * exprArray multiplication is shielded by an if (exprArray) gate
and an accompanying else obj.value = eval(obj.value); clause takes (3.141592653589793) to 3.141592653589793.

Another remembering

I was originally going to carry out the number1(number2) multiplication with a statement that calls on the \$1 and \$2 properties of the predefined RegExp object to remember the parenthesized substring matches of a mult_expr.test(obj.value) match.

if (mult_expr.test(obj.value))
obj.value = RegExp.\$1 ? RegExp.\$1 * RegExp.\$2 : eval(obj.value);

However, the RegExp.\$1-\$9 properties were deprecated as of JavaScript 1.5, and Mozilla warns, These deprecated features can still be used, but should be used with caution because they are expected to be removed entirely sometime in the future. You should work to remove their use from your code.
Sounds like we should stick with the exec( ) thing, wouldn't you say?

The errors of eval( )

As intimated above, the attempted eval( )uation of an inappropriate input throws an error of some sort. We can display the SC's eval( )-generated errors to the user (vs. letting them languish in the browser's debugging console) via a try...catch statement:

try { /* calc( ) conditional code */ }
catch (err) { window.alert(err.name + ":\n" + err.message); }