7.3. Caso de estudio: números romanos

Seguramente ha visto números romanos, incluso si no los conoce. Puede que los haya visto en copyrights de viejas películas y programas de televisión (“Copyright MCMXLVI” en lugar de “Copyright 1946”), o en las placas conmemorativas en bibliotecas o universidades (“inaugurada en MDCCCLXXXVIII” en lugar de “inaugurada en 1888”). Puede que incluso los haya visto en índices o referencias bibliográficas. Es un sistema de representar números que viene de los tiempos del antiguo imperio romano (de ahí su nombre).

En los números romanos, existen siete caracteres que representan números al combinarlos y repetirlos de varias maneras.

Se aplican las siguientes reglas generales al construir números romanos:

7.3.1. Comprobar los millares

¿Qué se necesitaría para certificar una cadena arbitraria como número romano válido? Miremos un carácter cada vez. Como los números romanos se escriben siempre de mayor a menor, empecemos con el mayor: el lugar de los millares. Para números del 1000 en adelante, los millares se representan con series de caracteres M characters.

Ejemplo 7.3. Comprobación de los millares

>>> import re
>>> pattern = '^M?M?M?$'       1
>>> re.search(pattern, 'M')    2
<SRE_Match object at 0106FB58>
>>> re.search(pattern, 'MM')   3
<SRE_Match object at 0106C290>
>>> re.search(pattern, 'MMM')  4
<SRE_Match object at 0106AA38>
>>> re.search(pattern, 'MMMM') 5
>>> re.search(pattern, '')     6
<SRE_Match object at 0106F4A8>
1 Este patrón tiene tres partes:
  • ^ para hacer que lo que sigue coincida sólo con el comienzo de la cadena. Si no se especificase, el patrón podría coincidir con cualquiera de los caracteres M que hubiese, que no es lo que deseamos. Quiere asegurarse de que los caracteres M, si los hay ahí, estén al principio de la cadena.
  • M? para coincidir con un único carácter M de forma opcional. Como se repite tres veces, estamos buscando cualquier cosa entre cero y tres caracteres M seguidos.
  • $ para que lo anterior preceda sólo al fin de la cadena. Cuando se combina con el carácter ^ al principio, significa que el patrón debe coincidir con la cadena al completo, sin otros caracteres antes o después de las M.
2 La esencia del módulo re es la función search, que toma una expresión regular (pattern) y una cadena ('M') para comprobar si coincide con la expresión regular. Si se encuentra una coincidencia, search devuelve un objeto que tiene varios métodos para describirla; si no hay coincidencia, search devuelve None, el valor nulo de Python. Todo lo que ha de preocuparnos por ahora es si el patrón coincide, cosa que podemos saber con sólo mirar el valor devuelto por search. 'M' coincide con esta expresión regular, porque se ajusta a la primera M opcional, mientras que se ignoran la segunda y tercera M opcionales.
3 'MM' también coincide porque se ajusta a los primeros dos caracteres M opcionales, mientras que se ignora el tercero.
4 'MMM' coincide porque se ajusta a los tres caracteres M del patrón.
5 'MMMM' no coincide. Hay coincidencia con los tres caracteres M, pero la expresión regular insiste en que la cadena debe terminar ahí (debido al carácter $), pero la cadena aún no ha acabado (debido a la cuarta M). Así que search devuelve None.
6 Interesante: una cadena vacía también coincide con esta expresión regular, ya que todos los caracteres M son opcionales.

7.3.2. Comprobación de centenas

Las centenas son más complejas que los millares, porque hay varias maneras mutuamente exclusivas de expresarlas, dependiendo de su valor.

  • 100 = C
  • 200 = CC
  • 300 = CCC
  • 400 = CD
  • 500 = D
  • 600 = DC
  • 700 = DCC
  • 800 = DCCC
  • 900 = CM

Así que hay cuatro patrones posibles:

  • CM
  • CD
  • De cero a tres caracteres C (cero si hay un 0 en el lugar de las centenas)
  • D, seguido opcionalmente de hasta tres caracteres C

Los últimos dos patrones se pueden combinar:

  • una D opcional, seguida de hasta tres caracteres C (opcionales también)

Este ejemplo muestra cómo validar el lugar de las centenas en un número romano.

Ejemplo 7.4. Comprobación de las centenas

>>> import re
>>> pattern = '^M?M?M?(CM|CD|D?C?C?C?)$' 1
>>> re.search(pattern, 'MCM')            2
<SRE_Match object at 01070390>
>>> re.search(pattern, 'MD')             3
<SRE_Match object at 01073A50>
>>> re.search(pattern, 'MMMCCC')         4
<SRE_Match object at 010748A8>
>>> re.search(pattern, 'MCMC')           5
>>> re.search(pattern, '')               6
<SRE_Match object at 01071D98>
1 Este patrón empieza igual que el anterior, comprobando el principio de la cadena, (^), luego el lugar de los millares (M?M?M?). Aquí viene la parte nueva, entre paréntesis, que define un conjunto de tres patrones mutuamente exclusivos, separados por barras verticales: CM, CD, y D?C?C?C? (que es una D opcional seguida de cero a tres caracteres C opcionales). El analizador de expresiones regulares comprueba cada uno de estos patrones en orden (de izquierda a derecha), toma el primero que coincida, y descarta el resto.
2 'MCM' coincide porque la primera M lo hace, ignorando los dos siguientes caracteres M, y CM coincide (así que no se llegan a considerar los patrones CD ni D?C?C?C?). MCM es la representación en números romanos de 1900.
3 'MD' coincide con la primera M, ignorando la segunda y tercera M, y el patrón D?C?C?C? coincide con la D (cada uno de los tres caracteres C son opcionales así que se ignoran). MD es la representación en romanos de 1500.
4 'MMMCCC' coincide con los tres primeros caracteres M, y con el patrón D?C?C?C? coinciden CCC (la D es opcional y se ignora). MMMCCC es la representación en números romanos de 3300.
5 'MCMC' no coincide. La primera M lo hace, las dos siguientes se ignoran, y también coincide CM, pero $ no lo hace, ya que aún no hemos llegado al final de la cadena de caracteres (todavía nos queda un carácter C sin pareja). La C no coincide como parte del patrón D?C?C?C?, ya que se encontró antes el patrón CM, mutuamente exclusivo.
6 Es interesante ver que la cadena vacía sigue coincidiendo con este patrón, porque todas las M son opcionales y se ignoran, y porque la cadena vacía coincide con el patrón D?C?C?C?, en el que todos los caracteres son opcionales, y por tanto también se ignoran.

¡Vaya! ¿Ve lo rápido que las expresiones regulares se vuelven feas? Y sólo hemos cubierto los millares y las centenas de los números romanos. Pero si ha seguido el razonamiento, las decenas y las unidades son sencillas, porque siguen exactamente el mismo patrón de las centenas. Pero veamos otra manera de expresarlo.