In the final part of this series, let's create a real program in PIL.

The problem

We will implement arabic to roman number conversion from Rosetta Code.

The algorithm we're going to use is similar to the one used there for BASIC:

  • Have a table of all distinct roman numbers ordered by size, including the -1 variants like IV. So roman(0) = "M", roman(1) = "CM", roman(2) = "D" etc.
  • Have another table with the same indices for their arabic equivalents. arabic(0) = 1000, arabic(1) = 900, arabic(2) = 500 etc.
  • Loop through each index. For each, if the input value is greater than the value of the arabic table at that value, accumulate the roman equivalent at the end of the output string and decrease the input value by the arabic amount. Keep doing this until the remaining input value is smaller than the arabic number.
  • So for input 2900 the steps would be
    • index 0, output -> "M", input -> 1900
    • index 0, output -> "MM" , input -> 900
    • index 1, output -> "MMCM", input -> 0 and end

The solution

As PIL is an interpreted language I'll show a lightly reformatted transcript of my session as I build up the program in separate parts (and make mistakes along the way). Let's get started!

# $run *pil
# Execution begins   20:09:19
  PIL/2: Ready

The tables

First we need to set up the tables for arabic numbers in part 1. I will use the number command so that PIL prompts me with line numbers followed by an underscore automatically.

*number 1, 0.01
&*1.0  _arabic(0) = 1000
&*1.01 _arabic(1) = 900
&*1.02 _arabic(2) = 500
&*1.03 _arabic(3) = 400
&*1.04 _arabic(4) = 100
&*1.05 _arabic(5) = 90
&*1.06 _arabic(6) = 50
&*1.07 _arabic(7) = 40
&*1.08 _arabic(8) = 10
&*1.09 _arabic(9) = 9
&*1.10 _arabic(10) = 5
&*1.11 _arabic(11) = 4
&*1.12 _arabic(12) = 1
&*1.13 _$unnumber

The unnumber command exits numbered line prompting mode. It needs to be prefixed with $ to be executed immediately rather than be entered as part of the program.

Let's run that immediately so we can check it looks correct

*do part 1
*type arabic
  arabic(0) =  1000.0
  arabic(1) =  900.0
  arabic(2) =  500.0
  arabic(3) =  400.0
  arabic(4) =  100.0
  arabic(5) =  90.0
  arabic(6) =  50.0
  arabic(7) =  40.0
  arabic(8) =  10.0
  arabic(9) =  9.0
  arabic(10) =  5.0
  arabic(11) =  4.0
  arabic(12) =  1.0

We can then do the same for the roman numbers.

*number 2, 0.01
&*2.0 _roman(0) = "M"
&*2.01 _roman(1) = "CM"
&*2.02 _roman(2) = "D"
&*2.03 _roman(3) = "CD"
&*2.04 _roman(4) = "C"
&*2.05 _roman(5) = "XC"
&*2.06 _roman(6) = "L"
&*2.07 _roman(7) = "XL"
&*2.08 _roman(8) = "X"
&*2.09 _roman(9) = "IX"
&*2.1 _roman(10) = "V"
&*2.11 _roman(11) = "IV"
&*2.12 _roman(12) = "I"
&*2.13 _$unnumber
*do part 2

The main loop

Let's now make the main loop to convert the number. We'll do it in three parts, first the loop over the indices. I put in some comments fir the function.

*number 5, 0.01
&*5.0 _* Main entry point to arabic -> roman converter
&*5.01 _* Input: a (arabic number to convert)
&*5.02 _* Output: r (roman number equivalent of a)
&*5.03 _for i = 0 to 12: do part 6
&*5.04 _done
&*5.05 _$unnumber

Next, the loop for each arabic number. We can use a for with a dummy variable and the while controlling how often it is run.

*number 6, 0.01
&*6.0 _for j = 0 while a >= arabic(i): do part 7
&*6.01 _done
&*6.02 _$unnumber

Finally, in part 7 build up the roman number string and decrease the arabic number.

*number 7, 0.01
&*7.0 _r = r + roman(i)
&*7.01 _a = a - arabic(i)
&*7.02 _done
&*7.03 _$unnumber

Let's see what these look like now.

*type part 5, part 6, part 7

  5.0    * Main entry point to arabic -> roman converter
  5.01   * Input: a (arabic number to convert)
  5.02   * Output: r (roman number equivalent of a)
  5.03   for i = 0 to 12: do part 6
  5.04   done


  6.0    for j = 0 while a >= arabic(i): do part 7
  6.01   done


  7.0    r = r + roman(i)
  7.01   a = a - arabic(i)
  7.02   done

Trying it out

We can set up the input number in a then call part 5 to convert. The output should go into r.

*a = 13
*do part 5
  Error at step 7.0: r = ?

Ah, r is not initialised so cannot be appended to. We can patch part 5 and try again.

*5.025 r = ""
*do part 5
*type r
  r = "XIII"
*type a
  a =  0.0

Great! There is a side effect though, the input value in a is wiped out as PIL does not have local variables.

Thinking about it, we are relying on the tables being initialised when we run part 5. We should really make it stand-alone by calling part 1 and 2 first.

*5.026 do part 1
*5.027 do part 2

Making it interactive

We should have a way to prompt for a number and then display the conversion.

*number 10, 0.01
&*10.0 _demand a
&*10.01 _do part 5
&*10.02 _type r
&*10.03 _$unnumber

*do part 10
& a = ? _1992
  r = "MCMXCII"

Unit tests!

It may be anachronistic, but we should have some unit tests to see if the conversion works. First let's define a unit test handler in part 20 that takes the arabic number in a, the expected result in rExpected and then checks this matches.

