RegExp avanzado en JavaScript: grupos nombrados, lookahead, lookbehind y matchAll

Las expresiones regulares en JavaScript son mucho más potentes de lo que la mayoría usa en el día a día. Los grupos nombrados, el lookahead, el lookbehind y el método matchAll abren posibilidades que simplifican enormemente el parseo de textos complejos como logs, fechas o precios.

Grupos nombrados con (?<nombre>)

En lugar de acceder a los grupos capturados por índice numérico, los grupos nombrados permiten referenciarlos por nombre descriptivo. Esto hace el código mucho más legible y resistente a cambios en la expresión.

const reLog = /(?<fecha>d{4}-d{2}-d{2}) (?<hora>d{2}:d{2}:d{2}) [(?<nivel>INFO|WARN|ERROR)] (?<mensaje>.+)/;

const linea = '2024-03-15 14:32:07 [ERROR] Conexión rechazada por el servidor';
const { groups } = linea.match(reLog);

console.log(groups.fecha);   // "2024-03-15"
console.log(groups.nivel);   // "ERROR"
console.log(groups.mensaje); // "Conexión rechazada por el servidor"

Los grupos nombrados también funcionan en el método replace usando la sintaxis $<nombre>:

const reIso = /(?<anio>d{4})-(?<mes>d{2})-(?<dia>d{2})/;
const fecha = '2024-03-15';
const europea = fecha.replace(reIso, '$<dia>/$<mes>/$<anio>');
console.log(europea); // "15/03/2024"

Lookahead positivo y negativo

El lookahead afirma que lo que sigue al punto actual cumple (o no) una condición, sin consumir esos caracteres. Esto permite hacer condiciones sin incluir el texto de la condición en el match.

// Lookahead positivo: precios seguidos de " EUR"
const reEur = /d+(?:.d{2})?(?= EUR)/g;
const texto = 'Total: 42.99 EUR, descuento: 5 EUR, subtotal: 37.99 EUR';
console.log(texto.match(reEur)); // ["42.99", "5", "37.99"]

// Lookahead negativo: números que NO van seguidos de " USD"
const reNoUsd = /d+(?:.d{2})?(?! USD)/g;
const mezcla = 'Precio: 10.00 USD, IVA: 2.10';
console.log(mezcla.match(reNoUsd)); // ["2.10"] (y subcadenas de "10.00")

Lookbehind positivo y negativo

El lookbehind (disponible desde ES2018) funciona igual que el lookahead pero mirando hacia atrás. Permite capturar valores precedidos por un prefijo concreto sin incluirlo en el resultado.

// Lookbehind positivo: extraer números tras "$"
const reDolar = /(?<=$)d+(?:.d{2})?/g;
const precios = 'Camiseta $19.99, Pantalón $34.50, Envío $0.00';
console.log(precios.match(reDolar)); // ["19.99", "34.50", "0.00"]

// Lookbehind negativo: capturar números que no van precedidos de "#"
const reNoId = /(?<!#)bd{4,}b/g;
const datos = 'Ref #10234, Cantidad 5000, Pedido #88901, Unidades 1200';
console.log(datos.match(reNoId)); // ["5000", "1200"]

matchAll para iterar todos los resultados

String.prototype.matchAll devuelve un iterador de todos los matches con flag g, y cada resultado incluye los grupos capturados completos. Es la alternativa moderna al bucle manual con exec.

const reFecha = /(?<dia>d{2})/(?<mes>d{2})/(?<anio>d{4})/g;
const log = 'Inicio 01/03/2024, revisión 15/03/2024, cierre 31/03/2024';

for (const match of log.matchAll(reFecha)) {
  const { dia, mes, anio } = match.groups;
  console.log(`${anio}-${mes}-${dia}`);
  // "2024-03-01"
  // "2024-03-15"
  // "2024-03-31"
}

// También puedes convertirlo a array
const todas = [...log.matchAll(reFecha)];
console.log(todas.length); // 3

Flag d (ES2022): índices de cada grupo

El flag d añade la propiedad indices al resultado de match y matchAll, con las posiciones de inicio y fin de cada grupo capturado. Útil para resaltado de sintaxis o editores.

const re = /(?<host>w+).(?<dominio>w+)/d;
const url = 'servidor.ejemplo';
const m = url.match(re);

console.log(m.indices.groups.host);    // [0, 8]   "servidor"
console.log(m.indices.groups.dominio); // [9, 16]  "ejemplo"

Flag v (ES2024): conjuntos de caracteres avanzados

El flag v (modo Unicode sets) introduce operaciones de conjuntos dentro de las clases de caracteres: diferencia (--), intersección (&&) y propiedades Unicode extendidas.

// Letras griegas excluyendo vocales griegas
const reConsonantesGriegas = /[p{Script=Greek}--[???????]]/gv;

// Intersección: dígitos ASCII que sean también Math symbols
const reMath = /[p{Math}&&[0-9]]/gv;

// Clase de caracteres literal con secuencias de varios caracteres
const reEmoji = /[q{????|????}]/v;

Caso práctico: parseo de líneas CSV con campos opcionales

Combinar grupos nombrados con matchAll es especialmente potente para parsear formatos estructurados donde algunos campos pueden estar vacíos o tener formato variable:

const reCSV = /(?<nombre>[^,]+),(?<precio>d+(?:.d{2})?),(?<stock>d*),(?<categoria>[^,n]*)/g;

const csv = `Teclado,45.99,12,Periféricos
Monitor,299.00,,Monitores
Ratón,19.50,45,Periféricos`;

for (const { groups } of csv.matchAll(reCSV)) {
  const stock = groups.stock ? parseInt(groups.stock) : 'sin stock';
  console.log(`${groups.nombre} | ${groups.categoria} | stock: ${stock}`);
}

La combinación de grupos nombrados, lookahead/lookbehind y matchAll convierte las expresiones regulares de JavaScript en una herramienta de parseo de texto realmente potente. Los flags d y v añaden capacidades para casos más especializados, aunque el soporte del flag v requiere navegadores modernos (Chrome 112+, Firefox 116+).

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP