PIL - Roman numerals
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.