The first thing a C programmer would notice about the javascript code is that all the functions and procedures have the key word 'function' before them, and no return type. If any value is returned at all (using 'return'), its type is implied by the type of the returned variable. This also brings us to a second key difference, there is almost no typing of variables (and you thought C was bad!). Variables do not have to be declared (though we will declare them in this example), and the type they take on is dependant on the data type they are assigned. A variable is declared with 'var <varname>'.
We will be manipulating strings extensively, and JavaScript has some built-in features for this. A string literal can be enclosed in single or double quotes. In this model an arbitrary convention is used where characters (strings of length 1) are enclosed in single quotes and strings are in double quotes. The length of a string can be determined directly. If a variable 'str' contains a string, then its length is 'str.length'. This also works for string literals (e.g. "1234".length), as does that which follows. To determine if a particular character is in a string, and its location, you can use something like str.indexOf('+'). This will be -1 if it is not found. To get the character at a particular location then str.charAt(0) can be used. To get a substring a statement such as str.substring(1, 3) is used. We can concatonate strings together using '+'. E.g. "hello " + "mum" equals "hello mum". Numbers can be converted to strings as "str = x.toString()".
Numbers are floating point. Other than that they are treated as you'd expect. To manipulate numbers beyond the basics, a set of built-in 'methods' are provided by a 'Math' object. So, to find the square root of a variable x, use Math.sqrt(x). The only other Math method we will use in this model is Math.round(). Refer to the JavaScript links for all the methods available. Changing a string to a number is quite useful, and this is done with the "parseFloat(str)" built-in function.
The last thing to introduce is the instantiation of the code in the HTML.
We have two choices---embed or reference. We will use the former, though the
latter is more usual as it allows more than one page to reference the code.
To embed code the <script> HTML directive is used eg.
<script type=text/javascript>
</script>
The javascript is placed bewtween the script tags.
To reference a file containing the script use (for example, in a file 'calc.js'):
<script type=text/javascript src="calc.js">
</script>
So we just about have enough to proceed.
As well as the functions we need some state. The calculator has registers
that we will need to reflect. So some global variables are declared
var x;
var y;
var m;
The x register is an accumulator where the results are placed. The y
register is the input number and the m register is the memory. As well
as the models of the calculator's register, we need internal state
to control the operation.
var disp="";
var is_new_num = true;
var is_decimal = false;
var last_op = "";
var error = false;
The disp variable is a string that holds the current value to
be displayed as a string rather than a number. It is the variable
passed to update_display(). The flag is_new_num is set whenever
a numeric input needs to start a new number input, as opposed to
appending to a number already being input. Similarly is_decimal
is set if the decimal point key has been pressed. The last_op
variable holds the last operational key (i.e. one of '+', '-'
'*' or '/') or is null if none active. Finally, the error
flag is set on overflows and other errors. It is used by
update_display() to display an error state, and also to prevent
all other inputs apart from 'ON/C'. These variables also have
initial values set at declaration.
We also declare variables to hold the images. This will make
referencing the images simpler, but also forces the HTML
to load the images immediately. In general a variable is declared
and assigned some space, and then the source file for that image
variable set to point to the relevant file.
lcd0 = new Image(20, 45);
lcd0.src="images/lcd0.jpg";
The 'new Image(20,45)' creates an image structure of the declared size,
and lcd0 is assigned to it. The src variable of the new structure is
then assigned to the image filename. This is done for all the images
we will use.
To do this an array is created to contain a list of image src filenames,
and then after assigning the names for each segment the 'document' references
updated to display the new value. A 'while' loop is employed to march through the
segments. A fragment of the code is shown below.
function update_display(dspin) {
var disp_array = new Array();
var dot_active = false;
var dsp=dspin;
var lcds=0;
var idx=dsp.length;
while (lcds < 8) {
idx--;
digit = dsp.charAt(idx);
if (digit == '.')
dot_active = true;
else if (digit == '-') {
disp_array[lcds] = lcdminus.src;
lcds++;
} else if (digit && "0123456789".indexOf(digit) != -1) {
if (dot_active)
disp_array[lcds] = "images/lcd"+digit+"dot.jpg";
else
disp_array[lcds] = "images/lcd"+digit+".jpg";
dot_active = false;
lcds++;
} else {
disp_array[lcds] = lcdoff.src;
lcds++;
}
}
document.d7.src = disp_array[7];
.
.
document.d0.src = disp_array[0];
}
The display is right justified, so our index (idx) is assigned to the
rightmost digit of the input string (dspin), and will count down.
An array is created (disp_array) using 'new Array()', and the input
assigned to a local variable (dsp) so that we can alter the value.
The 'lcds' variable is used to index the array, and is initialised
to 0. So the while loop continues until all 8 segments are processed.
A character is pulled from the string and assigned to a local
variable digit, which is then tested for various values. If
it is a decimal point, then dot_active is set true. If it is a
minus, then the array indexed by lcds is set to the src of
the minus image variable. For numeric characters the array
element is set dependant on two things. Is dot_active true, in
which case use the dot versions of the images, and what the
value of the digit is. Strings are constructed from this which
will refer to the correct file and assigned to the array. If
digit is none of the recognised values, the array is assigned
with the lcdoff source. Note that the idx value is decremented
for each loop except if the digit is '.' which is simply
consumed as it only modifies a digit display to a dot version,
but does not move a segment on.
Not shown in the fragment is the case where the 'error' flag is set, or ensuring that the input string has a decimal place (even if at the end for integers). These are small enhancements, and you can see the full code via the link at the bottom of the page.
function keyboard(e) { ky = String.fromCharCode(e.keyCode); key_pressed(ky); }Without going into too much detail, the event structure in 'e' has a keyCode value (the unicode value of the key pressed) which we can convert to a single character string with the built in method of 'String.fromCharCode()'. The main input processing function key_pressed() can then be called with the extracted character, so that all input is processed in the same way.
The key_pressed() function is a big 'if' statement (we might have used 'case', but
that's not available in JavaScript 1.1). The input character falls into one of
four classes. A number (including decimal place), an operation (+, -, *, or /),
a completion (= or %) and the rest. After processing, a call to update_display()
is made to display a new value (though it could be unchanged). So the general
structure of key_pressed() is as shown below.
function key_pressed(key) {
if ((key && ".0123456789".indexOf(key)) != -1) {
// Process number
} else if ("*/+-".indexOf(key) != -1) {
// Process operation
} else if ("=%".indexOf(key) != -1) {
// Process completion
} else {
// Process other
}
update_display(disp);
}
In the first branch of the tree (numbers), we generally want to keep adding the input
to disp (disp = disp + key). If the number is not a decimal (is_decimal is false), we
keep the decimal at the end. If the display is full we ignore the input. When a
decimal is input is_decimal is set true. If a decimal is the first number (is_new_num
is true) then we imply a leading 0 (disp = "0."). After processing the input number,
the numeric value is assigned to y (y = parseFloat(disp)) so that y always contains
the latest input value should an operation key be pressed. The variable is_new_num
is always cleared on a numeric value, as we must be in the middle of an input.
For operation keys, in a sequence like "2 + 3 = ", the operation key (+) simply copies the input (in the y register) to the x register, the actual operation being delayed until the completion key (=). So the pending operation must be stored until completion. This is what last_op is used for. After any operation a new number key must be a new input, so is_new_num is set true, with is_decimal false. However, for a sequence such as "2 + 3 - 1 =", the second operation (-) must effect a completion as if the equals had been pressed. So in this case, where last_op is not an empty string, we don't copy y to x, but do the last operation on x and y. A function reduce() is called with the last_op value to do this (see below), and then disp updated with the resulting value in x.
Completion is very similar, except reduce() is always called to update x (unless mistakenly pressed before an operation input). If '%' is input instead of '=' then y is divided by 100 before calling reduce.
The remaining input processing has three catagories: modifying y, clearing and memory register manipulation. The 's' key square roots the y register, and disp is updated. Before disp is updated, y is rounded to a value that fits on the display by calling a function rnd_to_display(). This is explained below. Also, square rooting negative numbers needs to be detected, and the error flag set. The 'i' key inverts y (i.e. multiplies by -1) and disp updated. The 'c' key clears y to 0, sets disp accordingly and sets state for a new number. The 'o' key initialises all the state back to the original values. For the memory operations, 'm' simply copies the value in the m register to y, and disp is updated. The key 'm' (add to memory) and 'M' (subtract from memory) simply update the variable 'm' by adding or subtracting y.
The reduce() function gathers together the actual math operations of
the calculator into one place (it's called from two separate places). It's
basic function is simple, in that the x register is updated with the
y register via the selected operation passed in as op. A fragment is shown
below.
function reduce (op) {
if (op == '+')
x = x + y;
if (op == '-')
x = x - y;
if (op == '*')
x = x * y;
if (op == '/') {
x = x / y;
}
}
In addtion to this, we need to trap division by zero and flag the error. Then
the resulting value of x must be rounded so that the result only has as many
decimal places as will fit on the display. Finally x is checked that it lies
within the maximum and minimum range of the calculator, and error set true if
it is outside.
The rounding of x is done via another function rnd_to_disp(). Like reduce(),
this is gathered into a function as it is called from key_pressed() as well.
Here we multiply x by a factor so that all relevant decimal points would become
whole numbers, call Math.round() to round to nearest value, and divide the
same number of places down again.
function rnd_to_display(val) {
var rnd_factor;
var result = (val < 0) ? -1 * val : val;
rnd_factor = 10000000;
while (result >= 10) {
result = result / 10;
rnd_factor = rnd_factor / 10;
}
if (val < 0)
rnd_factor = rnd_factor / 10;
result = (Math.round(val * rnd_factor))/rnd_factor;
return result;
}
A different rounding factor is used for negative numbers as then only
seven segments are available for numbers (the eighth being used for the
minus).
The full JavaScript for the simulation (without the HTML) is shown here, and the entire package (HTML, JavaScript and graphics) can be accessed here (87K).
The calculator as it is works fine as far as it goes. Shortcuts were taken to simplify things, and a real model would attempt to simulate all behaviour. You also notice that the code is devoid of comments. This again is for clarity, and real code would benefit enormously from liberal commenting. Finally, there are many literals in the code. JavaScript 1.1 does not allow the 'const' declaration (though later revisions do). However, it is possible to have global 'var' declarations with constant values (just be careful not to modify them---choosing all capitals for the name, say, helps to remind). Many other improvements, I'm sure, can be made and the structure of the example is not meant to be definitive in good coding.
In some senses we can say we're finished. We have a model that works. Except, how do we know it works? We'll deal with this in the next section.
"How to write a calculator simulator"
<- Prev Page
Next Page->