*number 20, 0.01
&*20.0 _do part 5
&*20.01 _if r = rExpected, then type "OK", r; else type "ERROR', r, rExpected
  Error at step 20.01: SYMBOLIC NAME TOO LONG
&*20.02 _if r = re, then type "OK", r; else type "ERROR", r, re
&*20.03 _done
&*20.04 _$unnumber

rExpected is too long for a variable number so we use a shorter name instead, re.

Let's test the tester out.

*re = "XLII"
*a = 42
*do part 20
  Error at step 20.01: SYMBOLIC NAME TOO LONG

Ah, the bad line is still there, so delete that and try again.

*delete step 20.01
*do part 20
  ERROR
  r = ""
  re = "XLII"

Wait, that's not right, why is the output in r blank?

 *type r
  r = ""
 *type a
  a =  0.0

Oh OK, a is clobbered. Let's set it up again.

*a = 42
*do part 5
*type r
  r = "XLII"
*do step 20.02
  OK
  r = "XLII"
*do step 20.02
  OK
  r = "XLII"
*type part 20

  20.0    do part 5
  20.02   if r = re, then type "OK", r; else type "ERROR", r, re
  20.03   done

*a = 42
*re = "XLII"
*do part 20
  OK
  r = "XLII"

That fixed it. Try the error case.

*a = 42
*re = "XXX"
*do part 20
  ERROR
  r = "XLII"
  re = "XXX"

With that done, set up the tests.

*number 21, 0.01
&*21.0 _a = 2009
&*21.01 _re = "MMIX"
&*21.02 _do part 20
&*21.03 _a = 1666
&*21.04 _re = "MDCLXVI"
&*21.05 _do part 20
&*21.06 _a = 3888
&*21.07 _re = "MMMDCCCLXXXVIII"
&*21.08 _do part 20
&*21.09 _done
&*21.1 _$unnumber

And run them.

*do part 21
  OK
  r = "MMIX"
  OK
  r = "MDCLXVI"
  OK
  r = "MMMDCCCLXXXVIII"

All green. However we did not test all cases such as zero, negative numbers, non-integral numbers etc.

Save and load

To confirm the program is all done and we are not relying on anything in the environment, save it to disk, quit and come back into PIL and try re-running.

*create "roman.pil"
  FILE "ROMAN.PIL" IS CREATED
*save as "roman.pil", all parts
  SAVE COMPLETED
*stop
# Execution terminated   18:51:16  T=0.279

# $run *pil
# Execution begins   18:51:37
  PIL/2: Ready
*load "roman.pil"
*do part 10
& a = ?  _42
  r = "XLII"
*do part 21
  OK
  r = "MMIX"
  OK
  r = "MDCLXVI"
  OK
  r = "MMMDCCCLXXXVIII"
*stop

The complete listing

*type all parts

  1.0    arabic(0) = 1000
  1.01   arabic(1) = 900
  1.02   arabic(2) = 500
  1.03   arabic(3) = 400
  1.04   arabic(4) = 100
  1.05   arabic(5) = 90
  1.06   arabic(6) = 50
  1.07   arabic(7) = 40
  1.08   arabic(8) = 10
  1.09   arabic(9) = 9
  1.1    arabic(10) = 5
  1.11   arabic(11) = 4
  1.12   arabic(12) = 1

  2.0    roman(0) = "M"
  2.01   roman(1) = "CM"
  2.02   roman(2) = "D"
  2.03   roman(3) = "CD"
  2.04   roman(4) = "C"
  2.05   roman(5) = "XC"
  2.06   roman(6) = "L"
  2.07   roman(7) = "XL"
  2.08   roman(8) = "X"
  2.09   roman(9) = "IX"
  2.1    roman(10) = "V"
  2.11   roman(11) = "IV"
  2.12   roman(12) = "I"

  5.0    * Main entry point to arabic -> roman converter
  5.01   * Input: a (arabic number to convert)
  5.02   * Output: r (roman number equivalent of a)
  5.025  r = ""
  5.026  do part 1
  5.027  do part 2
  5.03   for i = 0 to 12: do part 6
  5.04   done

  6.0    for j = 0 while a >= arabic(i): do part 7
  6.01   done

  7.0    r = r + roman(i)
  7.01   a = a - arabic(i)
  7.02   done

  10.0    demand a
  10.01   do part 5
  10.02   type r

  20.0    do part 5
  20.02   if r = re, then type "OK", r; else type "ERROR", r, re
  20.03   done

  21.0    a = 2009
  21.01   re = "MMIX"
  21.02   do part 20
  21.03   a = 1666
  21.04   re = "MDCLXVI"
  21.05   do part 20
  21.06   a = 3888
  21.07   re = "MMMDCCCLXXXVIII"
  21.08   do part 20
  21.09   done

Final thoughts

JOSS is a simple but well designed language - it's easy to pick up, has a carefully chosen set of features and does the job it's supposed to do well. Compared to BASIC it seems much more intuitive as a simple language for non-specialists who want to get numeric calculations done quickly. The lack of functions and local variables, plus the heavily interactive nature of the language makes it harder to write larger programs, but given the first version was running in 1963 it's quite an impressive feat of engineering.

PIL, the version of JOSS implemented on MTS, improves the usability of the original language, eg by not requiring a period at the end of each statement. There is enough integration with the operating system to make it usable. It would be interesting to know what type of use it got at UM.

Several languages were inspired by JOSS, including FOCAL on PDP-8s. It's also one of the influences on MUMPS, which is still in use today.

Further information

Full source code for this program can be found on github.