¿Cómo combina solo números romanos válidos con una expresión regular?

165

Pensando en mi otro problema , decidí que ni siquiera puedo crear una expresión regular que coincida con números romanos (y mucho menos una gramática libre de contexto que los genere)

El problema es hacer coincidir solo números romanos válidos. Por ejemplo, 990 NO es "XM", es "CMXC"

Mi problema al hacer la expresión regular para esto es que para permitir o no ciertos caracteres, necesito mirar hacia atrás. Tomemos miles y cientos, por ejemplo.

Puedo permitir M {0,2} C? M (para permitir 900, 1000, 1900, 2000, 2900 y 3000). Sin embargo, si la coincidencia está en CM, no puedo permitir que los siguientes caracteres sean C o D (porque ya estoy en 900).

¿Cómo puedo expresar esto en una expresión regular?
Si simplemente no se puede expresar en una expresión regular, ¿se puede expresar en una gramática libre de contexto?

Daniel Magliola
fuente

Respuestas:

328

Puede usar la siguiente expresión regular para esto:

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

Desglosándolo, M{0,4}especifica la sección de miles y básicamente lo restringe entre 0y 4000. Es un relativamente simple:

   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}

Por supuesto, podría usar algo como M*permitir cualquier número (incluido cero) de miles, si desea permitir números más grandes.

El siguiente es (CM|CD|D?C{0,3}), un poco más complejo, esto es para la sección de cientos y cubre todas las posibilidades:

  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

En tercer lugar, (XC|XL|L?X{0,3})sigue las mismas reglas que la sección anterior pero para el lugar de las decenas:

 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

Y, por último, (IX|IV|V?I{0,3})es la sección de unidades, la manipulación 0a través 9y también similar a las dos secciones anteriores (números romanos, a pesar de su aparente rareza, siguen algunas reglas lógicas vez que averiguar lo que son):

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

Solo tenga en cuenta que esa expresión regular también coincidirá con una cadena vacía. Si no quiere esto (y su motor regex es lo suficientemente moderno), puede usar una mirada positiva hacia atrás y hacia adelante:

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

(la otra alternativa es simplemente verificar que la longitud no sea cero de antemano).

paxdiablo
fuente
12
¿No debería ser M {0,3}?
limón
3
¿Alguna solución para evitar hacer coincidir la cadena vacía?
Facundo Casco
11
@Aashish: Cuando los romanos eran una fuerza a tener en cuenta, MMMMera la forma correcta. La representación overbar llegó mucho después de que el imperio central se desmoronara.
paxdiablo
2
@paxdiablo así es como encontré que mmmcm falla. 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)) -> esto evalúa a falso para MMMCM / MMMM en 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

En realidad, tu premisa es defectuosa. 990 IS "XM", así como "CMXC".

Los romanos estaban mucho menos preocupados por las "reglas" que su maestra de tercer grado. Mientras se acumulara, estaba bien. Por lo tanto, "IIII" fue tan bueno como "IV" para 4. Y "IIM" fue completamente genial para 998.

(Si tiene problemas para lidiar con eso ... Recuerde que la ortografía en inglés no se formalizó hasta la década de 1700. Hasta entonces, siempre que el lector pudiera entenderlo, era lo suficientemente bueno).

James Curran
fuente
8
Claro, eso es genial. Pero mi necesidad de sintaxis de "maestro estricto de tercer grado" hace un problema de
expresiones regulares
55
Buen punto James, uno debería ser un autor estricto pero un lector indulgente.
Corin
@Corin: también conocido como el principio de robustez de Postel
jfs
13

Solo para guardarlo aquí:

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

Coincide con todos los números romanos. No le importan las cadenas vacías (requiere al menos una letra de número romano). Debería funcionar en PCRE, Perl, Python y Ruby.

Demostración en línea de Ruby: http://rubular.com/r/KLPR1zq3Hj

Conversión en línea: http://www.onlineconversion.com/roman_numerals_advanced.htm

smileart
fuente
2
No sé por qué, pero la respuesta principal no me funcionó en las listas de autotranslate en MemoQ. Sin embargo, esta solución sí lo hace, excluyendo los símbolos de inicio / final de cadena.
orlando2bjr
1
@ orlando2bjr encantado de ayudar. Sí, en este caso estaba haciendo coincidir un número solo, sin entorno. Si lo busca en un texto, asegúrese de que necesitará eliminar ^ $. ¡Salud!
smileart
12

