The dark art of unearthing magic formulae to improve your golf score was highlighted recently by thospel during the Fonality Golf Challenge.

During the recent follow-up codegolf Roman to Decimal game, I explored the other side of this Roman coin, experimenting with a number of different magic formulae to convert a Roman letter to its corresponding arabic number.

This meditation describes my first attempt at cooking up a golfic magic formula, then further offers, for your enjoyment, a simple golf challenge drawn from that experience.

In many golf games, some of the more alluring approaches turn out to be too long to be competitive. During the recent Fonality game, for instance, Daniel Tuijnman's 135.51 and tybalt89's 155.32 were among the most fascinating of the solutions, yet they proved too lengthy to be serious contenders.

So it proved for me during the codegolf Roman to Decimal game where all my shortest solutions were fairly boring, not employing any magic formulae at all. I doubt, therefore, that the magic formulae presented below would be of much practical use in that game ... unless, of course, you find a dramatic improvement on what I've done.

The Challenge

The mapping of each Roman letter to its corresponding arabic number is shown in the table below:


The challenge is to write a subroutine that takes a single valid Roman letter (I,V,X,L,C,D,M) in $_ and returns the corresponding Arabic (or Scientific) representation.

Example Solution

An example solution is:

# 12345678901234567890123456789012345 sub r{1+4*y/VLD//.E.(3*/M/+2*/C|D/+/X|L/)}
The score is the number of characters in the subroutine body, 35 for this example.

Different Approaches

There are at least two fundamental approaches to this problem:

  • A formula. In this approach, illustrated in the example solution above, you calculate the arabic number from a formula containing the Roman letter.
  • A lookup table. Here, you lookup the arabic number corresponding to a Roman letter from some sort of lookup table or data structure. AFAICT, this approach does seem to produce the shortest solutions. I won't discuss this approach further in this meditation.

The Magic Formula Approach

There's just no sane regularity in this thing. But in "random" mappings with a very small result set like this, the shortest solution is often to make up some magic formula that has no particular meaning, but just happens to give the wanted result.

-- Ton Hospel explaining his original decimal-to-roman magic formula

In the example solution above, notice that the last bit, namely:

maps each Roman letter to a digit as follows:


How to shorten this code? Since a %4 modulo operation always produces a digit in the required 0-3 range, an obvious approach is to concoct some sort of magic formula ending in %4.

Brute Force Search Programs

To find such a formula requires a program to search for the elusive magic formula from the vast number of possible ones. Here was my first such program:

for my $m (1 .. 999) { for my $n (1 .. 99) { for my $p (4 .. 9) { $M = ord(M)x3%$m%$n%$p; $D = ord(D)x3%$m%$n%$p; $C = ord(C)x3%$m%$n%$p; $L = ord(L)x3%$m%$n%$p; $X = ord(X)x3%$m%$n%$p; $V = ord(V)x3%$m%$n%$p; $I = ord(I)x3%$m%$n%$p; if ($M==3 && $D==2 && $C==2 && $L==1 && $X==1 && $V==0 && $I==0) + { print "m=$m n=$n p=$p: M=$M D=$D C=$C L=$L X=$X V=$V I=$I\n"; } } } }

These sort of brute force search programs tend to be a bit slow in Perl. And they're not hard to write in C. Accordingly, I rewrote this Perl program in C as follows:

#include <stdio.h> #define magic(v,m,n,p) ((v) % (m) % (n) % (p)) #define MATCH_M(M,D,C,L,X,V,I) ( (M)==3 \ && (D)==2 && (C)==2 \ && (L)==1 && (X)==1 \ && (V)==0 && (I)==0) int main(int argc, char* argv[]) { int m, n, p; int I, V, X, L, C, D, M; for (m = 1; m <= 999; ++m) { for (n = 1; n <= 99; ++n) { for (p = 4; p <= 9; ++p) { M = magic(777777, m, n, p); // 777777 is ord(M)x3 D = magic(686868, m, n, p); // 686868 is ord(D)x3 C = magic(676767, m, n, p); // 676767 is ord(C)x3 L = magic(767676, m, n, p); // 767676 is ord(L)x3 X = magic(888888, m, n, p); // 888888 is ord(X)x3 V = magic(868686, m, n, p); // 868686 is ord(V)x3 I = magic(737373, m, n, p); // 737373 is ord(I)x3 if (MATCH_M(M,D,C,L,X,V,I)) { printf("m=%d n=%d p=%d: M=%d D=%d C=%d L=%d X=%d V=%d I=%d\n +", m, n, p, M, D, C, L, X, V, I); } } } } return 0; }

