Como você combina apenas algarismos romanos válidos com uma expressão regular?

165

Pensando no meu outro problema , decidi que não posso nem criar uma expressão regular que corresponda a números romanos (sem falar em uma gramática livre de contexto que os gerará)

O problema está correspondendo apenas a números romanos válidos. Por exemplo, 990 NÃO é "XM", é "CMXC"

Meu problema ao fazer a regex para isso é que, para permitir ou não certos caracteres, preciso olhar para trás. Vamos pegar milhares e centenas, por exemplo.

Eu posso permitir M {0,2} C? M (para permitir 900, 1000, 1900, 2000, 2900 e 3000). No entanto, se a correspondência estiver no CM, não posso permitir que os seguintes caracteres sejam C ou D (porque eu já tenho 900).

Como posso expressar isso em uma regex?
Se simplesmente não é expressável em uma regex, é expressável em uma gramática livre de contexto?

Daniel Magliola
fonte

Respostas:

328

Você pode usar o seguinte regex para isso:

^M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$

Dividindo, M{0,4}especifica a seção milhares e basicamente a restringe a entre 0e 4000. É relativamente simples:

   0: <empty>  matched by M{0}
1000: M        matched by M{1}
2000: MM       matched by M{2}
3000: MMM      matched by M{3}
4000: MMMM     matched by M{4}

Obviamente, você poderia usar algo como M*para permitir qualquer número (incluindo zero) de milhares, se desejar permitir números maiores.

A seguir (CM|CD|D?C{0,3}), um pouco mais complexo, isso é para a seção de centenas e abrange todas as possibilidades:

  0: <empty>  matched by D?C{0} (with D not there)
100: C        matched by D?C{1} (with D not there)
200: CC       matched by D?C{2} (with D not there)
300: CCC      matched by D?C{3} (with D not there)
400: CD       matched by CD
500: D        matched by D?C{0} (with D there)
600: DC       matched by D?C{1} (with D there)
700: DCC      matched by D?C{2} (with D there)
800: DCCC     matched by D?C{3} (with D there)
900: CM       matched by CM

Em terceiro lugar, (XC|XL|L?X{0,3})segue as mesmas regras da seção anterior, mas para o lugar das dezenas:

 0: <empty>  matched by L?X{0} (with L not there)
10: X        matched by L?X{1} (with L not there)
20: XX       matched by L?X{2} (with L not there)
30: XXX      matched by L?X{3} (with L not there)
40: XL       matched by XL
50: L        matched by L?X{0} (with L there)
60: LX       matched by L?X{1} (with L there)
70: LXX      matched by L?X{2} (with L there)
80: LXXX     matched by L?X{3} (with L there)
90: XC       matched by XC

E, finalmente, (IX|IV|V?I{0,3})é a seção unidades, a manipulação 0através de 9e também semelhante às duas seções anteriores (algarismos romanos, apesar de sua estranheza aparente, seguir algumas regras lógicas uma vez que você descobrir o que eles são):

0: <empty>  matched by V?I{0} (with V not there)
1: I        matched by V?I{1} (with V not there)
2: II       matched by V?I{2} (with V not there)
3: III      matched by V?I{3} (with V not there)
4: IV       matched by IV
5: V        matched by V?I{0} (with V there)
6: VI       matched by V?I{1} (with V there)
7: VII      matched by V?I{2} (with V there)
8: VIII     matched by V?I{3} (with V there)
9: IX       matched by IX

Lembre-se de que esse regex também corresponderá a uma sequência vazia. Se você não deseja isso (e seu mecanismo de expressão regular é moderno o suficiente), pode usar um look-behind e um look-ahead positivos:

(?<=^)M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})(?=$)

(a outra alternativa é apenas verificar se o comprimento não é zero antes).

paxdiablo
fonte
12
Não deveria ser M {0,3}?
limão
3
alguma solução para evitar combinar a string vazia?
Facundo Casco
11
@Ashish: quando os romanos eram uma força a ser reconhecida, MMMMera o caminho correto. A representação da barra ocorreu muito depois que o império central desmoronou.
22413
2
@paxdiablo é assim que eu encontrei mmmcm falha. String regx = "^ M {0,3} (CM | CD | D? C {0,3}) (XC | XL | L? X {0,3}) (IX | IV | V? I {0, 3}) $ "; if (input.matches (regx)) -> avalia como false para MMMCM / MMMM em java.
Amit
2
/^M{0,3}(?:C[MD]|D?C{0,3})(?:X[CL]|L?X{0,3})(?:I[XV]|V?I{0,3})$/i
Crissov
23

Na verdade, sua premissa é falha. 990 IS "XM", bem como "CMXC".

Os romanos estavam muito menos preocupados com as "regras" do que o seu professor da terceira série. Contanto que tudo funcionasse, tudo bem. Portanto, "IIII" foi tão bom quanto "IV" para 4. E "IIM" foi completamente legal para 998.

(Se você tiver problemas para lidar com isso ... Lembre-se de que as grafias em inglês não foram formalizadas até a década de 1700. Até então, desde que o leitor pudesse descobrir, era bom o suficiente).

James Curran
fonte
8
Claro, isso é legal. Mas o meu "strict professor terceiro grau" necessidade sintaxe torna um problema regex muito mais interessante, na minha opinião ...
Daniel Magliola
5
Bom ponto James, é preciso ser um autor rigoroso, mas um leitor que perdoa.
Corin
13

Apenas para salvá-lo aqui:

(^(?=[MDCLXVI])M*(C[MD]|D?C{0,3})(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$)

Corresponde a todos os números romanos. Não se importa com cadeias de caracteres vazias (requer pelo menos uma letra numérica romana). Deve funcionar em PCRE, Perl, Python e Ruby.

Demonstração on-line do Ruby: http://rubular.com/r/KLPR1zq3Hj

Conversão on-line: http://www.onlineconversion.com/roman_numerals_advanced.htm

sorriso
fonte
2
Não sei por que, mas a resposta principal não funcionou para mim nas listas de tradução automática no MemoQ. No entanto, esta solução sim - excluindo os símbolos de início / fim da string.
Orlando2bjr
1
@ orlando2bjr feliz em ajudar. Sim, neste caso, eu estava combinando um número por conta própria, sem ambiente. Se você o procurar em um texto, com certeza precisará remover ^ $. Felicidades!
Smileart 6/05
12

Para evitar encontrando a string vazia você precisará repetir o padrão quatro vezes e substituir cada 0um 1por sua vez, e são responsáveis por V, Le D:

(M{1,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})|M{0,4}(CM|C?D|D?C{1,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})|M{0,4}(CM|CD|D?C{0,3})(XC|X?L|L?X{1,3})(IX|IV|V?I{0,3})|M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|I?V|V?I{1,3}))

Nesse caso (porque esse padrão usa ^e $), seria melhor verificar primeiro as linhas vazias e não se incomodar em combiná-las. Se você estiver usando limites de palavras , não terá problemas porque não existe uma palavra vazia. (Pelo menos regex não define um; não comece a filosofar, estou sendo pragmático aqui!)


No meu caso particular (do mundo real), eu precisava de números correspondentes às terminações de palavras e não encontrei outra maneira de contornar isso. Eu precisava retirar os números das notas de rodapé do meu documento de texto sem formatação, onde textos como "o cl do Mar Vermelho e o cli da Grande Barreira de Corais " haviam sido convertidos the Red Seacl and the Great Barrier Reefcli. Mas eu ainda tinha problemas com palavras válidas como Tahitie fantasticsão arrastados para Tahite fantasti.

Corin
fonte
Estou com um problema semelhante (!): Fazer um "recorte esquerdo" do número romano restante / residual de uma lista de itens (HTML OL do tipo I ou i). Então, quando houver, eu preciso limpar (como uma função de aparar) com sua regex no início (esquerda) do item-texto ... Mas mais simples: os itens nunca usam Mou Cou L, então, você tem isso tipo de regex simplificado?
Peter Krauss
... ok, aqui parece ok, (!)(X{1,3}(IX|IV|V?I{0,3})|X{0,3}(IX|I?V|V?I{1,3}))
Peter Krauss
1
você não precisa repetir o padrão, para rejeitar cadeias vazias. Você poderia usar uma afirmação lookahead
jfs
7