Para evitar hacer coincidir la cadena vacía, deberá repetir el patrón cuatro veces y reemplazar cada uno 0por uno 1, y tener en cuenta V, Ly 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}))

En este caso (debido a que este patrón usa ^y $) sería mejor que primero verifique si hay líneas vacías y no se moleste en combinarlas. Si está utilizando límites de palabras, entonces no tiene un problema porque no existe una palabra vacía. (Al menos la expresión regular no define uno; no empieces a filosofar, ¡estoy siendo pragmático aquí!)


En mi caso particular (del mundo real), necesitaba números coincidentes en las terminaciones de palabras y no encontré otra forma de evitarlo. Necesitaba borrar los números de las notas al pie de página de mi documento de texto plano, donde se había convertido texto como "el cl del Mar Rojo y el cli de la Gran Barrera de Coral " the Red Seacl and the Great Barrier Reefcli. Pero todavía tenía problemas con palabras válidas como Tahitiy fantasticse eliminan en Tahity fantasti.

Corin
fuente
Tengo un problema similar (!): Hacer un "recorte izquierdo" del número romano restante / residual de una lista de elementos (HTML OL de tipo I o i). Por lo tanto, si hay restos, necesito limpia (como una función de ajuste) con su expresión regular al principio (a la izquierda) del elemento de texto ... Pero lo más simple: nunca utilizan elementos Mo C, o L, por lo que, ¿tiene este tipo de expresiones regulares simplificadas?
Peter Krauss
... ok, aquí parece que está bien (!),(X{1,3}(IX|IV|V?I{0,3})|X{0,3}(IX|I?V|V?I{1,3}))
Peter Krauss
1
no necesita repetir el patrón para rechazar cadenas vacías. Podría usar una afirmación
anticipada
7

Afortunadamente, el rango de números está limitado a 1..3999 o más o menos. Por lo tanto, puede construir la comida de piezas regex.

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

Cada una de esas partes se ocupará de los caprichos de la notación romana. Por ejemplo, usando la notación Perl:

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

Repetir y armar.

Agregado : <opt-hundreds-part>se puede comprimir aún más:

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

Dado que la cláusula 'D? C {0,3}' no puede coincidir con nada, no hay necesidad del signo de interrogación. Y, lo más probable, los paréntesis deben ser del tipo sin captura, en Perl:

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

Por supuesto, también debería ser insensible a mayúsculas y minúsculas.

También puede ampliar esto para manejar las opciones mencionadas por James Curran (para permitir XM o IM para 990 o 999, y CCCC para 400, etc.).

<opt-hundreds-part> = m/(?:[IXC][MD]|D?C{0,4})/;
Jonathan Leffler
fuente
Comenzando con thousands hundreds tens units, es fácil crear un FSM que calcule y valide los números romanos dados
jfs
¿Qué quiere decir con Afortunadamente, el rango de números se limita a 1..3999 o por ahí ? ¿Quién lo limitó?
SexyBeast
@SexyBeast: No hay ninguna notación romana estándar para 5,000, y mucho menos números más grandes, por lo que las regularidades que funcionan hasta entonces dejan de funcionar.
Jonathan Leffler
No estoy seguro de por qué cree eso, pero los números romanos pueden representar números en millones. en.wikipedia.org/wiki/Roman_numerals#Large_numbers
AmbroseChapel
@AmbroseChapel: Como dije, no hay ninguna notación estándar (única) para 5,000, y mucho menos números más grandes. Debe utilizar uno de los diversos sistemas divergentes, tal como se describe en el artículo de Wikipedia con el que se vincula, y enfrenta problemas con la ortografía del sistema con barras superpuestas, barras inferiores o C invertida, etc. Y tendrá que explicar a todos qué sistema que está utilizando y lo que significa; la gente, en general, no reconocerá los números romanos más allá de M. Puede optar por pensar lo contrario; esa es su prerrogativa, así como es mi prerrogativa mantener mis comentarios 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 las personas que realmente quieren entender la lógica, eche un vistazo a una explicación paso a paso en 3 páginas sobre diveintopython .

