Expresiones regulares

Eso que nunca terminamos de aprender

-Todos los ejemplos son para expresiones regulares en javascript, a menos que explícitamente se indique lo contrario.

 

Wait, forgot to escape a space. Wheeeeee[taptaptap]eeeeee.
http://xkcd.com/208/

Empezaremos con un repaso rápido de lo básico:

  • La teoría sólo nos indica tres reglas
    • Concatenación: aa es a concatenado con a.
    • Alternación: a|b indica que aparecerá a o b, pero no ambas.
    • Repetición: a* es a cero o más veces.
  • Existen derivaciones de esas reglas
    • a+ es lo mismo que aa* (Una o más veces a)
    • a{2} es igual a aa
    • a{1,5} es igual a a|aa|aaa|aaaa|aaaaa
    •  a? es lo mismo que a|<vacío> (Como máximo una vez a)
  • Existen atajos
    • Rangos ([0-9], [a-z], [a-e], [a-e1-5])
    • Representaciones: \d es igual a [0-9], \w para [a-zA-Z0-9_].
    • Caracter comodín: .
    • Listados: [abcde123-] es igual a [a-e1-3\-] o a a|b|c|d|e|1|2|3|-
    • Negaciones: [^zxy] significa “cualquier char excepto z, x o  y”.
    • Comienzo del string: ^z significa que empieza con z (Comienzo del string seguido de una z).
    • Finalización del string: z$ significa que termina con z (z seguido de la finalización del string).
  • Los paréntesis son caracteres especiales que indican agrupación
    • aa y (aa) representan el mismo lenguaje regular, pero la última expresión recuerda la cadena encontrada para su uso posterior.

 

Modificadores

Existen 3 modificadores opcionales, que deben ser colocados al final de la expresión regular y en cualquier orden:

g: La expresión regular es marcada como global. No retorna ante el primer match, sino que continúa buscando.

mMultilínea, convierte ^ y $ en «comienzo/final del string o comienzo/final de línea».

iCase insensitive. Ignora la diferencia entre mayúsculas y minúsculas.

Por ejemplo:

La expresión regular /a$/gmi busca todas las a o A seguidas del fin de línea o fin de string.

Greedy vs lazy

Por defecto, los modificadores de repetición (*,  + y ?) se comportan de forma greedy. Esto quiere decir, a grandes rasgos, que harán match con la cadena más grande que admita la expresión regular. Un ejemplo es:

/<.*>/g aplicado sobre la cadena «<uno> y <dos> » hará un solo match, igual a «<uno> y <dos>«.

Si lo convertimos a lazy, agregando un ? (después del modificador en cuestión), nos queda lo siguiente:

/<.*?>/g

Y aplicado sobre el mismo string, producirá dos matches: «<uno>« y «<dos>«.

La explicación de este comportamiento puede verse aquí: http://javascript.info/tutorial/greedy-and-lazy

 

Reemplazando con expresiones regulares

-Los ejemplos siguientes fueron extraídos de casos reales con los que me he encontrado. Fueron modificados ligeramente para demostrar casos puntuales. Las soluciones pueden no ser óptimas, dado que intentan hacer uso de tokens específicos.

 


 

Ejemplo 1:

Supongamos el siguiente escenario:

Debemos crear una función que elimine los ceros sobrantes a la derecha del punto de un nº decimal. A los efectos de este ejemplo, supondremos que el parámetro siempre tendrá el formato de decimal con 2 cifras más un % opcional.

Algunas transformaciones de ejemplo:

  • 100.00 -> 100
  • 100.00% -> 100%
  • 100.01 -> 100.01
  • 100.10% -> 100.1%

 

Rompemos la expresión regular en partes, y agrupamos usando paréntesis:

Comenzamos pensando: “En el caso de que nos llegue ‘.00’, debemos reemplazarlo por la cadena vacía”, lo que traducimos a:

return mysteryString.replace(/(\.00)/, "");

La otra regla es: “Si nos llega ‘.#0’ con # distinto de 0, debemos eliminar el 0 y conservar el resto”, lo que traducimos a (incluyendo la expresión regular anterior):

 

function trimTrailingZeroes(mysteryString) {
return mysteryString.replace(/(\.00)|(\.[1-9])(0)/, "$2");
}

Después de , agregamos dos grupos:

  • $2: (\.[1-9]): el punto seguido de un número.
  • $3: (0): el 0 restante.

Como segundo parámetro de replace(), pasamos un string que acepta ciertos tokens especiales, que son los siguientes:

  • $n: donde n es el índice del grupo, de izquierda a derecha, comenzando en 1. Cada par de paréntesis define un grupo.
  • $&: representa el match  completo.
  • $’: representa la subcadena restante (a la derecha del match).
  • $`: representa la subcadena restante (a la izquierda del match).

Cabe destacar que si se quiere usar el caracter $ se debe escapar como $$.

 


 

Ejemplo 2 (avanzado):

 

Vamos a un ejemplo un poco más completo, esta vez usando la opción de Find & Replace de un IDE cualquiera (ya sea Visual Studio,  Intellij, etcétera).

-Al ser un find & replace, las expresiones regulares mantienen la sintaxis de las regexp en js, excepto por los tokens al principio y al final (“//”).

 

Se requiere reemplazar todos los tags mega-sad-grid por mega-awesome-grid, manteniendo la gran mayoría de los atributos, pero reemplazando grid por data y agregando awesomeness y awesomeness-level.
Una transformación de ejemplo:

<mega-sad-grid grid="myData"
enter="doEnter()"
ng-show="myData.Visible"
filter="filter"
properties="properties">
</mega-sad-grid>

 

 

Resultaría en:


<mega-awesome-grid
data="myData"
enter="doEnter()"
ng-show="myData.Visible"
filter="filter"
awesomeness="true"
awesomeness-level="100"
properties="properties">
</mega-awesome-grid>

Nuevamente, dividimos la expresión regular en partes:

 

Primero debemos hacer match con el tag de apertura correcto, así que tenemos la expresión:

  • $1: (<mega-sad-grid)

 

A continuación buscamos el atributo grid, para poder reemplazarlo. Como no sabemos el orden inicial de los atributos, ni si grid se encontrará al comienzo de una línea o después de algún otro atributo, necesitamos otros 7 grupos:

$2: ([^>]*\n)*

  • Cualquier caracter excepto > (0 o más veces) seguido de un salto de línea. 0 o más veces. Esto es para capturar todas las líneas pero deteniéndonos antes de > (Para evitar un mal match en caso de que exista más de un sad-grid en el documento).

$3: (.*)

$4:(grid)

  • Cualquier carácter, 0 o más veces, seguido de la cadena “grid”. Para capturar el nombre del atributo.
    Aquí usamos .* porque estamos seguros de que, si aparece “grid”, será antes del cierre del tag.

$5: (=\».*\»)

  • Este grupo viene inmediatamente después de grid. Es el valor que le daremos al atributo data a la hora de reemplazar.
    Es  = seguido de una cadena entre comillas.
    Importante: Ignoramos el efecto secundario de .* (que por ser greedy hará match con todo lo que esté a la derecha de =, hasta las últimas comillas) porque es una cadena que igualmente vamos a mantener.

$6: ([^<]*\n)*

  • Igual que antes de grid, tomamos todos los atributos que existan antes del tag de cierre.

$7: (.*</mega-)

  • Hacemos match con la línea en que se encuentra el tag de cierre. (No hace falta escapar /; en js deberíamos usar \/)

sad-grid>

  • Finalmente, el final del tag de cierre, que será reemplazado por el nuevo.

La expresión regular completa queda así:

(<mega-sad-grid)([^>]*\n)*(.*)(grid)(=\".*\")([^<]*\n)*(.*</mega-)(sad-grid>)

Ahora es cuestión de armar la cadena de reemplazo:

<mega-awesome-grid $3 data$5 awesomeness=»true» awesomeness-level=»100″ $6$7awesome-grid>

Donde cada $n corresponde a los índices de los grupos.

 


Como hemos visto, mucho de crear una expresión regular es crear una serie de reglas simples y concatenarlas progresivamente (a diferencia de lo que a simple vista podría parecer una única y gran regla).
Cabe destacar que no todo conjunto de strings puede ser representado por una expresión regular. Un ejemplo es: abc, aabbcc, aaabbbccc, etc. Así que no hay que pensar en una expresión regular como una solución mágica, que se puede aplicar a cualquier problema. Son apenas una herramienta sencilla, cuyo aprendizaje nos causará menos dolores de cabeza que su desconocimiento.

 


Recursos:

Get in Touch