Capítulo 9
Expresiones Regulares

Algunas personas, cuando se enfrentan a un problema, dicen ya sé, usaré expresiones regulares. entonces tienen dos problemas.

Jamie Zawinski

Yuan-Ma dijo: "Cuando se corta contra la veta de la madera, se necesita mucha fuerza. Cuando se programa contra la lógica de un problema, se necesita mucho código."

Master Yuan-Ma, The Book of Programming

Las herramientas y técnicas de programación sobreviven y se propagan de una manera caótica y evolutiva. No siempre son las formas mas bonitas y luminosos las que ganan, sino las que funcionan lo suficientemente bien en el nicho adecuado. Por ejemplo, aquellas que estan correctamente integrada con otra pieza tecnologica.

En este capítulo, hablaré de una de esas herramientas, las expresiones regulares. Las expresiones regulares son una forma de describir patrones en los datos tipo cadena. Forman un lenguaje pequeño y separado que forma parte de JavaScript y muchos otros idiomas y herramientas.

Las expresiones regulares son terriblemente incómodas y extremadamente útiles. Su sintaxis es críptica, y la interfaz de programación que proporciona JavaScript es torpe. Pero son una poderosa herramienta para inspeccionar y procesar cadenas. La correcta comprensión de expresiones regulares le hará un programador más eficaz.

Creación de una expresión regular

Una expresión regular es un tipo de objeto. Puede construirse con el constructor _RegExp_ o escribirse como un valor literal encerrando el patrón en caracteres de barra diagonal (_/_).

var re1 = new RegExp("abc");
var re2 = /abc/;

Ambos objetos de expresión regular representan el mismo patrón pattern: un carácter a seguido de un b seguido de un c.

Cuando se utiliza el constructor _RegExp_, el patrón se escribe como una cadena normal, por lo que se aplican las reglas habituales para las barras invertidas.

La segunda notación, donde el patrón aparece entre los caracteres de barra (_/_), trata las barras inversas de manera algo diferente. En primer lugar, ya que una barra inclinada termina el patrón, necesitamos poner una barra invertida antes de cualquier barra inclinada que queramos ser parte del patrón. Además, las barras invertidas que no forman parte de códigos de caracteres especiales (como _\n_) se conservarán, en lugar de ignorarse como están en cadenas, y cambiaran el significado del patrón. Algunos caracteres, como signos de interrogación (_?_) y signos más (_+_), tienen significados especiales en expresiones regulares y deben estar precedidos por una barra invertida si están destinados a representar el carácter en sí.

var eighteenPlus = /eighteen\+/;

Saber cuales carácteres deben usar la barra invertida y cuales no al momento que se escriben expresiones regulares requiere que usted conozca a cada uno de los carácteres especiales. Como en un principio esto puede ser muy engorroso, es recomendable que en caso de duda, sólo ponga una barra invertida antes de cualquier carácter que no sea una letra, número o espacio en blanco.

Testeando las coincidencias (matches)

Los objetos de expresiones regulares tienen varios métodos. El más simple es _test_. Si le pasas una cadena, devolverá un booleano diciéndote si la cadena contiene una coincidencia con el patrón de la expresión propuesta.

console.log(/abc/.test("abcde"));
// → true
console.log(/abc/.test("abxde"));
// → false

Una expresión regular que consta sólo de caracteres no especiales representa simplemente esa secuencia de caracteres. Si “abc” se produce en cualquier parte de la cadena que estamos probando (no sólo al principio), el metodo _test_ devolverá _true_.

Matchear un conjunto de caracteres

Descubrir si una cadena contiene abc podría hacerse con una llamada al método _indexOf_. Las expresiones regulares nos permiten ir más allá y expresar patrones más complicados.

Digamos que queremos buscar en una cadena cualquier número del 0 al 9. En una expresión regular, poner un conjunto de caracteres entre corchetes (_[]_) hace en la expresión de destino busque coincidencias con cualquiera de los caracteres entre los corchetes.

Las dos expresiones siguientes coinciden con todas las cadenas que contienen un dígito:

console.log(/[0123456789]/.test("in 1992"));
// → true
console.log(/[0-9]/.test("in 1992"));
// → true

Entre corchetes, se puede utilizar un guión (_-_) entre dos caracteres para indicar un rango, donde el orden se determina por el número Unicode de cada cáracter. Como los caracteres _0_ a _9_ se sitúan uno al lado del otro en este orden (códigos 48 a 57), entonces _[0-9]_ cubre todos ellos y coincide con cualquier dígito.

Hay una serie de grupos de caracteres comunes que tienen sus propios atajos incorporados. Los dígitos son uno de ellos: _\d_ significa lo mismo que _[0-9]_.

\d Cualquier carácter de dígito
\w Un carácter alfanumérico (carácter de texto o (“word character”)
\s Cualquier espacio en blanco (espacio, pestaña, nueva línea y similar)
\D Carácter que no es un dígito
\W Un carácter no alfanumérico
\S Un carácter no-espacio-en-blanco
. Cualquier carácter excepto carácter de nueva línea

Así que se podría coincidir (matchear) con un formato de fecha y hora como 30-01-2003 15:20 con la siguiente expresión:

var dateTime = /\d\d-\d\d-\d\d\d\d \d\d:\d\d/;
console.log(dateTime.test("30-01-2003 15:20"));
// → true
console.log(dateTime.test("30-jan-2003 15:20"));
// → false

Se ve horrible, ¿verdad? Tiene demasiadas barras invertidas y es difícil detectar el patrón real expresado. Veremos una versión ligeramente mejorada de esta expresión más adelante más adelante.

Estos códigos de barra invertida también se pueden utilizar dentro de corchetes. Por ejemplo, _[\ d.]_ significa cualquier dígito o un carácter de período. Pero tenga en cuenta que el propio período, cuando se usa entre corchetes, pierde su significado especial. Lo mismo ocurre con otros caracteres especiales, como _+_.

Para invertir un conjunto de caracteres, es decir, para expresar que desea que coincida con cualquier carácter excepto los del conjunto, puede escribir (^) después del corchete de apertura.

var notBinary = /[^01]/;
console.log(notBinary.test("1100100010100110"));
// → false
console.log(notBinary.test("1100100010200110"));
// → true

Repitiendo partes de un patrón

Ya sabemos cómo hacer coincidir un solo dígito. ¿Qué pasa si queremos igualar un número entero? ¿Una secuencia de uno o más dígitos?

Cuando se coloca un signo más (_+_) después de algo en una expresión regular, indica que el elemento se puede repetir más de una vez. Así, _/\d+/_ coincide con uno o más caracteres de dígito.

console.log(/'\d+'/.test("'123'"));
// → true
console.log(/'\d+'/.test("''"));
// → false
console.log(/'\d*'/.test("'123'"));
// → true
console.log(/'\d*'/.test("''"));
// → true

El asterisco (_) tiene un significado similar, pero también permite que el patrón coincida cero veces. Algo con un _ al final, nunca impide un patrón de emparejamiento. Igualará cero instancias si no puede encontrar cualquier texto adecuado al patrón.

Un signo de interrogación (?) hace que una parte de un patrón sea "opcional", lo que significa que puede ocurrir ninguna o una vez. En el ejemplo siguiente, se permite que el carácter u se produzca, pero el patrón también coincide (matchea) cuando este falta.

var neighbor = /neighbou?r/;
console.log(neighbor.test("neighbour"));
// → true
console.log(neighbor.test("neighbor"));
// → true

Para indicar que un patrón debe ocurrir un número exacto de veces, utilice llaves (_{}_). Poner `_{4}_ después de un elemento, por ejemplo, indica que se requiere que el elemento se encuentre (o matchee) exactamente cuatro veces. También es posible especificar un rango de esta manera: _{2,4}_` significa que el elemento debe ocurrir al menos dos veces y como máximo cuatro veces.

Aquí hay otra versión del patrón de fecha y hora que permite los días, meses y horas de un solo dígito y dos dígitos, lo que lo hace más agradable y fácil de leer.

var dateTime = /\d{1,2}-\d{1,2}-\d{4} \d{1,2}:\d{2}/;
console.log(dateTime.test("30-1-2003 8:45"));
// → true

También puede especificar rangos abiertos cuando se usan llaves, omitiendo el número después de la coma. Así que _{5,}_ significa cinco o más veces.

Agrupar subexpresiones

Para usar un operador como _*_ o _+_ en más de un elemento a la vez, puede utilizar paréntesis _()_. Una parte de una expresión regular que está encerrada entre paréntesis cuenta como un solo elemento en lo que se refiere a los operadores que lo siguen.

var cartoonCrying = /boo+(hoo+)+/i;
console.log(cartoonCrying.test("Boohoooohoohooo"));
// → true

El primer _+_ y el segundo _+_ se aplican solamente al segundo _o_ en _boo_ y _hoo_, respectivamente. El tercero _+_ se aplica a todo el grupo _(hoo+)_, haciendo coincidir una o más secuencias como esa.

El _i_ al final de la expresión en el ejemplo anterior hace que esta expresi��n regular no sea sensible a mayúsculas y minúsculas (case insensitive), permitiendo que coincida con la mayúscula B en la cadena de entrada, aunque el patrón es todo en minúsculas.

Matcheos y grupos

El método _test_ es la forma más simple de buscar coincidencias con una expresión regular. Sólo dice si coincide y nada más. Las expresiones regulares también tienen un método _exec_ (ejecutar) que devolverá _null_ si no se encontró ninguna coincidencia o, si encontró una, devolverá un objeto con información sobre la coincidencia.

var match = /\d+/.exec("one two 100");
console.log(match);
// → ["100"]
console.log(match.index);
// → 8

Un objeto devuelto desde el método _exec_ tiene una propiedad _índex_ que nos dice en dónde comienza la coincidencia correcta en la cadena. Aparte de eso, el objeto se ve como (y de hecho es) una matriz de cadenas, cuyo primer elemento es la cadena que matcheó (en el ejemplo anterior, ésta es la secuencia de dígitos que estábamos buscando).

Los valores de cadena tienen un método _match_ de coincidencia que se comporta de manera similar.

console.log("one two 100".match(/\d+/));
// → ["100"]

Cuando la expresión regular contiene subexpresiones agrupadas con paréntesis, el texto que coincida con esos grupos también aparecerá en la matriz. Todo el _match_ es siempre el primer elemento. El siguiente elemento es la parte que coincidió por el primer grupo (aquel cuyo paréntesis de apertura viene primero en la expresión), luego el segundo grupo, y así sucesivamente.

var quotedText = /'([^']*)'/;
console.log(quotedText.exec("she said 'hello'"));
// → ["'hello'", "hello"]

Cuando un grupo no encuentra coincidencia en absoluto (por ejemplo, cuando es seguido por un signo de interrogación), su posición en la matriz de salida se mantendrá indefinida. Del mismo modo, cuando un grupo es matcheado varias veces, sólo el último match termina en la matriz.

console.log(/bad(ly)?/.exec("bad"));
// → ["bad", undefined]
console.log(/(\d)+/.exec("123"));
// → ["123", "3"]

Los grupos pueden ser útiles para extraer partes de una cadena. Si no queremos solamente verificar si una cadena contiene una fecha sino también queremos extraerla y construir un objeto que la represente, podemos insertar paréntesis alrededor de los patrones de dígitos y escoger directamente la fecha del resultado de _exec_.

