Create a Roman Numeral calculator

18

4

Create a basic calculator for Roman numerals.

Requirements

  • Supports +,-,*,/
  • Input and output should expect only one subtractor prefix per symbol (i.e. 3 can't be IIV because there are two I's before V)
  • Handling of the subtraction principle in input and output must at minimum support modern standard conventions, in which only powers of ten are subtracted from larger numbers (e.g. I,X,C are required subtractors but not V,L,D) and subtraction is never done from a number more than 10x the subtractor (e.g. IX must be supported but IC is not required).
  • Input and output should be left to right in order of value, starting with the largest (i.e. 19 = XIX not IXX, 10 is larger than 9)
  • Left to right, no operator precedence, as if you were using a hand calculator.
  • Supports whole positive numbers input/output between 1-4999 (no need for V̅)
  • No libraries that do roman numeral conversion for you

For you to decide

  • Case sensitivity
  • Spaces or no spaces on input
  • What happens if you get a decimal output. Truncate, no answer, error, etc..
  • What to do for output that you can't handle. Negatives or numbers to large to be printed.
  • Whether to support a more liberal use of the subtraction principle than the minimum requirement.

Extra Credit

  • -50 - Handle up to 99999 or larger. Symbols must include a vinculum

Sample input/output

XIX + LXXX                 (19+80)
XCIX

XCIX + I / L * D + IV      (99+1/50*500+4)
MIV

The shortest code wins.

Danny

Posted 2014-02-12T13:40:11.490

Reputation: 1 563

(99+1/50*500+4)=113 with my calculator (and to formula precedence)... If you want something else have to add parenthesis... – RosLuP – 2017-11-01T21:09:59.670

(99+1/50*500+4) = (99+10+4) = 113, but your sample input/output says it is MIV (1004). – Victor Stafusa – 2014-02-12T15:05:22.157

1@Victor - strict left to right operation - no precedence rules - so 99 + 1 / 50 * 500 + 4 should be calculated as ((((99 + 1) / 50) * 500) + 4) – None – 2014-02-12T15:09:12.047

Is handling numbers like IM = 999 required? – Kendall Frey – 2014-02-12T18:45:54.670

@KendallFrey I would expect you could input IM. Whether the output is IM or CMXCIX for 999 is up to you. Both fit the requirements. – Danny – 2014-02-12T18:51:05.890

2IM is non-standard for modern Roman numeral usage. Typically it's only the 4s and 9s of each order of magnitude (4, 9, 40, 90, 400, 900, etc.) that are done by subtraction. For 1999, MCMXCIX would be canonical, not MIM...watch the credits of any film from that year. Otherwise, where does it end? Are we also expected to support other non-standard subtractions like VL for 45? Would IC with a vinculum over the C have to be supported as 99999 for the bonus? – Jonathan Van Matre – 2014-02-12T23:11:58.017

@JonathanVanMatre Can you think of a way to edit the question such to define the standard way of writing Roman numerals? I am not sure I understand the standard way enough to define it in the requirements (but it looks like most if not all of the answers follow it) – Danny – 2014-02-13T03:54:14.603

I've proposed an edit accordingly. – Jonathan Van Matre – 2014-02-13T05:50:29.533

Answers

9

JavaScript (ES6), 238

c=s=>{X={M:1e3,CM:900,D:500,CD:400,C:100,XC:90,L:50,XL:40,X:10,IX:9,V:5,IV:4,I:1}
n=eval('W='+s.replace(/[\w]+/g,n=>(o=0,n.replace(/[MDLV]|C[MD]?|X[CL]?|I[XV]?/g,d=>o+=X[d]),
o+';W=W')));o='';for(i in X)while(n>=X[i])o+=i,n-=X[i];return o}

Usage:

c("XIX + LXXX")
> "XCIX"
c('XCIX + I / L * D + IV')
> "MIV"

Annotated version:

/**
 * Process basic calculation for roman numerals.
 * 
 * @param {String} s The calculation to perform
 * @return {String} The result in roman numerals
 */
