learning-front

Nivel 3 · JavaScript moderno y asíncrono

Expresiones regulares

Una expresión regular es un patrón que describes una vez y aplicas a cualquier texto: para validar un id, extraer partes de un string o reemplazar todas las apariciones de una palabra. Aprenderás a leer y escribir patrones básicos, a usar las flags g e i, y a sacar partido de los cuatro métodos que los usan: test, match (con grupos nombrados), replace y matchAll.

Ya sabes trabajar con datos, asincronía y errores. Lo que falta para moverte con soltura en cualquier proyecto es conocer las herramientas que vienen con el lenguaje y que aparecen una y otra vez. Las expresiones regulares son una de ellas: un patrón que describes una vez y aplicas a cualquier texto.

¿Qué es una expresión regular?#

Una expresión regular (o regex, del inglés regular expression) es una forma de describir un patrón de texto. En lugar de decir “este string concreto”, dices “un string que empieza por OW-, luego lleva letras mayúsculas, un guión y exactamente tres dígitos”. Cualquier string que encaje con esa descripción pasa la validación.

En JavaScript, una regex se escribe entre barras:

javascript
// el patrón entre barras: un literal es literal, un metacarácter describe una clase
/^OW-[A-Z]+-\d{3}$/

No tienes que memorizar los metacaracteres: aprenderás a leerlos poco a poco y consultas la referencia cuando la necesitas. Lo que sí importa hoy es entender la idea: describes el molde una vez y el motor comprueba si un texto encaja.

Los metacaracteres básicos#

Cuando escribes /OW-DPS-017/ en una regex, cada letra se interpreta al pie de la letra. Pero en cuanto usas metacaracteres, ya no describes un carácter concreto sino una clase de caracteres:

javascript
// \d  →  cualquier dígito del 0 al 9
// \w  →  cualquier letra, dígito o guión bajo
// .   →  cualquier carácter (excepto salto de línea)
// [A-Z]  →  cualquier letra mayúscula
// [a-z]  →  cualquier letra minúscula
// +   →  uno o más del elemento anterior
// ?   →  cero o uno (el elemento anterior es opcional)
// {3} →  exactamente 3 repeticiones del elemento anterior
// ^   →  inicio del string
// $   →  fin del string

No intentes memorizar esto ahora: es una referencia a la que vuelves. Hoy basta con saber que existen y aplicar test y match con los ejemplos de abajo.

Las flags: g e i#

Las flags modifican el comportamiento de la regex y se escriben después de la segunda barra:

javascript
// i = case-insensitive: ignora si las letras son mayúsculas o minúsculas
/tracer/i   // casa con "tracer", "Tracer", "TRACER"...

// g = global: actúa sobre TODAS las coincidencias, no solo la primera
/tracer/g   // en replace y matchAll, reemplaza o recorre todas las apariciones

// se combinan: primero g, luego i (o al revés: el orden no importa)
/tracer/gi

regex.test(texto): ¿encaja sí o no?#

test es la pregunta de sí/no: devuelve un booleano según si el patrón aparece en el texto. Es lo que usas para validar:

javascript
// el patrón de un id válido
const patronId = /^OW-[A-Z]+-\d{3}$/;

// ^ fuerza que el patrón empiece al inicio del string
// [A-Z]+ uno o más letras mayúsculas (el rol)
// \d{3} exactamente tres dígitos
// $ fuerza que el patrón termine al final del string
// true: encaja del principio al final
console.log(patronId.test("OW-DPS-017"));
// false: minúsculas y un solo dígito
console.log(patronId.test("ow-tank-9"));

texto.match(regex): extraer lo que casa#

match va más allá de sí/no: devuelve la parte del texto que encajó. Si el patrón no casa, devuelve null (no un array vacío). Comprueba siempre el resultado antes de usarlo:

javascript
const id = "OW-DPS-017";
// match devuelve un array: el primer elemento es lo que casó en total
const resultado = id.match(/^OW-[A-Z]+-\d{3}$/);
// si no casa, resultado es null
if (resultado) {
  // "OW-DPS-017" (el match completo)
  console.log(resultado[0]);
}

Con grupos nombrados ((?<nombre>...)), match también te da las partes capturadas en match.groups:

javascript
// (?<rol>...) etiqueta el grupo "letras mayúsculas" como "rol"
// (?<num>...) etiqueta el grupo "tres dígitos" como "num"
const m = "OW-DPS-017".match(/^OW-(?<rol>[A-Z]+)-(?<num>\d{3})$/);
if (m) {
  // "DPS": lo que capturó el grupo nombrado "rol"
  console.log(m.groups.rol);
  // "017": lo que capturó el grupo nombrado "num"
  console.log(m.groups.num);
}

Los grupos nombrados evitan depender de posiciones numéricas (m[1], m[2]): el código se lee solo. Cuando la regex cambia, los nombres siguen siendo válidos.

¿Qué pasa cuando no casa?

javascript
// null, no un array vacío
const malo = "ow-tank-9".match(/^OW-[A-Z]+-\d{3}$/);
// null
console.log(malo);

texto.replace(regex, reemplazo): cambiar lo que casa#

replace sustituye lo que encaja con el patrón. Sin flag g, solo cambia la primera aparición; con g, todas. Con i, sin importar mayúsculas:

javascript
// Sin flag g, replace solo cambia la PRIMERA coincidencia. Con g, todas.
const frase = "tracer y TRACER juegan";
// g = todas las apariciones, i = ignora mayúsculas
// "Tracer y Tracer juegan"
console.log(frase.replace(/tracer/gi, "Tracer"));

