JavaScript Calculator - CodeProject

:

  1. Sentiments
  2. Main Idea
  3. Additional Capabilities and Console
  4. Exception Handling
  5. Handling Lexical Errors
  6. Strict Mode
  7. Dynamic Strict Mode Switching and Web Storage
  8. Layout
  9. Versions
  10. Conclusions

1 Sentiments

Few things amaze me more than those software calculators with buttons, simulating historical calculators of mid-1970s. How typical for human civilization and, in particular, our technosphere!

Such calculators carry the clear message to the regular non-programming users: "Even though you leave in the era of voice and handwritten recognition and computer-rendered movies and video communications, when you use a calculator, you are reduced to a loser allowed to see only tiny one-line screen showing just few digits, you should click operation buttons seeing no feedback and cannot edit or even see your expression you just entered".

The inertia of human thinking here is amazing. I can understand that computer keyboards mimicked old mechanical keyboards. After all, human muscle memory can be very strong. But what human habits can justify those calculators? I was the one who had to use even those small "scientific" (what a word!) calculators. and just suffered from them, I used to use even the "programming calculators". What could be more natural than just typing (2 + 2)*4, being able to edit any part of expression, just because it is not overwriting with the result or, worse, other operands?

What about "calculators with expressions"? Oh, they are more complicated. One needs to scan and parse the code, interpret each expression and sub-expression and, eventually, evaluate the results. But isn't it a usual part of any non-nonsense course for the students of programming specializations, and a useful exercises?

But these days, even that is not needed in practice. Everyone can create a simple calculator with expressions in no time, using already available environment. This could be a Web browser.

2 Main Idea

In a way, JavaScript calculator already exists. Enter some text input and call eval(input), the return value will be the result of calculation which you can output somewhere. It can be even done directly in the browser's address line:

javascript:alert(eval("2+2"))

This will show a message box and output 4. Any valid JavaScript calculation will work this way. The only remaining work would be to provide input and output controls, "Evaluate" button and put it together in the simplest event workflow: take input, pass it to eval, and output the returned result.

I had some calculator based in this simple idea on my site, mostly for may own convenience, but only recently found some time to learn a bit more of JavaScript and improve the implementation. Anyone can give it a try on my site (found on my CodeProject profile page), without downloading anything. It's all in one file (not counting help file), so the page can also be downloaded and work immediately.

3 Additional Capabilities and Console

Very basic calculations can look like

2 * 2

With variables:

x = 256
x/2

With embedded mathematical functions:

pow(3, 4) + 1

It is obvious that the input code can be any JavaScript code, with object, functions, control structures, anything.

I added few important items to the library. For more detail, see the calculator help page.

In addition to trivial functions like hex and Celsius to/from Fahrenheit, which mostly reflect the ridicules of my own life, I added the console output with couple of output functions, .NET-style string formatting and object dump.

The console is a pretty trivial thing: I create an addition hidden div on the right of the input text area, and show it if at least one operation in the script outputs anything to the console. This is how it looks:

String formatting function is less trivial; it's interesting enough to discuss:

if (!String.prototype.format) {
	Object.defineProperty(String.prototype, "format", {
		enumerable: false,
		configurable: false,
		writable: false,
		value: function () {
			var args = arguments;
			return this.replace(/{(\d+)}/g, function (match, number) {
				return args[number] != undefined
				? args[number]
       	 		: match;
			})
		}
	});
} //if !String.prototype.format

// usage example:

"{0} * {1} = {2}".format(7, 4, 28)
//returns: 7 * 4 = 28

Note that this is a property added to the prototype of embedded object, String. It is not added as a regular function-type property to the object in an usual easy way, but Object.defineProperty is used. Please see:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String,
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty.

The main purpose is to define the detail of the behavior of this property. Most important one, in this case, is the enumerable property, which allows hiding the format property from for-in enumeration shown below. Why? To avoid unwanted modification of the standard behavior of this embedded object prototype and hide it from dumping which I also developed for all objects.

This is how dump works:

if (!Object.prototype.dump) {
	Object.defineProperty(Object.prototype, "dump", {
		enumerable: false,
		configurable: false,
		writable: false,
		value: function (name, showFunctionBodies, maxRecursionLevels) {
			function removeFunctionBody(f) {
				var string = f.toString();
				var bodyIndex = string.indexOf(")");
				return string.substr(0, bodyIndex + 1);
			} //removeFunctionBody
			if (maxRecursionLevels === unlimited)
				maxRecursionLevels = unlimited.value;
			else if (!(maxRecursionLevels &&
					typeof maxRecursionLevels == types.number && maxRecursionLevels > 0))
				maxRecursionLevels = 1;
			if (name === undefined || name === null) {
				if (typeof this == types.function)
					name = removeFunctionBody(this);
				else
					name = this;
			} //if no name
			var recursionBreaker = [];
			recursionBreaker.push(this);
			function singleLevel(object, name, level) {
				var pad = String.empty;
				if (level && typeof level == types.number)
					for (var index = 0; index < level; ++index)
						pad += settings.textFeatures.indentPad;
				writeLine("{0}{1}:".format(pad, name));
				for (var index in object) {
					try { // maybe access exception:
						var value = object[index];
					} catch (e) {
						writeLine("{0}{1}{2}: <not accessible="">".format(pad,
							settings.textFeatures.indentPad, index));
						continue;
					}
					if (level < maxRecursionLevels - 1
							&& value && (typeof value === types.object)) {
						if (recursionBreaker.indexOf(value) < 0) {
							recursionBreaker.push(value);
							singleLevel(value, index, level + 1);
						} else
							writeLine("{0}{1}: {2}…".format(
								pad + settings.textFeatures.indentPad, index, value));
						continue;
					} //if object
					var showQuotes = typeof value === types.string;
					var incomplete = String.empty;
					if (showQuotes) {
						var values = value.split(/[\r\n]/);
						if (values.length  == 2  && values[1] == String.empty)
							incomplete = "↲";
						else if (values.length > 1)
							incomplete = "↲…";
						value = values[0];
					} //if
					if (value === undefined || value === null) {
						value = value + String.empty; showQuotes = false;
					} //if
					if ((!showFunctionBodies) && (typeof value === types.function)) {
						value = removeFunctionBody(value);
						showQuotes = false;
					} //if
					writeLine("{3}{0}: {2}{1}{4}{2}".format(
						index,
						value,
						showQuotes ? "\"" : String.empty,
						pad + settings.textFeatures.indentPad,
						incomplete));
				} //loop
			} //singleLevel
			singleLevel(this, name, 0);
			writeLine("end {0}".format(name))
			return this;
		} //value
	});
} //if !Object.prototype.dump
</not>

First argument is used to give a name to the dump, and the second one is the option to show the function bodies.

Note that this property is also not enumerable, it is hidden from itself.

This is how the usage looks:

Notably, such practice of extending prototype of embedded (built-in) types is usually called monkey patching and is often described as "bad practice": https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain.

In my opinion, this feature lies in the very heart of the nature of the JavaScript object system, is very natural to JavaScript technology and can be used in certain cases. The Mozilla documentation article referenced above admits that the technique is "used by popular frameworks such as Prototype.js". The technique is blamed for "cluttering built-in types with additional non-standard functionality", but it does not sound like a rational argument to me. Why extension should be considered as "cluttering"? Why, indeed, extensions of the objects created by the developer (JavaScript user) are not considered as "cluttering"? The whole idea of having all objects, built-in or not, first-class citizen, and, by this reason, equal partners, is much more important and productive.

Wikipedia's article on the topic discusses the problem in a more careful way, limiting possible pitfalls to "carelessly written or poorly documented monkey patches": http://en.wikipedia.org/wiki/Monkey_patch.

I hope that even more or less reasonable anti-monkey purists can agree with using this technique cannot argue against using this technique for the calculator; after all, all the code goes nowhere out of the single HTML page. Moreover, the possibility of monkey-patching the same prototype twice, with a name clash (the main argument usually put forward against this technique) is guarded by the check if (!Object.prototype.dump).

4 Exception Handling

Naturally, the call to the eval function is sandwiched with try-catch block and all exceptions are presented to the user. The method handling the exception is based on the same very console described and shown above, only the console background color is different.

function Console(console, consoleWindow, editor) {
   
   //...
   
   this.showException = function(exception) {
      this.writeLine(exception.name + ":");
      this.writeLine(exception.message);
      var knownPosition =
         !isNaN(exception.lineNumber) &&
         !isNaN(exception.columnNumber); 
          if (knownPosition)
         this.writeLine(
            "Line: {0}, column: {1}".format(
               exception.lineNumber - 1,
               exception.columnNumber + 1)); 
      this.show(true);
      if (knownPosition)
         setCaret(
            editor,
            exception.lineNumber - 1,
            exception.columnNumber);
   } //showException
}

// ...

function setCaret(input, line, col) {
   var lines = input.value.split(/\n/);
   var position = 0;
   for (var index = 0; (index < lines.length) && (index < line - 1); ++index)
      position += lines[index].length + 1;
   input.setSelectionRange(position + col, position + col + 1);
} //setCaret

The try-catch block itself is shown below.

Note that exception is the built-in Error object is implemented differently in different browsers: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error. It may implement the properties for showing line and column numbers of the code fragment associated with the error. It is taken into account in the handler. If this feature is implemented, the location of the error in the string is shown, and the caret is put on the reported position. This is how it looks:

But do those exceptions caught cover all possible errors? The problem is not as trivial as it may seem. JavaScript interpreter is not a "pure interpreter". It actually does some kind of "compilation phase", to check up and build general lexical structure of the script. This fact is not widely realized. What are the consequences? Let's see…

5 Handling Lexical Errors

It's very typical than the script does not execute in the browser at all, silently, even if you use try to catch all exceptions. It happens when some errors are of lexical level.

When you write regular JavaScript code without eval, you cannot catch some errors on lexical level.

You can catch

try {
   var x = 3;
   x = x/y;
} catch (e) {
   alert(e); // Reference error: y is not defined
}

but it would not work with

try {
   var a = [1, 2, 3;] // ';' is the lexical-level bug
} catch (e) { // won't be caught! 
   alert(e);
}
// the script won't execute at all, silently

However, if you enter the second code fragment in text area, pass its value to eval(code) and sandwich it a try-catch block, the lexical-level problem will be processed as an exception! Compare:

try {
   eval("var a = [1, 2, 3;]");
} catch (e) {
   alert(e); // SyntaxError: missing ] after element list
}

This is a method of "converting all errors into exceptions". As all user's code comes through the eval, this JavaScript peculiarity gives a special benefit to the development of some JavaScript code using the calculator: no silent failures to execute the script anymore.

6 Strict Mode

Now, let's take a look at the small check box in the top right corner.

As I mentioned the use of the calculator, a very light-weight tool, for "serious" JavaScript development, it's important to have such an important feature as "strict mode". This is the feature introduced with ECMA-262 (EMCAScript standard) of 2011:
http://www.ecma-international.org/publications/standards/Ecma-262.htm,
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode.

As one can see from the description of strict mode limitations, it can highly improve the stability of development. The applicability of strict mode is quite non-trivial. The tricky part is: the usability of the calculator requires the dynamic switch between strict and non-strict modes. To show how it is implemented, let's look at the core :

var evaluate = function() {
   function strictEval(text) { "use strict"; return(eval(text)) }
   var isStrict = strictMode.checked;
   aConsole.reset();
   try {
      var bra = "with (Math) {\n";   
      var ket = "\n}";
      if (isStrict) bra = "\n";
      if (isStrict) ket = "";
      var text = bra + anElementSet.editor.value + ket;
      if (isStrict)
         anElementSet.result.value = strictEval(text);
      else
         anElementSet.result.value = eval(text);
   } catch (e) {
      anElementSet.result.value = "";
      aConsole.showException(e); return;
   }
   if (aConsole.isEmpty)
      aConsole.hide();
   else
      aConsole.show(false);
} //evaluate

Depending on the check box checked state, eval is either called directly or in the wrapping function declaring strict mode. With such implementation, there is the catch: page reload is required for each switch to the strict mode.

7 Dynamic Strict Mode Switching and Web Storage

Re-loading of the page requires restoring the states of controls after reloading. First of all, this is the strict mode check box itself (otherwise its checked state will never be changed) and, importantly, the code in the input text area. Some browsers will restore these states (Mozilla), others won't. To implement this kind of persistence, Web storage feature is used: http://en.wikipedia.org/wiki/Web_storage.