c = s => {
  // Create a lookup table.
  X = {
    M: 1e3, CM: 900, D: 500, CD: 400, C: 100, XC: 90, 
    L: 50,  XL: 40,  X: 10,  IX: 9,   V: 5,   IV: 4, I: 1
  };
  // Do the calculation.
  // 
  // The evaluated string is instrumented to as below:
  //   99+1/50*500+4 -> W=99;W=W+1;W=W/50;W=W*500;W=W+4;W=W
  //                 -> 1004
  n = eval('W=' + s.replace(
    // Match all roman numerals.
    /[\w]+/g,
    // Convert the roman number into an integer.
    n => (
      o = 0,
      n.replace(
        /[MDLV]|C[MD]?|X[CL]?|I[XV]?/g,
        d => o += X[d]
      ),
      // Instrument number to operate left-side operations.
      o + ';W=W'
    )
  ));

  // Convert the result into roman numerals.
  o = '';
  for (i in X)
    while (n >= X[i])
      o += i,
      n -= X[i];

  // Return calculation result.
  return o
}

Florent

Posted 2014-02-12T13:40:11.490

Reputation: 2 557

9

T-SQL, 1974 - 50 = 1924 bytes

I know that golfing in SQL is equivalent to playing 18 holes with nothing but a sand wedge, but I relished the challenge of this one, and I think I managed to do a few interesting things methodologically.

This does support the vinculum for both input and output. I adopted the convention of using a trailing tilde to represent it , so V~ is 5000, X~ is 10000, etc. It should also handle outputs up to 399,999 according to standard modern Roman numeral usage. After that, it will do partially non-standard Roman encoding of anything in INT's supported range.

Because it's all integer math, any non-integer results are implicitly rounded.

DECLARE @i VARCHAR(MAX)
SET @i='I+V*IV+IX*MXLVII+X~C~DCCVI'
SELECT @i

DECLARE @t TABLE(i INT IDENTITY,n VARCHAR(4),v INT)
DECLARE @u TABLE(n VARCHAR(50),v INT)
DECLARE @o TABLE(n INT IDENTITY,v CHAR(1))
DECLARE @r TABLE(n INT IDENTITY,v INT,r VARCHAR(MAX))
DECLARE @s TABLE(v INT,s VARCHAR(MAX))
DECLARE @p INT,@x VARCHAR(4000)='SELECT ',@j INT=1,@m INT,@y INT,@z VARCHAR(2),@q VARCHAR(50)='+-/*~]%'
INSERT @t(n,v) VALUES('i',1),('iv',4),('v',5),('ix',9),('x',10),('xl',50),('l',50),('xc',90),('c',100),('cd',400),('d',500),('cm',900),('m',1000),('mv~',4000),('v~',5000),('mx~',9000),('x~',10000),('x~l~',40000),('l~',50000),('x~c~',90000),('c~',100000)
INSERT @u VALUES('%i[^i'+@q,-2),('%v[^vi'+@q,-10),('%x[^xvi'+@q,-20),('%l[^lxvi'+@q,-100),('%c[^clxvi'+@q,-200),('%d[^dclxvi'+@q,-1000),('%mx~%',-2010),('%x~l~%',-20060),('%x~c~%',-20110)
WHILE PATINDEX('%[+-/*]%', @i)!=0
BEGIN
    SET @p=PATINDEX('%[+-/*]%', @i)
    INSERT @o(v) SELECT SUBSTRING(@i,@p,1)
    INSERT @r(r) SELECT SUBSTRING(@i,1,@p-1)
    SET @i=STUFF(@i,1,@p,'')
