21
\$\begingroup\$

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.

\$\endgroup\$
9
  • \$\begingroup\$ (99+1/50*500+4) = (99+10+4) = 113, but your sample input/output says it is MIV (1004). \$\endgroup\$ Commented Feb 12, 2014 at 15:05
  • 1
    \$\begingroup\$ @Victor - strict left to right operation - no precedence rules - so 99 + 1 / 50 * 500 + 4 should be calculated as ((((99 + 1) / 50) * 500) + 4) \$\endgroup\$
    – user15259
    Commented Feb 12, 2014 at 15:09
  • \$\begingroup\$ Is handling numbers like IM = 999 required? \$\endgroup\$ Commented Feb 12, 2014 at 18:45
  • \$\begingroup\$ @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. \$\endgroup\$
    – Danny
    Commented Feb 12, 2014 at 18:51
  • 2
    \$\begingroup\$ IM 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? \$\endgroup\$ Commented Feb 12, 2014 at 23:11

10 Answers 10

9
\$\begingroup\$

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
}
\$\endgroup\$
1
  • \$\begingroup\$ while(n>=X[i])o+=i,n-=X[i]; => for(;n>=X[i];n-=X[i])o+=i; \$\endgroup\$
    – l4m2
    Commented Mar 16, 2021 at 13:40
9
\$\begingroup\$

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?

\$\endgroup\$
3
\$\begingroup\$

APL (Dyalog Unicode 18.0), 187 − 50 = 137 bytes (SBCS)

Full program using stdin/stdout. Requires 0-based indexing and space(s) between symbols and numbers.

d←,∘⌊⍨'IVXLCD'
'[a-z]'⎕R('\u&',⎕UCS 773)⊢d[∊(2×⌽⍳≢r)+{⍵=⍛⌽⍺0/⍨⍺×⍛⍮⍵-1}⌿0 5⊤1+⍎¨r←⍕⍎∊'\w+'⎕R{⍕(×\1,11⍴5 2)[d⍳⍵.Match](⊣+.ׯ1*2</,)0}'(.)\pM'⎕R'\l1'⌽'-' '\*' '/' '\S+'⎕S'-⍨' '×' '÷⍨' '&'⊢⍞]

Try it online!

  • Full support for vinculum, so 1000 is written as (M is a modern addition)
  • Handles all positive results until 899999
  • Allows non-standard input, including, e.g. purely additive notation and too large/small subtractors
  • Includes support for factorial ! (and actually a bunch of other operations)
\$\endgroup\$
2
\$\begingroup\$

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))))
\$\endgroup\$
3
  • \$\begingroup\$ I don't think the -20 applies: see vinculum \$\endgroup\$
    – SeanC
    Commented Feb 12, 2014 at 16:02
  • \$\begingroup\$ 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. \$\endgroup\$
    – Danny
    Commented Feb 12, 2014 at 17:27
  • 1
    \$\begingroup\$ @Danny I added a version which handles the vinculus, but it increases the code in 116 characters. \$\endgroup\$ Commented Feb 12, 2014 at 18:51
2
\$\begingroup\$

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̅
\$\endgroup\$
2
\$\begingroup\$

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)
  )
}
\$\endgroup\$
2
\$\begingroup\$

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.

\$\endgroup\$
1
\$\begingroup\$

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
\$\endgroup\$
1
\$\begingroup\$

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")
\$\endgroup\$
1
\$\begingroup\$

MATLAB - 543 bytes

I tried not to look at others' answers before working on my own, although my approach ended up similar regardless. I use a regular expression to match word characters (the numbers) or operators, which splits up the input into a cell array alternating between numbers and operators. I then replace the roman numerals in every other cell with itself and a plus sign, using order of precedence on the groups to ensure proper grouping. I define variables with the same name as each split group and use an eval statement on the split roman numeral to convert it to a value with a closing parenthese. I then concatenate the whole list of values and operators with proper opening parentheses to ensure strict left-to-right order of operations. I then deconstruct the resulting number digit-by-digit and convert it to the proper roman numeral based on its "power".

function n=m(I)
e=regexp(I,'\w+|[\+\-\*\/]','match');CM=900;XC=90;IX=9;CD=400;XL=40;IV=4;M=1e3;D=500;C=100;L=50;X=10;V=5;I=1;e(1:2:end)=cellfun(@(x) [num2str(evalin('caller',[x '0'])) ')'],regexprep(e(1:2:end),'CM|XC|IX|CD|XL|IV|.?','$0+'),'un',0);S=num2str(eval(strcat(repmat('(',1,(length(e)+1)/2),e{:})));n='';I='IXCM';V='VLD ';l=length(S);for i=1:l
p=l-i+1;a=eval(S(i));if p>3
n=repmat('M',1,a);continue
end
b=a>4;switch a
case 4
n=[n I(p) V(p)];case 9
n=[n I(p:p+1)];otherwise
n=[n repmat(V(p),1,b) repmat(I(p),1,a-5*b)];end
end

ungolfed:

function n=m(I)
e=regexp(I,'\w+|[\+\-\*\/]','match'); % match roman numerals or operators
CM=900;XC=90;IX=9; % nines
CD=400;XL=40;IV=4; % fours
M=1e3;D=500;C=100; % single character numbers (ones and fives)
L=50;X=10;V=5;I=1;
e(1:2:end)=cellfun(@(x) [num2str(evalin('caller',[x '0'])) ')'],... % Evaluate each group and add a parenthesis
                   regexprep(e(1:2:end),'CM|XC|IX|CD|XL|IV|.?','$0+'),'un',0); % replace each group or single digit with itself and a +
S=num2str(eval(strcat(repmat('(',1,(length(e)+1)/2),e{:}))); % concatenate the split string and evaluate with proper parentheses for left-to-right Order of Operations
n='';         % initialize output 
I='IXCM';     % Define ones and fives
V='VLD ';
l=length(S);
for i=1:l
p=l-i+1;
a=eval(S(i));
if p>3              % if the number has 4 or more digits, just add a bunch of Ms
n=repmat('M',1,a);
continue
end
b=a>4;
switch a
case 4
n=[n I(p) V(p)]; % If the digit is 4, add the proper 1 and 5
case 9
n=[n I(p:p+1)]; % If the digit is 9, add the correct 9 group
otherwise
n=[n repmat(V(p),1,b) repmat(I(p),1,a-5*b)]; % Add a 5 if the digit is greater than 4. add 1s for either the digit value (1,2,3) or the value - 5 (6,7,8)
end
end
\$\endgroup\$

Not the answer you're looking for? Browse other questions tagged or ask your own question.