Becoming aware that you've stopped playing Perl golf and are now instead writing little C programs to search for magic formulae is an odd experience. :-) Anyway, running either of these programs produces:

m=75 n=50 p=4: M=3 D=2 C=2 L=1 X=1 V=0 I=0 m=734 n=13 p=4: M=3 D=2 C=2 L=1 X=1 V=0 I=0 m=737 n=92 p=5: M=3 D=2 C=2 L=1 X=1 V=0 I=0
which tells you that there are three magic formulae, the shortest of which is the shortest line above, corresponding to Perl code of:
Thankfully, this is 3 strokes shorter than the original non-magic:

Now that was the first magic formula that popped into my head. There are many, many other possible formulae. How to find the shortest one? I have no idea, but, much to my annoyance, I've now found five others the same length as the original above ... but none shorter. Here are the six shortest I've found so far:

sub m1{1+4*y/VLD//.E.ord()x3%75%50%4} sub m2{1+4*y/VLD//.E.ord()*49/3%54%4} sub d1{1 .E.ord()*39%73%7%5>>y/VLD//} sub d2{1 .E.ord()*38/9%62%4>>y/VLD//} sub d3{1 .E.~-ord()*41%52%5>>y/VLD//} sub d4{1 .E.(8^ord)*29%65%4>>y/VLD//}
Update: Found three that are one stroke shorter:
sub d5{1 .E.(72^ord)*5/7%5>>y/VLD//} sub d6{1 .E.(6^ord)%72%7%4>>y/VLD//} sub d7{1 .E.(76^2+ord)%7%5>>y/VLD//}
and two that are three strokes shorter:
sub d8{1 .E.(3^77%ord)%7>>y/VLD//} sub m8{5**y/VLD//.E.(42^88*ord)%5}

Notice that the solutions ending in >>y/VLD// above divide by two rather than multiply by five and therefore have a slightly different mapping, namely:


Can you find a shorter magic formula?


References Added Later

Replies are listed 'Best First'.
Re: Golf: Magic Formula for Roman Numerals
by ambrus (Abbot) on Feb 18, 2007 at 11:21 UTC

    sub m1{1+4*y/VLD//.E.ord()x3%75%50%4}
    can be improved to
    sub m3{5**y/VLD//.E.ord()x3%75%50%4}

    This is much longer than yours, but I thought I'd still mention it:

    sub b0{(1e3,10,5,1,100,500,50)[ord()%87%7]}

    Update: a new one that's only one character too long

    sub b1{M1E3D500C100L50X10V5I1=~$_;$'}

      Update: a new one that's only one character too long
      sub b1{M1E3D500C100L50X10V5I1=~$_;$'}
      Not any more. ;-)
      sub b2{M999D499C99L49X9V4I=~$_+$'}

      Update: As described in The 10**21 Problem (Part I) this one is four strokes shorter:

      sub b3{XCMVLD=~$_;"1E@+"%9995}

        Oh yes, using the return value of matches is a trick that could have helped me in the Fonalty golf as well, as I've realized when reading other's solutions.

Re: Golf: Magic Formula for Roman Numerals
by bart (Canon) on Feb 18, 2007 at 18:51 UTC
    A useless thought: in FORTH, they'd be using the /mod operator. That's a division operator that returns both the (integer) quotient, and the remainder. Just look at the pattern:
    The factor clearly has a repetitive pattern, with a period of 2, the exponent increments once for every period.