END 
INSERT @r(r) SELECT @i
UPDATE r SET v=COALESCE(q.v,0) FROM @r r LEFT JOIN (SELECT r.r,SUM(u.v)v FROM @u u JOIN @r r ON r.r LIKE u.n GROUP BY r.r)q ON q.r=r.r
UPDATE r SET v=r.v+q.v FROM @r r JOIN (SELECT r.n,r.r,SUM((LEN(r.r)-LEN(REPLACE(r.r,t.n,REPLICATE(' ',LEN(t.n)-1))))*t.v) v FROM @r r JOIN @t t ON CHARINDEX(t.n,r.r) != 0 AND (LEN(t.n)=1 OR (LEN(t.n)=2 AND RIGHT(t.n,1)='~')) GROUP BY r.n,r.r) q ON q.r=r.r AND q.n = r.n
SELECT @m=MAX(n) FROM @o
SELECT @x=@x+REPLICATE('(',@m)+CAST(v AS VARCHAR) FROM @r WHERE n=1
WHILE @j<=@m
BEGIN
    SELECT @x=@x+o.v+CAST(r.v AS VARCHAR)+')'
    FROM @o o JOIN @r r ON r.n=o.n+1 WHERE o.n=@j
    SET @j=@j+1
END 
INSERT @s(v,s) EXEC(@x+',''''')
UPDATE @s SET s=s+CAST(v AS VARCHAR(MAX))+' = '
SET @j=21
WHILE @j>0
BEGIN
    SELECT @y=v,@z=n FROM @t WHERE i = @j
    WHILE @y<=(SELECT v FROM @s)
    BEGIN
        UPDATE @s SET v=v-@y,s=s+@z
    END  
    SET @j=@j-1
END
SELECT @x+' = '+UPPER(s) FROM @s

I'm still tinkering with a set-based solution to replace some of the WHILE looping that might whittle down the byte count and be a more elegant example of idiomatic SQL. There are also some bytes to be gained by reducing use of table aliases to a bare minimum. But as it's essentially un-winnable in this language, I'm mostly just here to show off my Don Quixote outfit. :)

SELECT @i at the top repeats the input:

I+V*IV+IX*MXLVII+X~C~DCCVI

And the SELECT at the end returns:

SELECT (((((1+5)*4)+9)*1047)+90706) = 125257 = C~X~X~V~CCLVII

And you can test it yourself at this SQLFiddle

And I will be returning to add some commentary on how it works, because why post an obviously losing answer if you're not going to exploit it for educational value?

Jonathan Van Matre

Posted 2014-02-12T13:40:11.490

Reputation: 2 307

2

Javascript - 482 476 characters

String.prototype.m=String.prototype.replace;eval("function r(a){return a>999?'Mk1e3j899?'CMk900j499?'Dk500j399?'CDk400j99?'Ck100j89?'XCk90j49?'Lk50j39?'XLk40j9?'Xk10j8?'IX':a>4?'Vk5j3?'IV':a>0?'Ik1):''}".m(/k/g,"'+r(a-").m(/j/g,"):a>"));s=prompt();h=s.match(/\w+/gi);for(k in h)s=s.m(h[k],eval(eval("'0'+h[k].m(/IVu4pIXu9pXLu40pXCu90pCDu400pCMu900pMu1000pDu500pCu100pLu50pXu10pVu5pIu1')".m(/u/g,"/g,'+").m(/p/g,"').m(/")))+")");for(k in h)s="("+s;alert(r(Math.floor(eval(s))))

The sample input/output works:

XIX + LXXX -> XCIX
XCIX + I / L * D + IV -> MIV

It badly handles large numbers too:

MMM+MMM -> MMMMMM
M*C -> MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM

And it accepts, but does not requires, spaces too.

But, since I was golfing it has some problems:

  • It does not validates if the input is well-formed. If the input is not well-formed, the behaviour is undefined (and in practice it is very bizarre and strange).
  • It truncates fraction numbers on output (but it is able to do intermediate calculations with them).
  • It really abuses the eval function.
  • It does not attempt to handle negative numbers.
  • It is case-sensitive.

This alternative version handles numbers over 5000 upto 99999, but it has 600 598 584 characters:

String.prototype.m=String.prototype.replace;eval("function r(a){return a>8zz?'XqCqk9e4j4zz?'Lqk5e4j3zz?'XqLqk4e4jzz?'Xqk1e4j89z?'IqXqk9e3j49z?'Vqk5e3j9z?'Mk1e3j8z?'CMk900j4z?'Dk500j3z?'CDk400jz?'Ck100j89?'XCk90j49?'Lk50j39?'XLk40j9?'Xk10j8?'IX':a>4?'Vk5j3?'IV':a>0?'Ik1):''}".m(/k/g,"'+r(a-").m(/j/g,"):a>").m(/q/g,"\u0305").m(/z/g,"99"));s=prompt();h=s.match(/\w+/gi);for(k in h)s=s.m(h[k],eval(eval("'0'+h[k].m(/IVu4pIXu9pXLu40pXCu90pCDu400pCMu900pMu1000pDu500pCu100pLu50pXu10pVu5pIu1')".m(/u/g,"/g,'+").m(/p/g,"').m(/")))+")");for(k in h)s="("+s;console.log(r(Math.floor(eval(s))))

Victor Stafusa

Posted 2014-02-12T13:40:11.490

Reputation: 8 612

I don't think the -20 applies: see vinculum

– SeanC – 2014-02-12T16:02:29.970

Agree with @SeanCheshire here. For the larger number handling the intention is to add a vinculum over the numeral to be 1000 times the value of what it normally is. Maybe it should be larger than a -20 so it make it worth trying for people.

– Danny – 2014-02-12T17:27:52.280

1@Danny I added a version which handles the vinculus, but it increases the code in 116 characters. – Victor Stafusa – 2014-02-12T18:51:35.603

2

Javascript 479 361 348 278 253

303 characters - 50 for supporting numbers up to 1 million, complete with vinculum support:

function p(s){s=s[r](/(^|[*\/+-])/g,"0;s$1=");for(i in v){f=R("\\b"+i);while(f.test(s))s=s[r](f,v[i]+"+")}eval(s+"0");h="";for(i in v)while(s>=v[i]){h+=i;s-=v[i]}return h}v={M̅:1e6,D̅:5e5,C̅:1e5,L̅:5e4,X̅:1e4,V̅:5e3,M:1e3,CM:900,D:500,CD:400,C:100,XC:90,L:50,XL:40,X:10,IX:9,V:5,IV:4,I:1};r="replace";R=RegExp

Usage: p(text), e.g., p('XIX + LXXX') returns XCIX.

Code with explanatory comments:

// Array mapping characters to values
v={M¯:1e6,D¯:5e5,C¯:1e5,L¯:5e4,X¯:1e4,V¯:5e3,M:1e3,CM:900,D:500,CD:400,C:100,XC:90,L:50,XL:40,X:10,IX:9,V:5,IV:4,I:1};
// Shortcut for String.replace
r='replace';
R=RegExp;

// The heart of the program
function p(s) {
    // Replace operators with ";s+=", ";s-=", and so on
    s=s[r](/(^|[*\/+-])/g,'0;s$1=');
    // Loop over the character map and replace all letters with numbers
    for(i in v){
        f=R('\\b'+i);
        while(f.test(s))
            s=s[r](f, v[i]+'+')
    }
    eval(s+'0');
    // Set up our return string
    h='';
    // Replace digits with characters
    for(i in v)
        while(s>=v[i]) {
            h+=i;
            s-=v[i];
        }
    return h;
}

This works for the given samples and for all others I have tried. Examples:

XIX + LXXX = XCIX
XCIX + I / L * D + IV = MIV
XL + IX/VII + II * XIX = CLXXI
CD + C + XL + X + I = DLI
M̅ + I = M̅I
MMMM + M = V̅

elixenide

Posted 2014-02-12T13:40:11.490

Reputation: 250

2

Ruby 2.1, 353 (and many other iterations), 295 - 50 = 245

The vinculum handling adds ~23 characters.

This handles "IL" or "VM" in the input, and fails without error on negatives (goes to high ints) or decimals (truncates), or any spaces. Now also handles a negative first number (though if the total is negative, it still fails poorly). Also fails poorly if you start with * or / or if the result is 4 million or larger.

Uses Object#send for "hand-calculator" functionality.

m=%w{I V X L C D M V̅ X̅ L̅ C̅ D̅ M̅};n=m.zip((0..12).map{|v|(v%2*4+1)*10**(v/2)}).to_h
d=0
gets.scan(/([-+*\/])?([A-Z̅]+)/){|o,l|f=t=0
l.scan(/.̅?/){t-=2*f if f<v=n[$&]
t+=f=v}
d=d.send o||:+,t}
7.downto(1){|v|z=10**v
y=(d%z)*10/z
q,w,e=m[v*2-2,3]
$><<(y>8?q+e : y<4?q*y : y<5?q+w : w+q*(y-5))}

Ungolfed:

m=%w{I V X L C D M V̅ X̅ L̅ C̅ D̅ M̅} # roman numerals
n=m.zip((0..12).map{|v|(v%2*4+1)*10**(v/2)}).to_h # map symbols to values
d=0
gets. # get input and...
  scan(/([-+*\/])?([A-Z̅]+)/) { |l,o|  # for each optional operator plus number
    f=t=0
    l.scan(/.̅?/){                           # read the number in one letter at a time
      t -= 2 * f if f < (v=n[$&])           # if the number's greater than the prev, subtract the prev twice since you already added it
      t += (f = v)                          # add this, and set prev to this number
    }
    d = d.send((o || :+), t)                # now that we've built our number, "o" it to the running total (default to +)
}
7.upto(1) { |v|                        # We now print the output string from left to right
  z = 10**v                            # z = [10, 100, 1000, etc.]
  y = (d%z)*10/z                       # if d is 167 and z is 100, y = 67/10 = 6 
  q,w,e = m[v*2-2,3]                   # q,w,e = X, L, C
  $><< (                               # print: 
    y>8 ? q+e :                        # if y==9,    XC
      y<4 ? q*y :                      # if y<4,     X*y
        y>3 ? q+w :                    # if y==4,    XL
          q*(y-5)                      # else,       L + X*(y-5)
  )
}

Not that Charles

Posted 2014-02-12T13:40:11.490

Reputation: 1 905

2

Python 2 - 427 418 404 401 396 395 392 characters

Reads from standard input. It only handles uppercase (could make it case-insensitive at the cost of 8 extra characters) and requires spaces. Does no validation--I haven't tested to see how it breaks in various cases. It does, however, handle numbers like VC = 95.

N=['?M','DC','LX','VI'];t=0;o='+'
for q in raw_input().split():
 if q in"+-*/":o=q;continue
 n=s=0;X=1
 for l in q:
  x=''.join(N).find(l);v=(5-x%2*4)*10**(3-x/2)
  if X<x:n+=s;s=v;X=x
  elif X>x:n+=v-s;s=0
  else:n+=v+s;s=0
 exec"t"+o+"=n+s"
r=t/1000*'M'
for p,d in enumerate("%04d"%(t%1e3)):
 i="49".find(d);g=N[p]
 if i<0:
  if'4'<d:r+=g[0]
  r+=int(d)%5*g[1]
 else:r+=g[1]+N[p-i][i]
print r

And the ungolfed version:

# Numerals grouped by powers of 10
N = ['?M','DC','LX','VI']
# Start with zero plus whatever the first number is
t = 0
o = '+'
for q in raw_input().split():
    if q in "+-*/":
        # An operator; store it and skip to the next entry
        o = q
        continue
    # n holds the converted Roman numeral, s is a temp storage variable
    n = s = 0
    # X stores our current index moving left-to-right in the string '?MDCLXVI'
    X = 1
    for l in q:
        # x is the index of the current letter in '?MDCLXVI'
        x = ''.join(N).find(l)
        # Calculate the value of this letter based on x
        v = (5 - x%2 * 4) * 10 ** (3 - x/2)
        if X < x:
            # We're moving forward in the list, e.g. CX
            n += s      # Add in any previously-stored value
            s = v       # Store this value in case we have something like CXL
            X = x       # Advance the index
        elif X > x:
            # Moving backward, e.g. XC
            n += v - s  # Add the current value and subtract the stored one
            s=0
        else:
            # Same index as before, e.g. XX
            n += v + s  # Add the current value and any stored one
            s = 0
    # Update total using operator and value (including leftover stored value
    # if any)
    exec "t" + o + "=n+s"

# Now convert the answer back to Roman numerals
# Special-case the thousands digit
r = t / 1000 * 'M'
# Loop over the number mod 1000, padded with zeroes to four digits (to make
# the indices come out right)
for p, d in enumerate("%04d" % (t % 1e3)):
    i = "49".find(d)
    g = N[p]
    if i < 0:
        # i == -1, thus d isn't '4' or '9'
        if '4' < d:
            # >= 5, so add the 5's letter
            r += g[0]
        # ... plus (digit % 5) copies of the 1's letter
        r += int(d) % 5 * g[1]
    else:
        # If it's a 4 or 9, add the 1's letter plus the appropriate
        # larger-valued letter
        r += g[1] + N[p-i][i]
print r

I have a feeling Perl would have been better, but I don't know enough of it. For a first stab at code golf, though, I feel pretty good about this.

DLosc

Posted 2014-02-12T13:40:11.490

Reputation: 21 213

1

PHP — 549 525 524 520 bytes

Nothing too innovative: normalizes the operators to ensure left to right precedence, converts Roman to decimal, runs eval on the statement, e.g. XCIX + I / L * D + IV is converted to something like return (((((+90 +9) + (+1)) / (+50)) * (+500)) + (+4));, then converts decimal back to Roman.

  • final results are truncated
  • answers less than 1 come back blank
  • results are undefined if given incorrect input
$f='str_replace';$g='str_split';$c=array('M'=>1e3,'CM'=>900,'D'=>500,'CD'=>400,'C'=>100,'XC'=>90,'L'=>50,'XL'=>40,'X'=>10,'IX'=>9,'V'=>5,'IV'=>4,'I'=>1);$j='['.$f(array('+','-','*','/'),array('])+[','])-[','])*[','])/['), $argv[1]).'])';$j=str_repeat('(',substr_count($j,')')).$j;$j=$f('[','(',$j);$j=$f(']',')',$j);foreach($g('IVIXXLXCCDCM',2)as$w)$j=$f($w,'+'.$c[$w],$j);foreach($g('IVXLCDM')as$w)$j=$f($w,'+'.$c[$w],$j);$k=eval('return '.$j.';');$l='';foreach($c as$a=>$b){while($k>=$b){$l.=$a;$k-=$b;}}print$l."\n";

e.g.

$ php roman.php 'XCIX + I / L * D + IV' — test case
MIV                                     — 1004

$ php roman.php 'XXXII * LIX'           — 32 × 59
MDCCCLXXXVIII                           — 1888

user15259

Posted 2014-02-12T13:40:11.490

Reputation:

0

Python - 446 bytes

This could be improved considerably. I felt I had to take the first swing using Python. It does 3 things on the first pass

  1. tokenizes the numbers and operators
  2. evaluates the numbers, and enlarges the symbol table x to include all possible combinations encountered (even if they are not used). For example, while XIX is being lexed, the partial values of "X":10, "XI":11 and "XIX":19 are added to the symbol table
  3. inserts nested parens to enforce left-to-right evaluation

At the end, it calls eval on the original string (except with added parens) and gives it the symbol table.

Then I just pasted in a known solution to convert integer to Roman, since I had worked on this long enough... please feel free to improve so that I learn something new :)

m=zip((1000,900,500,400,100,90,50,40,10,9,5,4,1),
('M','CM','D','CD','C','XC','L','XL','X','IX','V','IV','I'))
def doit(s):
 x={'M':1e3,'D':500,'C':100,'L':50,'X':10,'V':5,'I':1};y=[];z='';a='0';s='+'+s
 for c in s.upper():
  if c in x:
   z+=c;y.append(x[c])
   if len(y)>1and y[-1]>y[-2]:y[-2]*=-1
   x[z]=sum(y)
  elif c in"+/*-":a='('+a+z+')'+c;y=[];z=''
 a+=z;i=eval(a,x);r = ''
 for n,c in m:d=int(i/n);r+=c*d;i-=n*d
 return r


print doit("XIX + LXXX")
print doit("XCIX + I / L * D + IV")

Mark Lakata

Posted 2014-02-12T13:40:11.490

Reputation: 1 631