La única diferencia con la solución original (que tenía M{0,4}) es porque descubrí que 'MMMM' no es un número romano válido (los romanos antiguos probablemente no hayan pensado en ese gran número y estarán en desacuerdo conmigo). Si eres de los romanos viejos en desacuerdo, perdóname y usa la versión {0,4}.

Salvador Dalí
fuente
1
La expresión regular en la respuesta permite números vacíos. Si no lo quieres podría usar una aserción anticipada para rechazar cadenas vacías (también ignora el caso de las letras).
jfs
2

Estoy respondiendo esta pregunta Expresión regular en Python para números romanos aquí
porque se marcó como un duplicado exacto de esta pregunta.

Puede ser similar en nombre, pero esta es una pregunta / problema específico de expresiones regulares
como se puede ver en esta respuesta a esa pregunta.

Los elementos que se buscan pueden combinarse en una sola alternancia y luego
encerrarse dentro de un grupo de captura que se colocará en una lista con la función findall ()
.
Se hace así:

>>> 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']

Las modificaciones de expresiones regulares para factorizar y capturar solo los números son las siguientes:

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

Como Jeremy y Pax señalaron anteriormente ... '^ M {0,4} (CM | CD | D? C {0,3}) (XC | XL | L? X {0,3}) (IX | IV | V? I {0,3}) $ 'debería ser la solución que buscas ...

La URL específica que debería haberse adjuntado (en mi humilde opinión) es http://thehazeltree.org/diveintopython/7.html

El ejemplo 7.8 es la forma corta usando {n, m}

Jonathan Leffler
fuente
1

En mi caso, estaba tratando de encontrar y reemplazar todas las apariciones de números romanos por una palabra dentro del texto, por lo que no pude usar el inicio y el final de las líneas. Entonces, la solución @paxdiablo encontró muchas coincidencias de longitud cero. Terminé con la siguiente expresión:

(?=\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})

Mi código final de Python fue así:

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)

Salida:

RULES OF LIFE: ROMAN. STAY CURIOUS; ROMAN. NEVER STOP LEARNING
usuario2936263
fuente
0

Steven Levithan usa esta expresión regular en su publicación que valida los números romanos antes de "desromanizar" el valor:

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

He visto múltiples respuestas que no cubren cadenas vacías o usan lookaheads para resolver esto. Y quiero agregar una nueva respuesta que cubra cadenas vacías y no use anticipación. La expresión regular es la siguiente:

^(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}))$

Estoy permitiendo infinito M, M+pero, por supuesto, alguien podría cambiar M{1,4}para permitir solo 1 o 4 si lo desea.

A continuación se muestra una visualización que ayuda a comprender lo que está haciendo, precedida por dos demostraciones en línea:

Demo de Debuggex

Regex 101 Demo

Visualización de expresiones regulares

Bernardo Duarte
fuente
0

Esto funciona en los motores de expresiones regulares Java y PCRE y ahora debería funcionar en el último JavaScript, pero puede no funcionar en todos los contextos.

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

La primera parte es la atroz mirada negativa detrás. Pero, para fines lógicos, es el más fácil de entender. Básicamente, el primero (?<!)dice que no coinciden con el medio ([MATCH])si hay letras que vienen antes del medio ([MATCH])y el último (?!)dice que no coinciden con el medio ([MATCH])si hay letras que vienen después.

El medio ([MATCH])es solo la expresión regular más utilizada para hacer coincidir la secuencia de números romanos. Pero ahora, no desea hacer coincidir eso si hay letras alrededor.

Ver por ti mismo. https://regexr.com/4vce5

ketenks
fuente
-1

El problema de la solución de Jeremy y Pax es que también coincide con "nada".

La siguiente expresión regular espera al menos un 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
fuente
66
ese no funcionará (a menos que esté usando una implementación de expresiones regulares muy extraña): la parte izquierda de la |puede coincidir con una cadena vacía y todos los números romanos válidos, por lo que el lado derecho es completamente redundante. y sí, todavía coincide con una cadena vacía.
DirtY iCE
"El problema de la solución de Jeremy y Pax es" ... exactamente el mismo problema que tiene esta respuesta. Si va a proponer una solución a un supuesto problema, probablemente debería probarlo. :-)
paxdiablo
Tengo una cadena vacía con esto
Aminah Nuraini
-2

Escribiría funciones en mi trabajo por mí. Aquí hay dos funciones de números romanos en 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
fuente