Pero primero, un breve desvío en el que discutimos la forma preferida de almacenar valores de fecha y hora en JavaScript.

El tipo fecha

JavaScript tiene un tipo (_typeof_) de objeto estándar para representar fechas, o más bien, puntos en el tiempo. Se llama _Date_. Si simplemente se crea un objeto _Date_ con _new_, se obtienen la fecha y la hora actual.

console.log(new Date());
// → Wed Dec 04 2013 14:24:57 GMT+0100 (CET)

También puede crear un objeto para una hora específica.

console.log(new Date(2009, 11, 9));
// → Wed Dec 09 2009 00:00:00 GMT+0100 (CET)
console.log(new Date(2009, 11, 9, 12, 59, 59, 999));
// → Wed Dec 09 2009 12:59:59 GMT+0100 (CET)

JavaScript utiliza una convención donde los números de mes empiezan en cero (por lo que diciembre es 11), sin embargo, los números de día empiezan en uno. Esto puede ser muy confuso y tonto. Ten mucho cuidado.

Los últimos cuatro argumentos (horas, minutos, segundos y milisegundos) son opcionales y se toman como cero cuando no se dan.

Las marcas de tiempo se almacenan como el número de milisegundos desde el comienzo de 1970, usando números negativos para tiempos anteriores a 1970 (siguiendo una convención establecida por “Unix time” creada en esa decada). El método _getTime_ en un objeto de _Date_ devuelve este número. Es un número grande.

console.log(new Date(2013, 11, 19).getTime());
// → 1387407600000
console.log(new Date(1387407600000));
// → Thu Dec 19 2013 00:00:00 GMT+0100 (CET)

Si le da al constructor _Date_ un solo argumento, ese argumento se trata como un recuento de milisegundos. Puede obtener el recuento actual de milisegundos creando un nuevo objeto _Date_ y llamando a _getTime_, pero también llamando a la función _Date.now_.

Los objetos _Date_ proporcionan métodos como _getFullYear_, _getMonth_, _getDate_, _getHours_, _getMinutes_ y _getSeconds_ para extraer sus componentes. También hay un método _getYear_ que da un valor de año de dos dígitos bastante inútil (como 93 o 14).

Poniendo paréntesis alrededor de las partes de la expresión que nos interesa, ahora podemos crear fácilmente un objeto _Date_ a partir de una cadena.

function findDate(string) {
  var dateTime = /(\d{1,2})-(\d{1,2})-(\d{4})/;
  var match = dateTime.exec(string);
  return new Date(Number(match[3]),
                  Number(match[2]) - 1,
                  Number(match[1]));
}
console.log(findDate("30-1-2003"));
// → Thu Jan 30 2003 00:00:00 GMT+0100 (CET)

Word and string boundaries

Desafortunadamente, _findDate_ también obtendra fechas sin sentido del tipo 00-1-3000 de la cadena "100-1-30000". Una coincidencia puede ocurrir en cualquier parte de la cadena, por lo que en este caso, sólo comenzará en el segundo carácter y finalizará en el segundo al último carácter.

Si queremos hacer que la coincidencia abarque toda la cadena, podemos agregar los marcadores _ y _$_. El __ coincide con el inicio de la cadena de entrada, mientras que _$_ coincide con el final. Por lo tanto, _/\d+$/_ coincide con una cadena compuesta enteramente de uno o más dígitos, _/!/_ coincide con cualquier cadena que comience con un signo de exclamación y _/x^/ no coincide con ninguna cadena (no puede haber una x antes el comienzo de la cadena).

Si, por el contrario, sólo queremos asegurarnos de que la fecha empiece y termine en un límite de palabra, podemos usar el marcador \b. Un límite de palabra puede ser el comienzo o el final de la cadena o cualquier punto de la cadena que tenga un carácter de palabra (como en \w) en un lado y un carácter nonword en el otro.

console.log(/cat/.test("concatenate"));
// → true
console.log(/\bcat\b/.test("concatenate"));
// → false

Tenga en cuenta que un marcador de límite no representa un carácter real. Sólo hace que la expresión regular coincida solamente si una determinada condición se mantiene donde aparece en el patrón.

Patrones de elección

Digamos que no queremos saber si una parte del texto contiene un número, sino que buscamos un número seguido por una de las palabras pig, cow o chicken, o cualquiera de sus formas plurales.

Podríamos escribir tres expresiones regulares y probarlas a su vez, pero hay una manera más agradable. El carácter | (tubo) indica una opción entre el patrón a su izquierda y el patrón a su derecha. Así que puedo decir esto:

var animalCount = /\b\d+ (pig|cow|chicken)s?\b/;
console.log(animalCount.test("15 pigs"));
// → true
console.log(animalCount.test("15 pigchickens"));
// → false

Los paréntesis () se pueden usar para limitar la parte del patrón a la que se aplica el operador | (tubo) y se pueden poner varios operadores de este tipo, uno al lado del otro, para expresar opciones entre más de dos patrones.

La mecánica del matcheo o coincidencia

Las expresiones regulares pueden considerarse diagramas de flujo. Este es el diagrama para el ejemplo anterior:

Visualization of /\b\d+ (pig|cow|chicken)s?\b/

Nuestra expresión coincide (matchea) con una cadena de caracteres si podemos seguir el camino desde el lado izquierdo del diagrama hacia el lado derecho. Mantenemos una posición en la cadena y, cada vez que nos movemos a través de una caja, verificamos que la parte de la cadena que dejamos atras coincide con dicha casilla.

Así que si tratamos de matchear "the 3 pigs" con nuestra expresión regular, nuestro progreso a través del diagrama de flujo sería así:

Conceptualmente, un motor de expresiones regulares busca una coincidencia en una cadena de la siguiente manera: comienza al principio de la cadena e intenta una coincidencia allí. En este caso, hay un límite de palabra (word boundary) allí, por lo que pasaría la primera casilla, pero no hay dígitos, por lo que fallaría en la segunda casilla. Luego se mueve al segundo carácter de la cadena e intenta comenzar un nuevo partido allí... y así sucesivamente, hasta que encuentre una coincidencia o llegue al final de la cadena y decida que realmente no hay coincidencia.

Retroceso

La expresión regular _/\b([01]+b|\d+|[\da-f]+h)\b/_ coincide con un número binario seguido de a b, un número decimal regular sin carácter de sufijo o un número hexadecimal (Es decir, base 16, con las letras de a - f para representar dígitos 10 a 15) seguido de h. Este es el diagrama correspondiente:

Visualization of /\b([01]+b|\d+|[\da-f]+h)\b/

Al hacer coincidir esta expresión, sucederá a menudo que la rama superior (binaria) se ingrese aunque la entrada no contenga realmente un número binario. Al buscar en la cadena "103", por ejemplo, sólo queda claro en el 3 que estamos en la rama equivocada. La cadena coincide con la expresión, pero no con la rama en la que estamos actualmente.

Así que el matcher retrocede. Al entrar en una rama recuerda su posición actual (en este caso, al comienzo de la cadena, justo después de la primera caja de límites en el diagrama) para que pueda volver atrás y probar otra rama si la actual no funciona. Para la cadena "103", después de encontrar el carácter 3, comenzará a probar la rama para los números decimales. Éste coincide, por lo que un match es reportado al final de todo.

El matcher se detiene tan pronto como encuentra una coincidencia completa. Esto significa que si varias ramas podrían potencialmente coincidir con una cadena, sólo se utiliza la primera (ordenada según aparecen en la expresión regular).

El retroceso también ocurre para los operadores de repetición como _+_ y _*_. Si coincide con _/^.*x/_ contra "abcxe", la parte _*_ Intentará primero consumir toda la cadena. El motor entonces se dará cuenta de que necesita una _x_ para que coincida con el patrón. Puesto que no hay _x_ superado el final de la cadena, el operador _*_ intentara igualar un carácter menos. Pero el _matcher_ tampoco encuentra una _x_ después de _abcx_, por lo que retrocede de nuevo, haciendo coincidir el operador _*_ con _abc_. Ahora si encuentra una _x_ donde lo necesita e informa de una coincidencia exitosa desde las posiciones _0_ a _4_.

Es posible escribir expresiones regulares con mucho retroceso (backtracking). Esto se produce cuando un patrón puede coincidir con una pieza de entrada de muchas maneras diferentes. Por ejemplo, si nos confundimos al escribir una expresión regular de un número binario, podríamos escribir algo como _/([01]+)+b/_.

Visualization of /([01]+)+b/

Si intenta hacer coincidir una serie larga de ceros y unos sin ningún carácter b, el matcher primero pasará por el bucle interno hasta que se quede sin dígitos. Entonces advierte que no hay b, por lo que retrocede una posición, pasa por el bucle externo una vez, y se da de nuevo, tratando de retroceder fuera del bucle interior una vez más. Seguirá intentando cada ruta posible a través de estos dos bucles. Esto significa que la cantidad de trabajo se duplica con cada carácter adicional. Para incluso sólo unas pocas docenas de caracteres, el matcheo resultante puede ser prácticamente eterno.

El método de reemplazo

Los valores de cadena tienen un método _replace_, que se puede usar para reemplazar parte de la cadena con otra cadena.

console.log("papa".replace("p", "m"));
// → mapa

El primer argumento también puede ser una expresión regular, en cuyo caso la primera coincidencia de la expresión regular se sustituye. Cuando se agrega una opción _g_ (global) a la expresión regular, todas las coincidencias de la cadena se reemplazarán, no sólo la primera.

console.log("Borobudur".replace(/[ou]/, "a"));
// → Barobudur
console.log("Borobudur".replace(/[ou]/g, "a"));
// → Barabadar

Hubiera sido razonable si la opción entre reemplazar un solo patrón o todos los patrones se hiciera a través de un argumento adicional o proporcionando un método diferente como _replaceAll_, pero por alguna razón, esta opción se indica en una propiedad de la misma expresión regular.

El poder real de usar expresiones regulares con _replace_ viene del hecho de que podemos referirnos de nuevo a los grupos coincidentes en la cadena de reemplazo. Por ejemplo, digamos que tenemos una cadena grande que contiene los nombres de las personas, un nombre por línea, en el formato Apellido, Nombre. Si queremos intercambiar estos nombres y eliminar la coma para obtener un simple formato apellido-nombre, podemos utilizar el siguiente código:

console.log(
  "Hopper, Grace\nMcCarthy, John\nRitchie, Dennis"
    .replace(/([\w ]+), ([\w ]+)/g, "$2 $1"));
// → Grace Hopper
//   John McCarthy
//   Dennis Ritchie

Los _$1_ y _$2_ en la cadena de reemplazo se refieren a los grupos entre paréntesis en el patrón. $1 se sustituye por el texto que coincide con el primer grupo, _$2_ por el segundo, y así sucesivamente hasta _$9_. Todo el _match_ puede ser referido con _$&_.

También es posible pasar una función, en lugar de una cadena, como el segundo argumento a reemplazar. Para cada reemplazo, la función se llamará con los grupos coincidentes (así como la coincidencia completa) como argumentos, y su valor de retorno se insertará en la nueva cadena.

Aquí hay un ejemplo sencillo:

var s = "the cia and fbi";
console.log(s.replace(/\b(fbi|cia)\b/g, function(str) {
  return str.toUpperCase();
}));
// → the CIA and FBI

Y aquí hay una más interesante:

var stock = "1 lemon, 2 cabbages, and 101 eggs";
function minusOne(match, amount, unit) {
  amount = Number(amount) - 1;
  if (amount == 1) // only one left, remove the 's'
    unit = unit.slice(0, unit.length - 1);
  else if (amount == 0)
    amount = "no";
  return amount + " " + unit;
}
console.log(stock.replace(/(\d+) (\w+)/g, minusOne));
// → no lemon, 1 cabbage, and 100 eggs

Este ejemplo toma una cadena, encuentra todas las veces que aparece un número seguido de una palabra alfanumérica, y devuelve una cadena (string) en la que cada una de tales apariciones se decrementa en una.

El grupo _(\d+)_ termina como el argumento _amount_ de la función, y el grupo _(\w+)_ se vincula al argumento _unit_. La función convierte _amount_ en un número -que siempre funciona, ya que coincide con _\d+-_ y hace algunos ajustes en caso de que sólo quede uno o cero, como quitar el plural o avisar la falta de stock con la palabra “no”.

Codicia

No es difícil utilizar _replace_ para escribir una función que elimina todos los comentarios de una pieza de código JavaScript. He aquí un primer intento:

function stripComments(code) {
  return code.replace(/\/\/.*|\/\*[^]*\*\//g, "");
}
console.log(stripComments("1 + /* 2 */3"));
// → 1 + 3
console.log(stripComments("x = 10;// ten!"));
// → x = 10;
console.log(stripComments("1 /* a */+/* b */ 1"));
// → 1  1

La parte anterior al operador _or_ (o) simplemente hace coincidir dos caracteres de barra diagonal seguidos de cualquier número de caracteres de no-nueva-línea. La parte para comentarios multilínea está más involucrada. Utilizamos _[^]_ (cualquier carácter que no esté en el conjunto vacío de caracteres) como una forma de coincidir con cualquier carácter. No podemos usar un punto aquí porque los comentarios de bloque pueden continuar en una nueva línea y los puntos no coinciden con el carácter de nueva-línea.

Pero la salida del ejemplo anterior parece funcionar mal. ¿Por qué?

La parte _[^]*_ de la expresión, tal como describí en la sección de retroceso, se ajustará primero tanto como pueda. Si esto hace que la siguiente parte del patrón falle, el matcher retrocede un carácter e intenta de nuevo desde allí. En el ejemplo, el primero busca coincidir con todo el resto de la cadena (string) y luego retrocede desde allí. Encontrará una instancia de _*/_ después de volver cuatro caracteres y emparejar eso. Esto no es lo que queríamos: la intención era hacer coincidir un solo comentario, no ir hasta el final del código y encontrar el final del último comentario de bloque.

Debido a este comportamiento, decimos que los operadores de repetición (_+_, _*_, _?_, y _{}_) son codiciosos, lo que significa que coinciden tanto como pueden y retroceden desde allí. Si lo combinas con un signo de interrogación (_+?_, _*?_, _??_, _{}_?), pierden su codicia y empiezan por coincidir lo menos posible, matcheando más sólo cuando el patrón restante no encaja en el match más pequeño.

Y eso es exactamente lo que queremos en este caso. Al tener _*_ coincidir con la menor extensión de caracteres que nos lleve a un _*/_, consumimos un comentario de bloque y nada más.

function stripComments(code) {
  return code.replace(/\/\/.*|\/\*[^]*?\*\//g, "");
}
console.log(stripComments("1 /* a */+/* b */ 1"));
// → 1 + 1

Una gran cantidad de errores bug en los programas de expresión regular se puede detectar al comprobar el uso indebido de este comportamiento codicioso de un operador de repetición. Cuando utilice un operador de repetición, considere primero la variante de cambiar su comportamiento con _?_.

Creación dinámica de objetos RegExp

Hay casos en los que puede que no conozca el patrón exacto que necesita para matchear en el momento que está escribiendo su código. Digamos que desea buscar el nombre del usuario en un fragmento de texto y adjuntarlo en caracteres de subrayado para que se destaque. Dado que sabrá el nombre sólo una vez que el programa se esté ejecutando, no podrá utilizar la notación basada en barras.

Pero puede construir una cadena y usar el constructor _RegExp_. He aquí un ejemplo:

var name = "harry";
var text = "Harry is a suspicious character.";
var regexp = new RegExp("\\b(" + name + ")\\b", "gi");
console.log(text.replace(regexp, "_$1_"));
// → _Harry_ is a suspicious character.

Al crear los _\b_ boundary marcadores de frontera, tenemos que usar dos barras invertidas porque las estamos escribiendo en una cadena normal, no una expresión regular con barra inclinada. El segundo argumento para el constructor _RegExp_ contiene las opciones para la expresión regular, en este caso _"gi"_ para global y sin distinción entre mayúsculas y minúsculas.

¿Pero qué ocurre si el nombre es _"dea+hl[]rd"_, un nombre posble si nuestro usuario es un nerd adolescente? Esto daría lugar a una expresión regular absurda, que no coincidirá con el nombre del usuario.

Para evitar esto, podemos agregar barras inversas antes de cualquier carácter en el que no confiamos. Agregar barras invertidas antes de caracteres alfabéticos es una mala idea porque cosas como _\b_ y _\n_ tienen un significado especial. Pero escapar de todo lo que no es alfanumérico o espacio en blanco es seguro.

var name = "dea+hl[]rd";
var text = "This dea+hl[]rd guy is super annoying.";
var escaped = name.replace(/[^\w\s]/g, "\\$&");
var regexp = new RegExp("\\b(" + escaped + ")\\b", "gi");
console.log(text.replace(regexp, "_$1_"));
// → This _dea+hl[]rd_ guy is super annoying.

El método search (búsqueda)

El método _indexOf_ de cadenas no se puede llamar desde una expresión regular. Pero hay otro método, _search_, que se puede invocar en una expresión regular. Al igual que _indexOf_, devuelve el primer índice en el que se encontró la expresión o devolverá -1 si no encontró nada.

console.log("  word".search(/\S/));
// → 2
console.log("    ".search(/\S/));
// → -1

Desafortunadamente, no hay manera de indicar que el matcheo comience en un determinado indice (como si podemos hacerlo con el segundo argumento en _indexOf_).

La propiedad lastIndex

El método _exec_ tampoco proporciona una manera comoda de iniciar una búsqueda desde una posición dada en la cadena, pero al menos si proporciona una manera incómoda de hacerlo.

Los objetos de expresión regular tienen propiedades. Una de estas propiedades es _source_, que contiene la cadena original de la que se creó la expresión. Otra propiedad es _lastIndex_, que controla, solo en algunas circunstancias, donde se iniciará la próxima partida.

Esas circunstancias son que la expresión regular debe tener activada la opción global (_g_), y la coincidencia debe pasar a través del método _exec_. Una vez más, una solución más lógica habría sido simplemente permitir que un argumento extra fuera pasado a _exec_, pero la cordura no es una característica de la interfaz de expresión regular de JavaScript.

var pattern = /y/g;
pattern.lastIndex = 3;
var match = pattern.exec("xyzzy");
console.log(match.index);
// → 4
console.log(pattern.lastIndex);
// → 5

Si el resultado fue satisfactorio, la llamada a _exec_ actualiza automáticamente la propiedad _lastIndex_ para apuntar después de la coincidencia. Si no se encontró ninguna coincidencia, _lastIndex_ se pone de nuevo a cero, que es también el valor que tiene en un objeto de expresión regular de nueva construcción.

Cuando se utiliza un valor de expresión regular global para varias llamadas _exec_, estas actualizaciones automáticas a la propiedad _lastIndex_ pueden causar problemas. Su expresión regular podría iniciarse accidentalmente en un índice que quedó de una llamada anterior.

var digit = /\d/g;
console.log(digit.exec("here it is: 1"));
// → ["1"]
console.log(digit.exec("and now: 1"));
// → null

Otro efecto interesante de la opción global es que cambia la forma en que funciona el método de coincidencia en cadenas (_match_). Cuando se llama con una expresión global, en lugar de devolver una matriz similar a la devuelta por _exec_, _match_ encontrará todas las coincidencias del patrón en la cadena y devuelve una matriz que contiene las cadenas coincidentes.

console.log("Banana".match(/an/g));
// → ["an", "an"]

Así que tenga cuidado con las expresiones regulares globales. Los casos en los que son necesarios (las llamadas a _replace_ y los lugares donde se desea utilizar explícitamente _lastIndex_) suelen ser los únicos lugares donde debería utilizarlos.

Bucle de matchs

Un patrón común es escanear todas las instancias de un patrón en una cadena, de una manera que nos da acceso al objeto de coincidencia en el cuerpo del bucle, usando _lastIndex_ y _exec_.

var input = "A string with 3 numbers in it... 42 and 88.";
var number = /\b(\d+)\b/g;
var match;
while (match = number.exec(input))
  console.log("Found", match[1], "at", match.index);
// → Found 3 at 14
//   Found 42 at 33
//   Found 88 at 40

Esto hace uso del hecho de que el valor de una expresión de asignación (_=_) es el valor asignado. Por lo tanto, utilizando _match = number.exec(input)_ como condición en la sentencia _while_, realizamos la coincidencia al comienzo de cada iteración, guardamos su resultado en una variable y detenemos el bucle (loop) cuando no se encuentran más coincidencias.

Análisis de un archivo INI

Para concluir el capítulo, veremos un problema que requiere expresiones regulares. Imagina que estamos escribiendo un programa para recolectar automáticamente información sobre nuestros enemigos desde Internet. (En realidad, no escribiremos ese programa aquí, solo la parte que lee el archivo de configuración. Sentimos decepcionarlo.) El archivo de configuración se verá así:

searchengine=http://www.google.com/search?q=$1
spitefulness=9.7

; comments are preceded by a semicolon...
; each section concerns an individual enemy
[larry]
fullname=Larry Doe
type=kindergarten bully
website=http://www.geocities.com/CapeCanaveral/11451

[gargamel]
fullname=Gargamel
type=evil sorcerer
outputdir=/home/marijn/enemies/gargamel

Las reglas exactas para este formato (que en realidad es un formato ampliamente utilizado, generalmente llamado archivo INI) son las siguientes:

Nuestra tarea es convertir una cadena como ésta en una matriz de objetos, cada uno con una propiedad _name_ y una matriz de configuraciones. Necesitaremos un objeto para cada sección y otro para la configuración global en la parte superior.

Dado que el formato tiene que ser procesado línea por línea, dividir el archivo en líneas separadas es un buen comienzo. No obstante, algunos sistemas operativos utilizan no sólo un carácter de nueva línea para separar líneas, sino un carácter de retorno de carro seguido de una nueva línea (_"\r\n"_). Dado que el método _split_ también permite que una expresión regular sea su argumento, podemos dividir en una expresión regular como _/\r?\n/_ para dividir de una manera que permita tanto _"\n"_ como _"\r\n"_ entre líneas.

function parseINI(string) {
  // Start with an object to hold the top-level fields
  var currentSection = {name: null, fields: []};
  var categories = [currentSection];

  string.split(/\r?\n/).forEach(function(line) {
    var match;
    if (/^\s*(;.*)?$/.test(line)) {
      return;
    } else if (match = line.match(/^\[(.*)\]$/)) {
      currentSection = {name: match[1], fields: []};
      categories.push(currentSection);
    } else if (match = line.match(/^(\w+)=(.*)$/)) {
      currentSection.fields.push({name: match[1],
                                  value: match[2]});
    } else {
      throw new Error("Line '" + line + "' is invalid.");
    }
  });

  return categories;
}

Este código pasa por todas las líneas del archivo, actualizando el objeto "currentSection" a medida que avanza. En primer lugar, comprueba si la línea se puede ignorar, utilizando la expresión _/^\s*(;.*)?$/_. ¿Ves cómo funciona? La parte entre los paréntesis coincidirá con los comentarios, y el signo _?_ se asegurará de que también coincida con las líneas que contienen sólo espacios en blanco.

Cuando la línea no es un comentario, el código comprueba si la línea inicia una nueva sección. Si es así, crea un nuevo objeto de sección actual, al que se añadirán los ajustes siguientes.

La última posibilidad significativa es que la línea es un ajuste normal, por lo que nuestro código lo agregará al objeto de sección actual.

Si una línea no coincide con ninguno de estos formularios, la función produce un error.

Tenga en cuenta el uso recurrente de _^_ y _$_ para asegurarse de que la expresión coincide con toda la línea, no sólo parte de ella. Dejando estos resultados en código que funciona sobre todo, pero se comporta extrañamente para algunos tipos de input, lo que puede originar un error difícil de rastrear.

El patrón _if (match = string.match(...))_ es similar al truco de usar una asignación como condición para _while_. A menudo no estaremos seguros de que nuestra llamada a _matchear_ tendrá éxito, por lo que se puede acceder al objeto resultante sólo dentro de una instrucción if que las pruebas de ello. Para no romper la agradable cadena que forma la sentencia _if_, asignamos el resultado de la coincidencia a una variable e inmediatamente utilizamos esa asignación como el _test_ en la sentencia _if_.

Caracteres internacionales

Debido a la implementación simplista inicial de JavaScript y al hecho de que este enfoque se estableció como base del comportamiento estándar, las expresiones regulares de JavaScript son bastante torpes acerca de los caracteres que no aparecen en el idioma inglés. Por ejemplo, en lo que se refiere a las expresiones regulares de JavaScript, un "carácter para texto" (word character) es sólo uno de los 26 caracteres del alfabeto latino (mayúsculas o minúsculas) y, por alguna razón, el carácter underscore (). Caracteres como _é o β, que definitivamente son caracteres de texto, no coincidirán con _\w_ (y coincidirán con _\W_, carácter para no texto (nonword category)).

Por un extraño accidente histórico, _\s_ (espacio en blanco) no tiene este problema y coincide con todos los caracteres que el estándar Unicode considera espacios en blanco, incluyendo cosas como el espacio no separable y el separador de vocales mongol.

Algunas implementaciones de expresiones regulares en otros lenguajes de programación tienen sintaxis para coincidir con categorías de caracteres Unicode específicas, como "todas las letras mayúsculas", "todos los signos de puntuación" o "caracteres de control". Hay planes para agregar soporte para JavaScript de estas categorías, pero por desgracia parece que no en un futuro próximo.

Resumen

Las expresiones regulares son objetos que representan patrones en cadenas. Utilizan su propia sintaxis para expresar estos patrones.

/abc/ Una secuencia de caracteres
/[abc]/ Cualquier carácter en un conjunto de caracteres
/[^abc]/ Cualquier carácter que no este en un conjunto de caracteres
/[0-9]/ Cualqier carácter en un rango de caracteres
/x+/ Una o mas ocurrencias del patron x
/x+?/ Una o mas ocurrencias, no codiciosos
/x*/ Cero o mas ocurrencias
/x?/ Cero o una ocurrencia
/x{2,4}/ Entre dos o cuatro ocurrencias
/(abc)/ Un grupo
/a|b|c/ Cualqiera de estos patrones
/\d/ Cualquier carácter digital
/\w/ Cualquier carácter alfanúmerico (“word character”)
/\s/ Cualquier carácter de espacio en blanco
/./ Cualquier carácter excepto nuevas líneas
/\b/ Limite de palabra
/^/ Inicio de la entrada
/$/ Fin de la entrada

Una expresión regular tiene un método _test_ para comprobar si una cadena dada tiene coincidencias con ella. También tiene un método _exec_ que, cuando se encuentra una coincidencia, devuelve una matriz que contiene todos los grupos coincidentes. Dicha matriz tiene una propiedad de índice (_index_) que indica dónde se inició la coincidencia.

Las cadenas tienen un método _match_ para buscar coincidencia con una expresión regular y un método _search_ para realizar busquedas, que devuelve sólo la posición inicial de la coincidencia. El método _replace_ puede reemplazar las coincidencias (los matcheos) de un patrón con una cadena de reemplazo. Como alternativa puedes crear una función _replace_, y así crear una cadena de reemplazo basada en el texto de coincidencia y los grupos coincidentes.

Las expresiones regulares pueden tener opciones, que se escriben después de la barra de cierre (|). La opción _i_ hace que el matcheo sea insensible a mayusculas y minusculas (case insensitive), mientras que la opción _g_ hace que la expresión sea global, eso genera, entre otras cosas, hace que el método replace reemplace todas las instancias en lugar de sólo la primera.

El constructor _RegExp_ se puede utilizar para crear un valor de expresión regular a partir de una cadena.

Las expresiones regulares son una herramienta muy afilada con un comando torpe. Ello simplifican algunas tareas pero pueden volverse incontrolables cuando se aplican a problemas complejos. Parte de saber cómo usarlos es resistir el impulso de intentar utilizarlas en los casos incorrectos.

Ejercicios

Es casi inevitable que cuando haga estos ejercicios, se confunda y se frustre por el comportamiento inexplicable de alguna expresión regular. A veces, ayuda a introducir su expresión en una herramienta en línea como debuggex.com para ver si su solución corresponde con lo que usted buscaba y experimentar con la forma en que ésta responde a distintas cadenas.

Regexp golf

Code golf es un término usado en un juego que busca expresar un programa determinado en la mínima cantidad de caracteres posible. Del mismo modo, regexp golf es la práctica de escribir una expresión regular tan pequeña como sea posible para que coincida con un patrón dado, y sólo ese patrón.

Para cada uno de los siguientes elementos, escriba una expresión regular que compruebe si alguna de las subcadenas se produce en una cadena. La expresión regular debe coincidir sólo con cadenas que contengan una de las subcadenas descritas. No se preocupe por los límites de palabras a menos que se mencionen explícitamente. Cuando logre que su expresión funcione, vea si puede lograr reducir la expresión.

  1. car and cat

  2. pop and prop

  3. ferret, ferry, y ferrari

  4. Cualquier palabra que termine en ious

  5. Un carácter de espacio en blanco seguido de un punto, coma, dos puntos o punto y coma

  6. Una palabra de más de seis letras

  7. Una palabra sin la letra e

Consulte la tabla del resumen del capítulo para obtener ayuda. Pruebe cada solución con unas cuantas cadenas de prueba.

// Fill in the regular expressions

verify(/.../,
       ["my car", "bad cats"],
       ["camper", "high art"]);

verify(/.../,
       ["pop culture", "mad props"],
       ["plop"]);

verify(/.../,
       ["ferret", "ferry", "ferrari"],
       ["ferrum", "transfer A"]);

verify(/.../,
       ["how delicious", "spacious room"],
       ["ruinous", "consciousness"]);

verify(/.../,
       ["bad punctuation ."],
       ["escape the dot"]);

verify(/.../,
       ["hottentottententen"],
       ["no", "hotten totten tenten"]);

verify(/.../,
       ["red platypus", "wobbling nest"],
       ["earth bed", "learning ape"]);


function verify(regexp, yes, no) {
  // Ignore unfinished exercises
  if (regexp.source == "...") return;
  yes.forEach(function(s) {
    if (!regexp.test(s))
      console.log("Failure to match '" + s + "'");
  });
  no.forEach(function(s) {
    if (regexp.test(s))
      console.log("Unexpected match for '" + s + "'");
  });
}

Estilo de citas

Imagine que ha escrito una historia en Ingles y ha usado comillas simples para marcar partes del diálogo. Ahora desea reemplazar todas las citas de diálogo con comillas dobles, manteniendo las comillas simples que se usan en contracciones.

Piense en un patrón que distinga estos dos tipos de comillado y cree una llamada al método _replace_ que haga la sustitución adecuada.

var text = "'I'm the cook,' he said, 'it's my job.'";
// Change this call.
console.log(text.replace(/A/g, "B"));
// → "I'm the cook," he said, "it's my job."

La solución más obvia es reemplazar las citas con un carácter sin palabras al menos en uno de los lados. Algo como _/\W'|'\W/_, pero también tenga en cuenta el principio y el final de cada línea.

Además, debe asegurarse de que el reemplazo también incluya los caracteres que coincidan con el patrón _\W_ para que no se eliminen. Esto se puede hacer envolviéndolos entre paréntesis e incluyendo sus grupos en la cadena de reemplazo (_$1_, _$2_). Los grupos que no coincidan serán reemplazados por nada.

Numbers again

Una serie de dígitos puede ser igualada por la expresión regular simple _/\d+/_.

Escriba una expresión que coincida sólo con los números válidos para JavaScript. Debe soportar un signo positivo o negativo adicional delante del número, el punto decimal y la notación de exponente _5e-3_ o 1E10- tambien con la opcion de un signo delante del exponente. Observe también que no es necesario que haya dígitos delante o después del punto, pero el número no puede ser solamente un punto, es decir, _.5_ y _5._ son números JavaScript válidos, pero un punto solitario no lo es.Write an expression that matches only JavaScript-style

// Fill in this regular expression.
var number = /^...$/;

// Tests:
["1", "-1", "+15", "1.55", ".5", "5.", "1.3e2", "1E-4",
 "1e+12"].forEach(function(s) {
  if (!number.test(s))
    console.log("Failed to match '" + s + "'");
});
["1a", "+-1", "1.2.3", "1+1", "1e4.5", ".5.", "1f5",
 "."].forEach(function(s) {
  if (number.test(s))
    console.log("Incorrectly accepted '" + s + "'");
});

En primer lugar, no se olvide de la barra invertida delante del punto.

Se puede hacer coincidir el signo opcional delante del número, así como delante del exponente, con _[+\-]?_ o _(\+|-|)_ (más, menos o nada).

La parte más complicada del ejercicio es el problema de coincidir con _"5."_ y _".5"_ sin igualar también _"."_ . Para ello, una buena solución es utilizar el operador _|_ para separar los dos casos: uno o más dígitos opcionalmente seguido por un punto y cero o más dígitos o un punto seguido por uno o más dígitos.

Por último, para que en el caso del carácter e no distinga entre mayúsculas y minúsculas, agregue una opción _i_ a la expresión regular o utilice _[eE]_.