The Tiny RPN Calculator: Version 1.05 Overview

Extending the Calculator

Version 1.05 incorporates three major enhancements to Version 1.04: (1) Users can now recall existing programs for inspection; (2) The calculator now supports strings as part of program output, which should prove useful in assigning units of measurement and otherwise clarifying outputs; (3) Five new math functions have been added: max, min, %of, %ch, and %t.

The ability to recall programs and output their instruction sequences to the screen has been implemented through several additions to the ProgramManager class. Chief among them is the public method RecallProgram(), which is called by the Calculator::Run() method whenever the user enters the instruction rcl. RecallProgram() contains a while loop that keeps running until one of three conditions is met. First, the method looks for a program file bearing the name supplied by the user. (After that name is checked for validity, of course.) If the file is found, its contents are output to the screen via the ProgramManager::WriteToScreen() private method. If not, the search resumes within the programs vector field. If found there, the program will be output by WriteToScreen(); but if not, a report is issued, and the user is asked whether to continue searching, or end the recall process. The WriteToScreen() method works very similarly to the method ProgramManager:: Write(), except that rather than writing to the file stream fout, WriteToScreen() sends program contents to cout instead.

The ease and simplicity of implementing strings as a component of calculator output actually took this developer quite by surprise. Only a few lines of code were required to make it possible. But the first issue to be dealt with was to supplement the ? function with another, similar function that does not automatically print a newline character, as ? does. Thus the function $ is added to the Calculator::DeployStackFunction() method. To inform the calculator that an entry is intended to be a string, it was determined that the entry should be distinguished as such by being enclosed in single quotes. In concert with this, two very brief, one-line methods are added to the Calculator class: EntryIsStringName(), which returns a boolean; and the method DeployStringFunction(), which is of return type void. EntryIsStringName() returns true if either the variable entry contains a single quote as its first and last characters, or if the user has entered the command nl, the newline command. In that case control is passed to DeployStringFunction(), which will either output the string to the screen (minus the single quotes), or output the newline character as appropriate.

Addition of the five new math functions is straightforward, involving just five new code blocks to the switch statement of the method Calculator::DeployMathFunction(). (See "Using the New Math Functions" below for an explanation of what these functions do, and how to invoke them.) It should go without saying that the method Calculator::MathFunctionIndex() had to be updated accordingly; also, eight new keywords had to be added to the ProgramManager::IsReservedKeyword() method, due to the new math functions, and the new instructions rcl, $, and nl described above.

Using the Recall Function to Inspect Your Programs

Let's create a test program to verify the following simple algebraic identity:  x2 - 1 = (x + 1)(x - 1) . But we will deliberately make an error in our program entry so that we can correct it later, using our ability to recall programs:

pgm
Enter a name for the program: test
Enter a sequence of program instructions; terminate by entering "end": X = X sq 1 - ? X 1 + X 1 + * ? end
   Program "test" created.
   Save "test" to file? (y/n) n
3 test
   8
   16
-4.5 test
   19.25
   12.25

Clearly, something's gone wrong, because in both cases our two outputs are not identical as predicted. Prior to Version 1.05, if we had closed out our shell program after creating this identity formula, we'd have no way to access its contents without opening the file test.pf inside a text editor. To see how easy it is to recall and inspect programs, use the rcl command:

rcl
Enter name of program: test
   Program "test" recalled: X = X sq 1 - ? X 1 + X 1 + * ?

Now we can see where we had erred: the second occurrence of + should be - instead. So we correct our program as follows:

pgm
Enter a name for the program: test
   The program "test" already exists. Do you want to overwrite it? (y/n) y
Enter a sequence of program instructions; terminate by entering "end": X = X sq 1 - ? X 1 + X 1 - * ? end
   Program "test" created.
   Save "test" to file? (y/n) n
3 test
   8
   8
-4.5 test
   19.25
   19.25
e chs test
   6.38905609893
   6.38905609893

But there's more to the rcl instruction than this. Let's say we recall the test program at some later point, only we misspell the name—or forget what its correct name is. As you'll soon see, the rcl facility issues a useful prompt, and allows us to repeat the procedure as necessary or desired:

rcl
Enter name of program: tess
   ERROR: The program "tess" doesn't exist. End recall? (y/n) n
Enter name of program: test
   Program "test" recalled: X = X sq 1 - ? X 1 + X 1 - * ?

Thus while the facility to recall programs is quite basic overall, it does contain code that will deal with certain contingencies in an intelligent manner, without causing the calculator to exit or crash. It's of the utmost importance to deal with such contingencies as you code your own applications; doing so will make your applications that much more robust.

Using Strings; Formatting Output with Units of Measurement

To output numbers with strings attached (there, I said it...), just keep in mind the following three steps: (1) Use $ instead of ? to begin output (remember that $ suppresses the newline so that the user can append strings to numbers); (2) Every string that is separated from other strings by spaces must be enclosed in single quotes; (3) Use nl, the newline command, when you want your output to skip to the next line.

Let's illustrate by writing a series of three short programs, each of which stores and outputs a physical constant. First, we create a program called c that displays the speed of light in meters per second; next, a program called year that shows the length of one year in seconds; lastly, a program called light-year that calls c and year to compute the distance, in meters, traveled by light in one year. Note particularly how string output is invoked in each program:

pgm
Enter a name for the program: c
Enter a sequence of program instructions; terminate by entering "end":
299792458 $ 'meters' 'per' 'second' nl end
   Program "c" created.
   Save "c" to file? (y/n) y
   Program "c" saved to file "c.pf".
pgm
Enter a name for the program: year
Enter a sequence of program instructions; terminate by entering "end":
31556926 $ 'seconds' 'per' 'year' nl end
   Program "year" created.
   Save "year" to file? (y/n) y
   Program "year" saved to file "year.pf".
pgm
Enter a name for the program: light-year
Enter a sequence of program instructions; terminate by entering "end":
c year * 9 pr $ 'meters' 'per' 'year' nl end
   Program "light-year" created.
   Save "light-year" to file? (y/n) y
   Program "light-year" saved to file "light-year.pf".
light-year
   299792458 meters per second
   31556926 seconds per year
   9.46052841e+15 meters per year

Using the New Math Functions

The functions max and min are virtually self-explanatory. Each takes two arguments, and max returns the greater of the two (provided the arguments are not equivalent), while min returns the lesser (again, provided the arguments are not equivalent). The following computations illustrate the effects of max and min:

pi 3 yx ? 4 exp ? max ?
   31.0062766803
   54.5981500331
   54.5981500331
pi 3 yx ? 4 exp ? min ?
   31.0062766803
   54.5981500331
   31.0062766803
e sq ? 2 exp ? max ?
   7.38905609893
   7.38905609893
   7.38905609893

On the other hand, the three percentage functions may require some explanation and clarification. All three take two arguments. Let's suppose that these arguments are A1 and A2, where A1 is understood to occupy the top level of the stack, and A2 the next level down. The function %of computes A2 percent of A1, and is effected by the formula  (A2 * A1) / 100 (%of also happens to compute A1 percent of A2, as the result is the same regardless of the order in which the arguments are taken). The function %ch computes the percent change from A2 to A1, measured as a percentage of A2. This function is effected by the formula  ((A1 - A2) / A2) * 100 . The function %t returns percent of total, wherein A2 is the total, and A1 is the value to which the total A2 is compared. The function %t is effected by the formula  (A1 / A2) * 100 .

To illustrate the differences among these functions, we will apply all three to the arguments 20 and 30, in that order, as shown below:

20 30 %of ?
   6
20 30 %ch ?
   50
20 30 %t ?
   150

Observe that 20 percent of 30 is 6, while the percent change from 20 to 30 is 50 percent, since 30 is larger than 20 by half of 20. Finally, 30 is 150 percent of 20, if 20 is taken to be the total to which 30 is compared.

Looking Ahead

In previous versions of the Tiny Calculator, we've seen that some programs that assign quantities to variables can be replaced by other programs that are functionally equivalent, but which do not assign to variables. Often we can manipulate the stack to replicate quantities that we need repeatedly, merely by using the sw and dp stack functions to swap the two topmost elements, and to duplicate an element, respectively. Consider the program that we wrote above, which verifies the algebraic identity  x2 - 1 = (x + 1)(x - 1) . Even that program can be replaced by another that dispenses with the variable X:

pgm
Enter a name for the program: test2
Enter a sequence of program instructions; terminate by entering "end": dp sq 1 - ? sw dp 1 + sw 1 - * ? end
   Program "test2" created.
   Save "test2" to file? (y/n) n
3 test2
   8
   8
-4.5 test2
   19.25
   19.25
pi exp 10 9 / - test2
   484.30246202
   484.30246202

Many times, however, the functions sw and dp will not suffice, and failing the addition of more stack functions, there will be formulas, functions, and applications for which we'll have no recourse other than to assign to variables. In RPN calculation the problem arises largely because once an operation or function is applied to an argument, or a pair of arguments, that argument (or argument pair) vanishes from the stack. Many times we would like to have those arguments back somehow, so that we can reuse them. This is why the much-beloved HP 48 series of calculators incorporates a LASTARG facility: to enable users to retrieve the argument (or arguments) of the most recent calculation. Accordingly, Version 1.06 will mimic this capacity by adding to the calculator a new class called Arguments, and a pair of new stack functions la and dr. Once you've used these functions, you'll come to appreciate that stack functions are much like derivation rules in a system of formal logic: they are your friends, not your enemies, and the more you have on hand, the easier your life will be!

Exercises

1) This exercise is very similar to Exercise 4 of the Version 1.02 Overview—except that we will now exploit the advantages of using strings for program output, and saving our programs to files. Write a program called raw that computes the raw displacement of an engine regardless of the units in which it's measured. The user will enter bore, stroke, and number of cylinders in that order. Use stack functions to perform the computation, then store the "unitless" raw displacement in the variable D. Save the raw program; then write two more programs, one called ede ("Engine Displacement—English Units"). This program should take the result yielded by raw, and produce two outputs: one in cubic inches, rounded to the nearest thousandth of a cubic inch; the second in cubic inches, rounded to the nearest cubic inch. Use strings to label your outputs accordingly. The other program is called edm ("Engine Displacement—Metric Units"), and should format raw displacement as follows: (a) cubic centimeters, rounded to the nearest cubic millimeter; (b) cubic centimeters, rounded to the nearest cubic centimeter; (c) liters, rounded to the nearest tenth of a liter. Again, use strings to label your outputs with the proper units. Test ede and edm against some of the examples already presented; or root around the Internet for dimensions of any other engines you can find.

2) In this exercise we develop more fully the program we created in Version 1.01, Exercise 3, which calculates compound interest on an investment. Recall the formula we used on that occasion:  A = P(1 + r / 100 / n)yn , where A is the amount recouped on the investment, P is the principal invested, r is the annual rate expressed as a percentage, n is the number of times per year that interest is compounded, and y is the length of the investment term in years. Using this formula, write and save to file a program called invest. This program should take as inputs the principal, rate, number of times compounded per year, and investment term, in that order. Your program should first output all of your inputs, with labels; then it should output the interest earned, followed by total return on investment, with appropriate labels. Test this program using the examples presented in Version 1.01, Exercise 3; or devise your own examples. Some hints: Have the program store your inputs in variables before proceeding; consider writing a subprogram called format that converts raw monetary values into a dollars-and-cents format.

3) What happens when you execute this sequence of instructions:

299792458 $ 'meters per second' nl 

Why does it happen? Explain in terms of what you see in the source code, and state what it takes to rectify the problem.

4) Write and run the following program, called devil, as follows:

pgm
Enter a name for the program: devil
Enter a sequence of program instructions; terminate by entering "end":
10 cb 2 * 3 / floor $ 'is' 'the' 'Devil's' 'phone' 'extension!' nl end
   Program "devil" created.
   Save "devil" to file? (y/n) n

When you run this program, is the calculator flummoxed by the single-quote character inside the string "Devil's"? Why or why not? Provide a thoroughgoing defense of your answer.