Felizmente, o intervalo de números é limitado a 1..3999 ou aproximadamente. Portanto, você pode criar a refeição regular regex.

<opt-thousands-part><opt-hundreds-part><opt-tens-part><opt-units-part>

Cada uma dessas partes lidará com os caprichos da notação romana. Por exemplo, usando a notação Perl:

<opt-hundreds-part> = m/(CM|DC{0,3}|CD|C{1,3})?/;

Repita e monte.

Adicionado : O <opt-hundreds-part>pode ser comprimido ainda mais:

<opt-hundreds-part> = m/(C[MD]|D?C{0,3})/;

Como a cláusula 'D? C {0,3}' pode corresponder a nada, não há necessidade do ponto de interrogação. E, provavelmente, os parênteses devem ser do tipo não capturável - em Perl:

<opt-hundreds-part> = m/(?:C[MD]|D?C{0,3})/;

Obviamente, tudo também não diferencia maiúsculas de minúsculas.

Você também pode estender isso para lidar com as opções mencionadas por James Curran (para permitir XM ou IM para 990 ou 999, e CC0 para 400, etc.).

<opt-hundreds-part> = m/(?:[IXC][MD]|D?C{0,4})/;
Jonathan Leffler
fonte
Começando com thousands hundreds tens units, é fácil criar um FSM que calcule e valide dados de números romanos
jfs
O que você quer dizer com felizmente, o intervalo de números é limitado a 1..3999 ou aproximadamente ? Quem o limitou?
SexyBeast
@SexyBeast: Não há nenhuma notação romana padrão para 5.000, muito menos números maiores; portanto, as regularidades que funcionam até então param de funcionar.
Jonathan Leffler
Não sei por que você acredita nisso, mas os números romanos podem representar números na casa dos milhões. pt.wikipedia.org/wiki/Roman_numerals#Large_numbers
AmbroseChapel
@ AmbroseChapel: Como afirmei, não há nenhuma notação padrão (única) para 5.000, muito menos números maiores. Você precisa usar um dos vários sistemas divergentes, conforme descrito no artigo da Wikipedia ao qual está vinculado, e enfrenta problemas com a ortografia do sistema com overbars, underbars ou C reverso etc. E terá que explicar a qualquer pessoa o que sistema que você está usando e o que isso significa; as pessoas, em geral, não reconhecerão os algarismos romanos além de M. Você pode optar por pensar o contrário; essa é sua prerrogativa, assim como é minha prerrogativa defender meus comentários anteriores.
Jonathan Leffler
7
import re
pattern = '^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$'
if re.search(pattern, 'XCCMCI'):
    print 'Valid Roman'
else:
    print 'Not valid Roman'

Para pessoas que realmente querem entender a lógica, dê uma olhada em uma explicação passo a passo em 3 páginas no diveintopython .

A única diferença em relação à solução original (que tinha M{0,4}) é porque descobri que 'MMMM' não é um número romano válido (também os romanos antigos provavelmente não pensaram nesse número enorme e discordarão de mim). Se você é um dos antigos romanos em desacordo, por favor me perdoe e use a versão {0,4}.

Salvador Dalí
fonte
1
o regex na resposta permite numerais vazios. Se você não quiser; você pode usar uma declaração lookahead , para rejeitar cadeias vazias (também ignora o caso das letras).
JFS
2

Estou respondendo a esta pergunta Expressão regular em Python para algarismos romanos aqui,
porque foi marcada como uma duplicata exata desta pergunta.

Pode ser semelhante no nome, mas esta é uma pergunta / problema específico do regex
como pode ser visto por esta resposta a essa pergunta.

Os itens procurados podem ser combinados em uma única alternância e, em seguida,
envoltos em um grupo de captura que será colocado em uma lista com a
função findall () .
É feito assim:

>>> import re
>>> target = (
... r"this should pass v" + "\n"
... r"this is a test iii" + "\n"
... )
>>>
>>> re.findall( r"(?m)\s(i{1,3}v*|v)$", target )
['v', 'iii']

