PLACE
spec
solutions
Vim script

RPN Calculator in Vim script

The Vim script language cannot directly access the operating system's standard input and output devices. Instead, its I/O facilities model a console-like device within the Vim editor's area where normally the so called ‘ex’ or ‘:’ commands are entered – this is how the following script works. It can be executed by ‘sourcing’ it from Vim. Version 7.2 of the editor or later is needed as the earlier ones have no support for floating-point arithmetic in the script language.

The script runs an infinite loop (while 1). Within it, a line is entered and broken down into a list tks of tokens by calling the built-in functions input and split, respectively. An inner loop then processes the list in order to evaluate the RPN expression that it encodes. Since the outer loop is nominally infinite, the only way it can be terminated is sending an abort signal to the script by pressing Ctrl-C. The latter can be seen as the analogue of Ctrl-D or Ctrl-Z for ending the standard input in some known operating systems. The loop is nested within a try-catch-endtry block so that the abort signal gets caught. The handler part, after catch, is empty. We only need to intercept the signal so that the script could terminate unexceptionally. Otherwise the signal would have caused an error message to be emitted.

The inner loop checks each token tk from tks in succession to see whether it parses as a number, or an arithmetic operator, or something else. Regular expressions are involved in the checking, pnum being the pattern that describes the grammar of a number as a sequence of characters. If tk reads as a number, we ensure that in tks it is stored in a floating-point format even if its value is whole. Since the Vim script language regards numbers written without a decimal dot as integers, and performs arithmetic on them as on integers, division would be an inexact operation; e.g., ‘2 / 3’ would give 0. The built-in functions str2float and string which we use to transform tk produce, respectively, a floating-point number from tk and then the string representation of that number.

If tk is an operator and is preceded in tks by at least two tokens, we form an infix expression with this operator and its arguments – the two immediately preceding tokens. The expression is evaluated and the result converted to a string, so that it can be stored back in tks, replacing the operator and its arguments – tks is thus shortened by two items.

If tk is neither of these two kinds of tokens, the evaluation of the RPN expression is terminated by exiting (break) the inner loop.

This way tks is progressively reduced. Its initial part, before the i index, is in fact a stack of intermediate values in the course of the evaluation of the RPN expression: all items up to i are numeric tokens. Eventually, when the inner while is exited, either there is a single numeric token in tks, which is the computed result, or there was a syntax error in the RPN expression. According to this, we print either the result or an error message.

Note that in case of an empty input line the call of split results in an empty list, the inner loop is not executed, and nothing gets printed at all.

It might seem strange that we keep strings and not numbers in tks even when we already know that a given token is a number. The results from all computations carried out by eval are also stored as strings although eval itself returns numbers. The reason for this is that initially all items in tks are strings. The language is peculiar in allowing different elements in a list to receive values of different types, but not allowing any element (or whatever variable) to change its type once it has received a value. So we have either to work with strings all the time, as we do, or introduce a separate list of numbers for storing the intermediate results.

Several remarks on the notation specific to the language. All variables in the script are prefixed with s: to make them local to the script. The vertical bar character (|) joins two commands together in the same line. endw and endt abbreviate endwhile and endtry, respectively. The language tolerates a number of such abbreviations.

let s:pnum = '^[+-]\?\(\.\d\+\|\d\+\(\.\d*\)\?\)$'
try | while 1
  let s:tks = split(input(''))
  echo "\n"
  let s:i = 0
  while s:i<len(s:tks)
    let s:tk = s:tks[s:i]
    if s:tk =~ s:pnum
      let s:tks[s:i] = string(str2float(s:tk))
      let s:i += 1
    elseif s:tk =~ '^[-+*/]$' && s:i > 1
      let s:tks[s:i-2] = string(eval(s:tks[s:i-2] . s:tk . s:tks[s:i-1]))
      call remove(s:tks,s:i-1,s:i)
      let s:i -= 1
    else | break
    endif
  endw
  if !empty(s:tks)
    echo len(s:tks) == 1 && s:tks[0] =~ s:pnum ? s:tks[0] : 'error'
  endif
endw | catch | endt

boykobbatgmaildotcom