5) Write and save to file a program called delta% that takes as inputs any two positive, non-zero numbers. The program will output the percentage of change between the two, going in both directions. Write the program so that it always outputs the percentage of decrease first, followed by the percentage of increase. Label your outputs accordingly. (Hint: Be prepared to tolerate a program that looks a bit clunky. When we move to Version 1.06, we'll have the tools to craft a much more elegant version of this program.)

6) Prove that the math functions %of, %ch, and %t are just so much syntactic sugar from the calculator's point of view. Do it in the best way imaginable: by desugaring these functions—which is to say, write three alternative programs my%of, my%ch, and my%t that accomplish exactly the same things without using percentage functions. Be sure to test your programs against the built-in percentage functions using identical inputs for both.

Solutions to Exercises

1) The solution should be quite similar to that for Version 1.02, Exercise 4, except that (a) you have used $ instead of ? to output unit measurements; (b) you thought to embed the instruction raw at the beginning of ede and edm, so there's no need to invoke raw on the command line (that did occur to you...yes?) Here's the solution (you may have represented units differently than here):

pgm
Enter a name for the program: raw
Enter a sequence of program instructions; terminate by entering "end": * sw 2 / sq pi * * D = end
   Program "raw" created.
   Save "raw" to file? (y/n) y
   Program "raw" saved to file "raw.pf".
pgm
Enter a name for the program: ede
Enter a sequence of program instructions; terminate by entering "end": 
raw D 1000 * round 1000 / $ 'cu.in.' nl D round $ 'cu.in.' nl end
   Program "ede" created.
   Save "ede" to file? (y/n) y
   Program "ede" saved to file "ede.pf".
pgm
Enter a name for the program: edm
Enter a sequence of program instructions; terminate by entering "end": 
raw D round 1000 / $ 'cc.' nl D 1000 / round $ 'cc.' nl D 100000 / round 10 / $ 'L.' nl end
   Program "edm" created.
   Save "edm" to file? (y/n) y
   Program "edm" saved to file "edm.pf".
3.25 2.90 4 ede
   96.231 cu.in.
   96 cu.in.
87.0 86.8 12 edm
   6191.977 cc.
   6192 cc.
   6.2 L.

2) We first write and save the program format as shown below, followed by invest; note that the interest earned, equal to total return on investment minus principal, is stored in the variable I:

pgm
Enter a name for the program: format
Enter a sequence of program instructions; terminate by entering "end": 100 * round 100 / end
   Program "format" created.
   Save "format" to file? (y/n) y
   Program "format" saved to file "format.pf".
pgm
Enter a name for the program: invest
Enter a sequence of program instructions; terminate by entering "end": 
Y = N = R = P = R 100 / N / 1 + Y N * yx P * A = A P - I = 
P format $ 'principal' nl R $ 'percent' nl N $ 'times' 'compounded' 'per' 'year' nl Y $ 'years' nl 
I format $ 'interest' nl A format $ 'return' 'on' 'investment' nl end
   Program "invest" created.
   Save "invest" to file? (y/n) y
   Program "invest" saved to file "invest.pf".
500 4.5 1 2 invest
   500 principal
   4.5 percent
   1 times compounded per year
   2 years
   46.01 interest
   546.01 return on investment
7500 2.8 4 5.5 invest
   7500 principal
   2.8 percent
   4 times compounded per year
   5.5 years
   1243.99 interest
   8743.99 return on investment
90000 0.9 365 17 12 / invest
   90000 principal
   0.9 percent
   365 times compounded per year
   1.41666666667 years
   1154.83 interest
   91154.83 return on investment
21000 2.375 365 657 365 / invest
   21000 principal
   2.375 percent
   365 times compounded per year
   1.8 years
   917.19 interest
   21917.19 return on investment

3) It's highly probable—almost inevitable—that the sequence of instructions will output the following:

299792458 $ 'meters per second' nl
   299792458   ERROR: "'meters" is neither a supported function nor a program name.
   ERROR: "per" is neither a supported function nor a program name.
   ERROR: "second'" is neither a supported function nor a program name.