As modificações de regex para fatorar e capturar apenas os números são as seguintes:

 (?m)
 \s 
 (                     # (1 start)
      i{1,3} 
      v* 
   |  v
 )                     # (1 end)
 $
x15
fonte
1

Como Jeremy e Pax apontaram acima ... '^ M {0,4} (CM | CD | D? C {0,3}) (XC | XL | L? X {0,3}) (IX | IV | V? I {0,3}) $ 'deve ser a solução que você procura ...

O URL específico que deveria ter sido anexado (IMHO) é http://thehazeltree.org/diveintopython/7.html

O exemplo 7.8 é o formato abreviado usando {n, m}

Jonathan Leffler
fonte
1

No meu caso, eu estava tentando encontrar e substituir todas as ocorrências de números romanos por uma palavra dentro do texto, para não poder usar o início e o fim das linhas. Portanto, a solução @paxdiablo encontrou muitas correspondências de comprimento zero. Acabei com a seguinte expressão:

(?=\b[MCDXLVI]{1,6}\b)M{0,4}(?:CM|CD|D?C{0,3})(?:XC|XL|L?X{0,3})(?:IX|IV|V?I{0,3})

Meu código Python final era assim:

import re
text = "RULES OF LIFE: I. STAY CURIOUS; II. NEVER STOP LEARNING"
text = re.sub(r'(?=\b[MCDXLVI]{1,6}\b)M{0,4}(?:CM|CD|D?C{0,3})(?:XC|XL|L?X{0,3})(?:IX|IV|V?I{0,3})', 'ROMAN', text)
print(text)

Resultado:

RULES OF LIFE: ROMAN. STAY CURIOUS; ROMAN. NEVER STOP LEARNING
user2936263
fonte
0

Steven Levithan usa esse regex em seu post, que valida números romanos antes de "desmanchar" o valor:

/^M*(?:D?C{0,3}|C[MD])(?:L?X{0,3}|X[CL])(?:V?I{0,3}|I[XV])$/
Mottie
fonte
0

Eu já vi várias respostas que não cobrem cadeias vazias ou usam viseiras para resolver isso. E quero adicionar uma nova resposta que cubra cadeias vazias e não use lookahead. A regex é a seguinte:

^(I[VX]|VI{0,3}|I{1,3})|((X[LC]|LX{0,3}|X{1,3})(I[VX]|V?I{0,3}))|((C[DM]|DC{0,3}|C{1,3})(X[LC]|L?X{0,3})(I[VX]|V?I{0,3}))|(M+(C[DM]|D?C{0,3})(X[LC]|L?X{0,3})(I[VX]|V?I{0,3}))$

Estou permitindo infinito M, M+mas é claro que alguém poderia mudar M{1,4}para permitir apenas 1 ou 4, se desejado.

Abaixo está uma visualização que ajuda a entender o que está fazendo, precedida por duas demos online:

Demo de depuração

Regex 101 Demo

Visualização de expressão regular

Bernardo Duarte
fonte
0

Isso funciona nos mecanismos regex Java e PCRE e agora deve funcionar no JavaScript mais recente, mas pode não funcionar em todos os contextos.

(?<![A-Z])(M*(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3}))(?![A-Z])

A primeira parte é o olhar negativo atroz. Mas, para fins lógicos, é o mais fácil de entender. Basicamente, o primeiro (?<!)está dizendo não corresponder ao meio, ([MATCH])se houver letras chegando antes do meio, ([MATCH])e o último (?!)está dizendo não corresponder ao meio, ([MATCH])se houver letras após ele.

O meio ([MATCH])é apenas o regex mais comumente usado para corresponder à sequência de algarismos romanos. Mas agora, você não quer combinar isso se houver alguma letra em torno dele.

Veja por si mesmo. https://regexr.com/4vce5

ketenks
fonte
-1

O problema da solução de Jeremy e Pax é que ele também não corresponde a "nada".

O seguinte regex espera pelo menos um número romano:

