RPN Calculator in Smalltalk

In this implementation, most of the work is done in methods attached to String, which is the standard Smalltalk class for strings.

First of all, a method is implemented that extracts a floating-point number from a string. Smalltalk does not have such a method standardly, and the popular Smalltalk systems offer insufficient provision in this respect. In the definition of the asFloat method, the string is converted into a mutable collection (a sequence) of characters, t. If there are an arithmetic sign and a decimal dot in t, account is taken for them and they are removed. The remainder must be a non-empty sequence of only digits. If it is not, an exception is signaled. The number is computed by accumulating its value along that sequence, multiplying by 10 and adding each digit, and finally dividing by an appropriate power of 10, according to where the decimal dot was.

Note how the asFloat method is applied to the resulting number at the very end of the other asFloat that we define. It is needed in order to convert the so far fractional-typed value into one of floating-point type. Using the same name asFloat to denote two different methods presents no problem, as one of them applies to fractional numbers, and the other to strings.

The evalAsRpn method is central in implementing the calculator. It splits a string into tokens and creates a sequence tks by translating each token into either an arithmetic operator symbol or a number (while an exception possibly raised by failing to convert a token into a number is propagated to the caller of evalAsRpn). The RPN expression contained in tks is progressively reduced by replacing each operator (o) and the two preceding arguments (x and y) by the result of applying o to x and y. The application takes place in the form of sending perform:with: with arguments o and y to x.

A correct RPN expression is thus finally reduced to a single number. An erroneous sequence leads to an exception in one of two ways. When tks contains more than one element but no operators, i is 0 and thus obtaining o fails. If, on the other hand, i is 1 or 2, signifying that there are less than two arguments for o, obtaining x fails. In each of these two cases an exception is raised by at: due to invalid subscript, and it propagates outside of evalAsRpn.

Upon success, evalAsRpn returns the only remaining in tks number as the result of evaluation.

Finally, one more method is defined for the String class which checks whether a string is all whitespace. This is needed in the main portion of the program to tell an empty line from one that contains tokens.

The last several lines of the program are responsible for linewise interacting with the console and utilizing the already created String methods. It should be noted that Smalltalk as a language has no notion of standard input or output. Some implementations, however, provide access to the console in one form or another.

The code presented here runs in GNU Smalltalk. It successively reads lines and if a line is non-empty, evaluates it and converts the resulting number into a string, in preparation for printing it. If an exception occurs, it is handled by producing the 'error' string instead of a numeric result to print. Whatever the eventual outcome from the evaluation, it is passed, through show:, to the Transcript, which by default is connected to the standard output.

There is a standard method atEnd to tell whether a stream is at its end. By making use of this method, it would have been possible to express the reading loop in our program more straightforwardly than in the form given here. At the time of writing this (2006), however, atEnd appears to work incorrectly on true (non-redirected to a file) stdin in GNU Smaltalk.

!String methodsFor: 'additions'!

|t sgn p|
t := self asOrderedCollection.
sgn := 1.
t first = $- ifTrue: [sgn := -1].
('+-' includes: t first) ifTrue: [t removeFirst].
p := t indexOf: $. .
p = 0
  ifTrue: [p := 1]
  ifFalse: [p := 10 raisedTo: t size - p.  t remove: $.].
(t isEmpty or: [t anySatisfy: [:c| c isDigit not]])
  ifTrue: [Error signal].
^ sgn / p * (t inject: 0 into:
             [:n :d| 10 * n + ('0123456789' indexOf: d) - 1])
  asFloat !

tks := self subStrings collect:
  [:tk| (#('+' '-' '*' '/') includes: tk)
          ifTrue: [tk asSymbol] ifFalse: [tk asFloat]].
[tks size > 1] whileTrue: [
  |i x y o|
  i := tks findFirst: [:tk| tk isSymbol].
  x := tks at: i - 2.
  y := tks at: i - 1.
  o := tks at: i.
  tks := tks copyReplaceFrom: i - 2 to: i
           with: (Array with: (x perform: o with: y))
^ tks first !

^ self allSatisfy: [:c| (c = Character space) | (c = Character tab)] !

|line notend|
notend := true.
[notend] whileTrue:
  [line := FileStream stdin nextLine.
   ((notend := line notNil) and: [line isWhitespace not])
     ifTrue: [Transcript show: ([line evalAsRpn printString]
                                on: Error do: [:e|'error']); cr]]