Observe that the calculator has not handled 'meters per second' as a single string despite its apparent enclosure within single quotes. The reason has to do with the way the input stream cin handles keyboard input. Once you've typed and entered the entire sequence, cin will feed characters one-by-one to the string variable entry only until cin encounters a whitespace character such as a space, a tab, or a newline. At that point entry will be processed by the while loop of the method Calculator::Run(), and this process will recur within the loop until there is no more keyboard input for cin to handle. That means the calculator has interpreted the alleged string as three entries, not one. Since none of the three passes muster as a string (why?), much less a numeric, variable name, operator or function name, or even a program name (unless you accidentally have a program in your system named per), control has fallen all the way to the bottom of the loop in each instance, and the calculator reports that three errors have taken place.

To rectify the problem, just keep in mind what was said above about entering strings: "Every string that is separated from other strings by spaces must be enclosed in single quotes." Here's the correct way to achieve the anticipated result:

299792458 $ 'meters' 'per' 'second' nl
   299792458 meters per second

4) When you run the devil program, you should get the following output:

devil
   666 is the Devil's phone extension!

It's obvious that the calculator is not fooled or flummoxed by the single-quote character inside the string "Devil's"; in particular, it does not truncate the output to read "Devil". The reason lies in the methods EntryIsStringName() and DeployStringFunction(), both member methods of the Calculator class. The fact is, neither method cares about the contents of a string stored in the variable entry. The only thing EntryIsStringName() does is check the first and last characters of that string to determine whether they are both single quotes. If they are, it passes control to DeployStringFunction(). But then DeployStringFunction() makes no account whatsoever of the contents of the string, whether or not those contents include single quotes! All it does is shore up the substring wedged between the first and last characters of the string stored in entry—in other words, the substring lying between the first and last occurrences of single quotes in entry, irrespective of other occurrences inside the substring.

5) There are at least two ways to write this program, and whichever way you choose, it's been intimated that the results may be clunky. That's because we'll have to resort to storing quantities in four different variables for the time being. Here is one possibility, along with inputs to test it:

pgm
Enter a name for the program: delta%
Enter a sequence of program instructions; terminate by entering "end": 
A = B = A B %ch C = B A %ch D = C D min $ 'percent' 'decrease' nl C D max $ 'percent' 'increase' nl end
   Program "delta%" created.
   Save "delta%" to file? (y/n) y
   Program "delta%" saved to file "delta%.pf".
8 10 delta%
   -20 percent decrease
   25 percent increase
10 8 delta%
   -20 percent decrease
   25 percent increase
320.2 3.202 delta%
   -99 percent decrease
   9900 percent increase
6.77 286.5 delta%
   -97.6369982548 percent decrease
   4131.90546529 percent increase
pi e sq delta%
   -57.4831668412 percent decrease
   135.200960586 percent increase

6) The solutions are as follows. It must be pointed out, however, that using my%ch in lieu of %ch imposes a certain amount of risk in that the former writes values to the variables A and B. If these variables have been previously assigned to, the function my%ch will overwrite those values. On the other hand, when we move to Version 1.06, we'll exploit two new stack functions that enable us to write an alternate version of mych% that does not assign to variables at all.

pgm
Enter a name for the program: my%of
Enter a sequence of program instructions; terminate by entering "end": * 100 / end
   Program "my%of" created.
   Save "my%of" to file? (y/n) y
   Program "my%of" saved to file "my%of.pf".
pgm
Enter a name for the program: my%ch
Enter a sequence of program instructions; terminate by entering "end": B = A = B A - A / 100 * end
   Program "my%ch" created.
   Save "my%ch" to file? (y/n) y
   Program "my%ch" saved to file "my%ch.pf".
pgm
Enter a name for the program: my%t
Enter a sequence of program instructions; terminate by entering "end": sw / 100 * end
   Program "my%t" created.
   Save "my%t" to file? (y/n) y
   Program "my%t" saved to file "my%t.pf".
20 25 my%of ?
   5
25 20 my%of ?
   5
18.3 4 exp my%of ?
   9.99146145607
18.3 4 exp %of ?
   9.99146145607
64 92 my%ch ?
   43.75
64 92 %ch ?
   43.75
92 64 my%ch ?
   -30.4347826087
92 64 %ch ?
   -30.4347826087
64 92 my%t ?
   143.75
64 92 %t ?
   143.75
92 64 my%t ?
   69.5652173913
92 64 %t ?
   69.5652173913