^(M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})|[IDCXMLV])$
Marvin Frommhold
fonte
6
esse não funcionará (a menos que você esteja usando uma implementação de regex muito estranha) - a parte esquerda do |pode corresponder a uma sequência vazia e a todos os algarismos romanos válidos; portanto, o lado direito é completamente redundante. e sim, ainda corresponde a uma sequência vazia.
DirtY iCE 08/08
"O problema da solução de Jeremy e Pax é" ... exatamente o mesmo que o problema que esta resposta tem. Se você vai propor uma solução para um suposto problema, provavelmente deve testá-lo. :-)
paxdiablo
Eu tenho string vazia com este
Aminah Nuraini
-2

Eu escreveria funções para o meu trabalho para mim. Aqui estão duas funções de números romanos no PowerShell.

function ConvertFrom-RomanNumeral
{
  <#
    .SYNOPSIS
        Converts a Roman numeral to a number.
    .DESCRIPTION
        Converts a Roman numeral - in the range of I..MMMCMXCIX - to a number.
    .EXAMPLE
        ConvertFrom-RomanNumeral -Numeral MMXIV
    .EXAMPLE
        "MMXIV" | ConvertFrom-RomanNumeral
  #>
    [CmdletBinding()]
    [OutputType([int])]
    Param
    (
        [Parameter(Mandatory=$true,
                   HelpMessage="Enter a roman numeral in the range I..MMMCMXCIX",
                   ValueFromPipeline=$true,
                   Position=0)]
        [ValidatePattern("^M{0,3}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$")]
        [string]
        $Numeral
    )

    Begin
    {
        $RomanToDecimal = [ordered]@{
            M  = 1000
            CM =  900
            D  =  500
            CD =  400
            C  =  100
            XC =   90
            L  =   50
            X  =   10
            IX =    9
            V  =    5
            IV =    4
            I  =    1
        }
    }
    Process
    {
        $roman = $Numeral + " "
        $value = 0

        do
        {
            foreach ($key in $RomanToDecimal.Keys)
            {
                if ($key.Length -eq 1)
                {
                    if ($key -match $roman.Substring(0,1))
                    {
                        $value += $RomanToDecimal.$key
                        $roman  = $roman.Substring(1)
                        break
                    }
                }
                else
                {
                    if ($key -match $roman.Substring(0,2))
                    {
                        $value += $RomanToDecimal.$key
                        $roman  = $roman.Substring(2)
                        break
                    }
                }
            }
        }
        until ($roman -eq " ")

        $value
    }
    End
    {
    }
}

function ConvertTo-RomanNumeral
{
  <#
    .SYNOPSIS
        Converts a number to a Roman numeral.
    .DESCRIPTION
        Converts a number - in the range of 1 to 3,999 - to a Roman numeral.
    .EXAMPLE
        ConvertTo-RomanNumeral -Number (Get-Date).Year
    .EXAMPLE
        (Get-Date).Year | ConvertTo-RomanNumeral
  #>
    [CmdletBinding()]
    [OutputType([string])]
    Param
    (
        [Parameter(Mandatory=$true,
                   HelpMessage="Enter an integer in the range 1 to 3,999",
                   ValueFromPipeline=$true,
                   Position=0)]
        [ValidateRange(1,3999)]
        [int]
        $Number
    )

    Begin
    {
        $DecimalToRoman = @{
            Ones      = "","I","II","III","IV","V","VI","VII","VIII","IX";
            Tens      = "","X","XX","XXX","XL","L","LX","LXX","LXXX","XC";
            Hundreds  = "","C","CC","CCC","CD","D","DC","DCC","DCCC","CM";
            Thousands = "","M","MM","MMM"
        }

        $column = @{Thousands = 0; Hundreds = 1; Tens = 2; Ones = 3}
    }
    Process
    {
        [int[]]$digits = $Number.ToString().PadLeft(4,"0").ToCharArray() |
                            ForEach-Object { [Char]::GetNumericValue($_) }

        $RomanNumeral  = ""
        $RomanNumeral += $DecimalToRoman.Thousands[$digits[$column.Thousands]]
        $RomanNumeral += $DecimalToRoman.Hundreds[$digits[$column.Hundreds]]
        $RomanNumeral += $DecimalToRoman.Tens[$digits[$column.Tens]]
        $RomanNumeral += $DecimalToRoman.Ones[$digits[$column.Ones]]

        $RomanNumeral
    }
    End
    {
    }
}
Vince Ypma
fonte