Criar uma calculadora de números romanos

18

Crie uma calculadora básica para algarismos romanos.

Exigências

  • Suportes +, -, *,/
  • A entrada e a saída devem esperar apenas um prefixo de subtrator por símbolo (ou seja, 3 não pode ser IIVporque existem dois Iantes V)
  • Manipulação do princípio da subtração na entrada e obrigação de saída em suporte mínimo modernas convenções padrão, em que apenas potências de dez são subtraídos números maiores (por exemplo I, X, Csubtratores são necessários, mas não V, L, D) e subtração nunca é feito a partir de um número de mais de 10x o subtrator (por exemplo, IXdeve ser suportado, mas ICnão é necessário).
  • Entrada e saída devem ser deixadas para a direita em ordem de valor, começando pelo maior (ou seja, 19 = XIXnão IXX, 10 é maior que 9)
  • Da esquerda para a direita, sem precedentes de operador, como se você estivesse usando uma calculadora manual.
  • Suporta números positivos inteiros de entrada / saída entre 1-4999 (sem necessidade de V̅)
  • Nenhuma biblioteca que faça a conversão de números romanos para você

Para você decidir

  • Sensibilidade a maiúsculas e minúsculas
  • Espaços ou nenhum espaço na entrada
  • O que acontece se você obtiver uma saída decimal. Truncar, sem resposta, erro, etc.
  • O que fazer com a saída que você não pode lidar. Negativos ou números muito grandes para serem impressos.
  • Se deve apoiar um uso mais liberal do princípio da subtração do que o requisito mínimo.

Crédito extra

  • -50 - Manuseie até 99999 ou maior. Os símbolos devem incluir um vinculo

Entrada / saída de amostra

XIX + LXXX                 (19+80)
XCIX

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

O código mais curto vence.

Danny
fonte
(99 + 1/50 * 500 + 4) = (99 + 10 + 4) = 113, mas sua entrada / saída de amostra diz que é MIV (1004).
Victor Stafusa
1
@Victor - estrita esquerda para a direita operação - sem regras de precedência - assim 99 + 1/50 * 500 + 4 deve ser calculada como ((((99 + 1) / 50) * 500) 4 +)
O manuseio de números é IM = 999obrigatório?
Kendall Frey
@KendallFrey Espero que você possa contribuir IM. Se a saída é IMou CMXCIXpara 999, é com você. Ambos se encaixam nos requisitos.
1111 Danny
2
IM não é padrão para o uso de números romanos modernos. Normalmente, apenas os 4s e 9s de cada ordem de magnitude (4, 9, 40, 90, 400, 900 etc.) são feitos por subtração. Para 1999, MCMXCIX seria canônico, não MIM ... assista os créditos de qualquer filme daquele ano. Caso contrário, onde termina? Também devemos apoiar outras subtrações não-padrão, como a VL por 45? O IC com um vínculo acima do C teria que ser apoiado como 99999 para o bônus?
Jonathan Van Matre

Respostas:

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}

Uso:

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

Versão anotada:

/**
 * 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
fonte
9

T-SQL, 1974 - 50 = 1924 bytes

Sei que jogar golfe no SQL equivale a jogar 18 buracos com nada além de uma cunha de areia, mas gostei do desafio deste e acho que consegui fazer algumas coisas interessantes metodologicamente.

Isso suporta o vínculo de entrada e saída. Adotei a convenção de usar um til à direita para representá-lo, então V ~ é 5000, X ~ é 10000, etc. Depois disso, ele fará codificação romana parcialmente fora do padrão de qualquer coisa no intervalo suportado pelo INT.

Como é toda matemática inteira, qualquer resultado não inteiro é arredondado implicitamente.

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

Ainda estou trabalhando em uma solução baseada em conjunto para substituir alguns dos loop WHILE que podem diminuir a contagem de bytes e ser um exemplo mais elegante de SQL idiomático. Também há alguns bytes a serem ganhos ao reduzir o uso de aliases da tabela a um mínimo. Mas como é essencialmente imbatível nesse idioma, estou aqui apenas para mostrar minha roupa de Don Quixote. :)

SELECT @i na parte superior repete a entrada:

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

E o SELECT no final retorna:

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

E você pode testá-lo neste SQLFiddle

E voltarei a acrescentar alguns comentários sobre como funciona, porque por que postar uma resposta obviamente perdida se você não deseja explorá-la por valor educacional?

Jonathan Van Matre
fonte
2

Javascript - 482 476 caracteres

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))))

A entrada / saída de amostra funciona:

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

Ele também lida com grandes números:

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

E aceita, mas não exige, espaços também.

Mas, desde que eu estava jogando golfe, ele tem alguns problemas:

  • Não valida se a entrada está bem formada. Se a entrada não for bem formada, o comportamento é indefinido (e, na prática, é muito bizarro e estranho).
  • Ele trunca números de fração na saída (mas é capaz de fazer cálculos intermediários com eles).
  • Abusa realmente a função eval.
  • Ele não tenta lidar com números negativos.
  • Faz distinção entre maiúsculas e minúsculas.

Esta versão alternativa lida com números acima de 5000 até 99999, mas possui 600 598 584 caracteres:

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
fonte
Eu não acho que a -20 se aplica: ver vinculum
SeanC
Concorde com @SeanCheshire aqui. Para o número maior de manipulação, a intenção é adicionar um vinculo sobre o numeral para ser 1000 vezes o valor do que normalmente é. Talvez deva ser maior que -20, para que valha a pena tentar para as pessoas.
1111 Danny
1
@ Danny eu adicionei uma versão que lida com o vinculus, mas aumenta o código em 116 caracteres.
Victor Stafusa 12/02
2

Javascript 479 361 348 278 253

303 caracteres - 50 para números de suporte de até 1 milhão, completos com suporte a vinculum:

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

Uso: p(text)por exemplo, p('XIX + LXXX')retornos XCIX.

Código com comentários explicativos:

// 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;
}

Isso funciona para as amostras fornecidas e para todas as outras que tentei. Exemplos:

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̅
elixenida
fonte
2

Ruby 2.1, 353 (e muitas outras iterações) , 295 - 50 = 245

A manipulação do vinculum adiciona ~ 23 caracteres.

Ele lida com "IL" ou "VM" na entrada e falha sem erro em negativos (vai para ints altos) ou decimais (trunca) ou em qualquer espaço. Agora também lida com um primeiro número negativo (embora, se o total for negativo, ele ainda falhe mal). Também falhará mal se você começar com * ou / ou se o resultado for 4 milhões ou maior.

Usa o objeto # send para a funcionalidade "calculadora de mão".

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)
  )
}
Não que Charles
fonte
2

Python 2 - 427 418 404 401 396 395 392 caracteres

Lê da entrada padrão. Ele lida apenas com maiúsculas (pode diferenciar maiúsculas de minúsculas ao custo de 8 caracteres extras) e requer espaços. Não faz validação - não testei para ver como ele quebra em vários casos. No entanto, lida com números como 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

E a versão não destruída:

# 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

Sinto que Perl teria sido melhor, mas não sei o suficiente. Para uma primeira tentativa no código de golfe, no entanto, me sinto muito bem com isso.

DLosc
fonte
1

PHP - 549 525 524 520 bytes

Nada muito inovador: normaliza os operadores para garantir precedência da esquerda para a direita, converte romano em decimal, é executado evalna instrução, por exemplo, XCIX + I / L * D + IV é convertido em algo como return (((((+90 +9) + (+1)) / (+50)) * (+500)) + (+4)); , em seguida, converte decimal novamente em romano.

  • resultados finais são truncados
  • responde menos de 1 volta em branco
  • os resultados são indefinidos se for dada entrada incorreta
$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";

por exemplo

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

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

fonte
0

Python - 446 bytes

Isso poderia ser melhorado consideravelmente. Eu senti que tinha que dar o primeiro balanço usando Python. Faz 3 coisas na primeira passagem

  1. tokeniza os números e operadores
  2. avalia os números e amplia a tabela de símbolos xpara incluir todas as combinações possíveis encontradas (mesmo que não sejam usadas). Por exemplo, enquanto XIXestá a ser lexed, os valores parciais de "X":10, "XI":11e "XIX":19são adicionados à tabela de símbolos
  3. insere parênteses aninhados para aplicar a avaliação da esquerda para a direita

No final, ele chama evala string original (exceto com parênteses adicionados) e fornece a tabela de símbolos.

Depois colei uma solução conhecida para converter inteiro para romano, pois já havia trabalhado nisso por tempo suficiente ... sinta-se à vontade para melhorar para aprender algo novo :)

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', ' EU'))
def doit (s):
 x = {'M': 1e3, 'D': 500, 'C': 100, 'L': 50, 'X': 10, 'V': 5, 'I': 1}; y = [] ; z = ''; a = '0'; s = '+' + s
 para c em s.upper ():
  se c em x:
   z + = c; y.append (x [c])
   se len (y)> 1e y [-1]> y [-2]: y [-2] * = - 1
   x [z] = soma (y)
  elif c em "+ / * -": a = '(' + a + z + ')' + c; y = []; z = ''
 a + = z; i = eval (a, x); r = ''
 Para n, c em m: d = int (i / n); r + = c * d; i- = n * d
 retornar r


doit de impressão ("XIX + LXXX")
print doit ("XCIX + I / L * D + IV")
Mark Lakata
fonte