texto.matchAll(regex): todas las coincidencias#

matchAll devuelve un iterador con todas las coincidencias. Necesita la flag g; sin ella, lanza un error. Como devuelve un iterador y no un array, hay que desplegarlo con [...] o Array.from para recorrerlo o mapearlo:

javascript
// La flag g es OBLIGATORIA con matchAll
const texto = "picks: Genji, Mercy, Ana";
// [A-Z][a-z]+  →  una mayúscula seguida de minúsculas (un nombre propio)
// matchAll devuelve un iterador: hay que desplegarlo con [...] antes de map
const nombres = [...texto.matchAll(/[A-Z][a-z]+/g)].map((x) => x[0]);
// ["Genji", "Mercy", "Ana"]
console.log(nombres);

matchAll con grupos nombrados es especialmente útil: cada coincidencia lleva su .groups, igual que en match. Así puedes recorrer un texto entero extrayendo partes estructuradas de cada aparición.

Pruébalo tú#

Mira la consola: la regex valida el id, los grupos nombrados extraen rol y número, replace corrige el nombre en todo el texto, y matchAll recoge todos los nombres propios. Cambia el id en la línea de test, prueba un id inválido, o añade un nombre nuevo al texto de matchAll. Pulsa Ejecutar (o Ctrl+Enter) para ver los resultados.

Comprueba lo que sabes#

Pregunta 1 de 5

¿Qué devuelve `regex.test(texto)`?

Tu turno#

Valida los ids del roster. Uno viene mal escrito: márcalo. En el nivel Excelente, usa grupos nombrados para extraer el rol y el número y muéstralos en la tabla. Cuando lo tengas, despliega las soluciones y observa cómo el nivel Excelente encapsula la regex en una función pura y usa matchAll para recorrer todos los ids de una sola pasada.

Ejercicio · en esta página

Valida y extrae los ids del roster

Recibes el roster en bruto: solo ids y nombres (uno de los ids está mal escrito). Monta una tabla que valide cada id y, en el nivel Excelente, extraiga el rol y el número para mostrarlos junto al id.

Paso 1: Que funcione

  • Muestra una fila por héroe con su id, nombre y estado.
  • Marcas el id que no cumple el patrón.
  • Vale validar el id a mano con comprobaciones de string (sin regex).
Ver soluciones
// ════════════════════════════════════════════════════════════════════════════
// NIVEL OK — "funciona"
//
// Valida el id a mano: comprueba que empiece por "OW-", que tenga la longitud
// esperada y que el último trozo sean tres caracteres. Funciona, pero...
//   - Las comprobaciones de string sueltas son verbosas y fáciles de equivocar:
//     basta un caso que no hayas previsto (id con guión extra, letras con tilde)
//     para que un id inválido pase o uno válido falle.
//   - No extrae nada del id: si luego necesitas el rol o el número, tienes que
//     escribir más código de corte de strings encima de esto.
//   - Es justo el trabajo que una expresión regular ya hace por ti en una línea.
// ════════════════════════════════════════════════════════════════════════════

// Copia local del roster para que esta solución sea autocontenida.
const EQUIPO = [
  { id: "OW-DPS-017", nombre: "Tracer" },
  { id: "OW-SUP-004", nombre: "Mercy" },
  { id: "ow-tank-9", nombre: "Reinhardt" },
  { id: "OW-DPS-048", nombre: "Genji" },
  { id: "OW-SUP-021", nombre: "Ana" },
];

// Comprueba si el id es válido a mano, sin regex.
// Un id válido: "OW-", un bloque de letras, "-", exactamente tres dígitos.
function esIdValido(id) {
  // si no empieza por "OW-", descartamos
  if (id.slice(0, 3) !== "OW-") return false;
  // separamos en trozos por "-"
  var trozos = id.split("-");
  // debe tener exactamente tres trozos: ["OW", "<ROL>", "<NUM>"]
  if (trozos.length !== 3) return false;
  // el primer trozo debe ser "OW"
  if (trozos[0] !== "OW") return false;
  // el segundo trozo debe ser al menos una letra (comprobamos solo que no esté vacío)
  if (trozos[1].length === 0) return false;
  // el tercer trozo debe ser exactamente 3 caracteres numéricos
  if (trozos[2].length !== 3) return false;
  // comprobamos que los tres caracteres sean dígitos del 0 al 9
  var num = trozos[2];
  if (num[0] < "0" || num[0] > "9") return false;
  if (num[1] < "0" || num[1] > "9") return false;
  if (num[2] < "0" || num[2] > "9") return false;
  return true;
}

var lineas = EQUIPO.map(function (heroe) {
  // comprobamos el id con la función manual
  var valido = esIdValido(heroe.id);
  // el estado dice "Válido" o avisa del problema
  var estado = valido ? "Válido" : "Id no válido";
  return heroe.id + " | " + heroe.nombre + " | " + estado;
});

function mostrar(lineas) {
  console.log("=== Roster del equipo ===");
  for (var i = 0; i < lineas.length; i++) {
    console.log(lineas[i]);
  }
}

mostrar(lineas);

Por qué este nivel

  • Validar el id a mano con comprobaciones de string sueltas (longitud, rangos de caracteres, guiones) es verboso y fácil de equivocar: basta un caso no previsto para que un id inválido pase o uno válido falle.
  • No extrae nada del id: si luego necesitas el rol o el número, hay que escribir más código de corte de strings encima de esto.
  • Es justo el trabajo que una expresión regular ya hace por ti en una línea.