It is important to use session storage and not local storage. Local storage is permanent; and we don't want to contaminate local browser data. Now, another problem is: data stored by one key should be string, and I want to store the object encapsulating the states of both controls, one of these state values being Boolean. For this purpose, I utilized JSON serialization embedded in JavaScript: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON.

This is complete implementation of this feature, storing and restoring the controls' states by a single key:

var persistence = {
   key: "S. A. Kryukov JavaScript Calculator",
   store: function(checked, code) {
      sessionStorage.setItem(
         this.key,
         JSON.stringify({checked:checked, code:code}));
           }, //store
   load: function() {
      var item = sessionStorage.getItem(this.key);
      if (item)
         return JSON.parse(item);      
   } //load
} //persistence
var item = persistence.load();
if (item)  {
   anElementSet.editor.value = item.code;
   strictMode.checked = item.checked;
} //if
strictMode.onclick = function() {
   persistence.store(strictMode.checked, anElementSet.editor.value);
   window.location.reload();
} //strictMode.onclick

Obviously, after reload, all the script is executed again, eventually reaches the point where persistence.load() is called. The data may or may not be found by the key. If it is not found, the script performs evaluation of the user code by default (non-strict); if it is found, the evaluation is performed using the last stored "strict" state. All works.

8 Layout

For this application, I developed the layout technique which was new to me: automatic layout which, depending on the current size of the browser window, nicely fills it from top to bottom, by adjusting the height of the input text area, in the "fill" dock style of desktop applications. The layout was completely rewritten in v.2.0:

function setDockLayout() {
   var children = document.body.childNodes;
   var parts = [];
   for (var index = 0; index < document.body.childNodes.length; ++index)
      if (document.body.childNodes[index].nodeName.toLowerCase() == "div")
         parts.push(document.body.childNodes[index]);
   var top = [];
   for (var index = 0; index < 3; ++index) {
      var topElement = document.createElement("div");
      top.push(topElement);
      topElement.appendChild(parts[index]);
      document.body.appendChild(topElement);
   } //loop
   top[0].style.cssText = "position: absolute; top:0; right:0; left:0";
   top[1].style.cssText = "position: absolute; left:0; right:0";
   top[1].style.backgroundColor = window.getComputedStyle(parts[1]).backgroundColor;
   top[2].style.cssText = "position: absolute; bottom:0; right:0; left:0";
   parts[1].style.backgroundColor = window.getComputedStyle(parts[1]).backgroundColor;
   parts[1].style.position = "absolute";
   parts[1].style.left = 0; parts[1].style.right = 0;
   parts[1].style.top = 0; parts[1].style.bottom = 0;
   var margin = "0.2em";
   parts[1].style["margin-top"] = margin; parts[1].style["margin-bottom"] = margin;
   (window.onresize = function () {
      var topHeight = top[0].offsetHeight;
      top[1].style.top = topHeight;
      top[1].style.height = window.innerHeight - top[0].offsetHeight - top[2].offsetHeight;
   })();
} //setDockLayout

I implemented similar layout for the very first time in my recent JavaScript work, "Tetris on Canvas". For such applications, this is the most adequate design. It resembles nicely resized desktop applications using docking of the control instead of manual positioning. The window-level scroll bars appear only when the window is resized down to extremes of the small size when it becomes nearly unusable.

9 Versions

Version 1.0: initial version.

Version 2.0: Many useful features have been added. The layout is fully replaced to make it universal and reliably maintainable. I applied my new technique for development of desktop-like JavaScript applications. The code of the function dump is almost completely rewritten, to allow nested object dump, yet preventing possible "infinite" recursion. Another big addition is the new smart formatting feature explained in detail in my recent article Simple Rule-Driven Smart Formatting for HTML Textarea.

10 Conclusions

It's hard to imagine such a fully-functional calculator which is so cross-platform and light-weight, and… required so little effort to implement. Yes, it was pretty easy; I almost never had to use the debugger.

This is not the only approach which would eliminate code scanning and parsing. On windows platform, one approach could be just the use of PowerShell, and another one — .NET with its CodeDOM; I wrote a lot on this topic. But this is quite a different story…