Original article: Regular Expressions (RegEx) in JavaScript – A Handbook for Beginners

Las expresiones regulares, también conocidos como regex, son herramientas poderosas para la coincidencia de patrones y manipulación de texto. Sea que estés validando entrada de usuario, extrayendo datos de cadenas, o realizando tareas de proceso de texto avanzados, entender regex es esencial para los desarrolladores.

Esta guía comprensiva te llevará a través de los fundamentos de las expresiones regulares en JavaScript, incluyendo cómo crearlos y usarlos, sus caracteres especiales, argumentos, y ejemplos prácticos.

Pre-requisitos:

Si bien este tutorial está diseñado para que sea amigable para principantes, tener un entendimiento básico de los fundamentos de JavaScript será de mucha ayuda. Familiaridad con las variables, tipos de datos, funciones, y manipulación de cadenas en JavaScript te ayudarán a captar los fundamentos abarcados.

Tabla de Contenidos:

  1. ¿Qué son los Regex?
    Cómo escribir Patrones de Expresiones Regulares
  2. Cómo usar las Expresiones Regulares en JavaScript
    Métodos de RegEx en JavaScript
    Búsqueda Avanzada con Argumentos
  3. Los Anclas en Regex
    Modo(m) Multilínea de Anclas
    Límites de una Palabra (\b)
  4. Cuantificadores en Regex
    Cuantificadores Ambiciosos
    Cuantificadores No Ambiciosos (Modo Perezoso)
  5. Conjuntos y Rangos en Regex
    Conjuntos
    Rangos
    Negando / Excluyendo Rangos
    Clases de Caracteres Predefinidos
  6. Caracteres Especiales y Escapando n Regex
    Metacaracteres
    Escapando Caracteres Especiales
  7. Grupos en RegEx
    Capturando Grupos
    Grupos de no captura
    Referencias anteriores
    Alternancia de Regex
  8. Afirmaciones de anticipación y de retrospectiva en Regex
    Anticipación (?=)
    Anticipación Negativa (?!)
    Retrospectiva (?<=)
    Retrospectiva Negativa (?<!)
  9. Ejemplos Prácticos y Casos de Uso de Regex
    Verificación de la Seguridad de Contraseña
    Validación de Email
    Formateo de Número de Teléfono
  10. Consejos de RegEx y Buenas Prácticas
  11. Conclusión

¿Qué son los Regex?

Una expresión regular, con frecuencia se abrevia como "regex", es una secuencia de caracteres que define un patrón de búsqueda. Este patrón es usado para encontrar coincidencias dentro de cadenas, ayudarte a identificar texto específico o patrones de caracteres, proveyendo una forma poderosa de buscar, reemplazar, y manipular texto.

En JavaScript, puedes crear expresiones regulares usando una notación literal o el constructor RegExp:

  • Usando una Expresión Regular Literal: Esto involucra encerrar el patrón entre barras ("/").
const re = /patron/;

// ejemplo
const re = /ab+c/;
  • Usando la Función Constructor: El constructor RegExp. Esto permite compilación en tiempo de ejecución de la expresión regular y es útil cuando el patrón puede cambiar.
const re = new RegExp("patron");

// ejemplo
const re = new RegExp("ab+c");

Ambos métodos producen el mismo resultado – es una cuestión de preferencia cuál elijas.

Cómo escribir Patrones de Expresiones Regulares

Un patrón de expresión regular puede consistir de simple caracteres o una combinación de caracteres simples y especiales.

  1. Patrón Simple: Coinciden secuencias de caracteres exactos. Por ejemplo, el patrón /abc/ coincide con la secuencia "abc" en una cadena.
  2. Caracteres Especiales: Mejoran la coincidencia de patrón con capacidades como repetición o coincidir tipos específicos de caracteres, permitiendo coincidencias de patrón más flexibles y poderosas. Por ejemplo, * igual a cero o más ocurrencias del ítem precedente. /ab*c/ coincide con "ac", "abc", "abbc", y así sucesivamente.

Cómo usar las Expresiones Regulares en JavaScript

Puedes usar expresiones regulares con varios métodos disponibles para los objetos RegExp y String en JavaScript. Algunos métodos como test(), exec(), y otros tienen esta sintaxis:

regex.nombremetodo(cadena)

// ejemplo
regex.test(string)

Mientras que otros métodos como match(), replace(), etc. tienen esta sintaxis:

string.nombremetodo(regex)

// ejemplo
string.replace(regex, reemplazo)

Aquí, string es la cadena y regex es un patrón de expresión regular.

Exploremos cómo estos métodos son usados en la práctica.

Métodos de RegEx en JavaScript

El Método test(): verificar si una cadena particular coincide con un patrón específico o una expresión regular. Devuelve true si el patrón se encuentra en la cadena, de otra forma, devuelve false.

let pattern = /hello/;
let str = "hello world";

let result = pattern.test(str);
console.log(result); // Salida: true

El Método `exec()`: busca una coincidencia de un patrón dentro de una cadena. Devuelve un arreglo conteniendo detalles como el texto coincidido, índice de la coincidencia dentro de la cadena, y la cadena de entrada misma. Ejemplo:

let pattern = /world/;
let str = "hello world";

let result = pattern.exec(str);
console.log(result); // Salida: ["world", index: 6, input: "hello world"]

El Método match(): busca ocurrencias de un patrón dentro de una cadena. Devuelve el primer elemento coincidido. Si tiene el argumento global (g) devuelve un arreglo conteniendo todas las coincidencias encontradas, o null si no se encontraron coincidencias.

let str = "The quick brown fox jumps over the lazy dog.";
let matches = str.match(/the/gi);

console.log(matches); // Salida: ["The", "the"]
/the/gi searches for all occurrences of the word "the" in the string, regardless of case. 

El método matchAll(): devuelve un iterador de todos los resultados que coinciden con una expresión regular con una cadena. Cada elemento del iterador es un arreglo que contiene detalles sobre la coincidencia, incluyendo grupos capturados.

let str = "Hello world! This is a test string.";
let regex = /[a-zA-Z]+/g;

let matches = str.matchAll(regex);

for (let match of matches) {
    console.log(match);
}

Este método es útil cuando necesitas información detallada sobre todas las coincidencias encontradas en una cadena.

El Método search(): busca un patrón específico en una cadena. Devuelve el índice de la primer ocurrencia del patrón dentro de la cadena, o -1 si el patrón no se encuentra.

let str = "The quick brown fox jumps over the lazy dog";
let pattern = /brown/;

let result = str.search(pattern);
console.log(result); // Salida: 10

El Método replace(): reemplaza la primer ocurrencia de una patrón específicado en una cadena con otra sub-cadena o valor. Para reemplazar todas las ocurrencias, puedes usar el argumento global (g) en la expresión regular.

let str = "Hello, World!";
let newStr = str.replace(/o/g, "0");

console.log(newStr); // Salida: "Hell0, W0rld!"

El Método replaceAll(): reemplaza todas las ocurrencias de una sub-cadena o patrón especificado con una cadena de reemplazo. Difiere de replace() en el hecho de que reemplaza todas las ocurrencias por defecto, sin la necesidad de un argumento global (g).

let str = "apple,banana,apple,grape";
let newStr = str.replaceAll("apple", "orange");
console.log(newStr); // Salida: "orange,banana,orange,grape"

El método simplifica el proceso de reemplazar todas las ocurrencias de una sub-cadena dentro de una cadena.

El Método split(): aunque es exclusivamente un método RegEx, split() puede aceptar un patrón RegEx como su argumento para dividir una cadena en un arreglo de sub-cadenas basado en los patrones especificados o delimitadores. Por ejemplo:

let str = "apple,banana,grape";
let arr = str.split(/,/);
console.log(arr); // Salida: ["apple", "banana", "grape"]

Estos métodos ofrecen diferentes funcionalidades basados en tus necesidades. Por ejemplo, si solamente necesitas saber si un patrón se encuentra en na cadena, los métodos test() o search() son eficientes. Si requieres más información sobre coincidencias, los métodos exec() o match() son los adecuados.

Búsqueda Avanzada con Argumentos

En JavaScript, las expresiones regulares soportan argumentos de patrón, los cuales son parámetros opcionales que modifican el comportamiento de la coincidencia de patrón.

Profundicemos en dos argumentos comunes: el argumento ignore (i) y el argumento global (g):

El Argumento Ignore (i):

El argumento ignore (i) instruye a la expresión regular que ignore la distinción entre mayúsculas y minúsculas cuando se busca por coincidencias. Por ejemplo:

let re = /hello/i;
let testString = "Hello, World!";
let result = re.test(testString);

console.log(result); // Salida: true

En este caso, la expresión regular /hello/i coincide con la cadena "Hello" a pesar de las diferencias en mayúsculas y minúsculas porque usamos el argumento ignore.

El Argumento Global (g):

El argumento global (g) le permite a la expresión regular que encuentre todas las coincidencias dentro de una cadena, en vez de que se detenga después de la primer coincidencia. Por ejemplo:

let re = /hi/g;
let testString = "hi there, hi again!";
let result = testString.match(re);

console.log(result); // Salida: ["hi", "hi"]

En este ejemplo, la expresión regular /hi/g encuentra las ocurrencias de "hi" en la cadena "hi there, hi again!"`.

Combinando Argumentos

Puedes combinar los argumentos para alcanzar comportamientos de coincidencia específicas. Por ejemplo, usar tanto el argumento ignore (i) como el argumento global (g) juntos permite la coincidencia sin distinción entre mayúsculas y minúsculas mientras que encuentra todas los casos del patrón.

let re = /hi/gi;
let testString = "Hi there, HI again!";
let result = testString.match(re);

console.log(result); // Salida: ["Hi", "HI"]

Aquí, la expresión regular /hi/gi coincide tanto con "Hi" como con "HI" en la cadena "Hi there, HI again!".

El argumento u:

Aunque no es usado comúnmente, el argumento u maneja los caracteres Unicode, especialmente pares sustitutos, correctamente. Los pares sustitutos son usados para representar caracteres fuera del Plano Multilingüe Básico (BMP) en la codificación UTF-16.

Ejemplo: Consideremos una cadena que contiene caracteres emojis e intentemos coincidirlos usando una expresión regular sin y con el argumento u:

// Sin el argumento u
let result1 = 'Smile Please 😊'.match(/[😒😊🙄]/);
console.log(result1); // Salida: ["�"]

// Con el argumento u
let result2 = 'Smile Please 😊'.match(/[😒😊🙄]/u);
console.log(result2); // Salida: ["😊"]

Son el argumento u, el regex falla en coincidir el emoji correctamente porque son representados como pares sustitutos en la codificación UTF-16. Sin embargo, con el argumento u, coincide correctamente el emoji '😊'.

Los Anclas en Regex

Las anclas son caracteres especiales en regex que no representan caracteres en si sino que indican posiciones dentro de una cadena. Hay dos anclas princippales: ^ y $.

El Ancla ^: El ancla ^ coincide con el comienzo del texto. Básicamente, verifica si una cadena comienza con un caracter o patrón.

let str = 'Mountain';
console.log(/^S/.test(str)); // Salida: false

El Ancla $: El ancla $ coincide con el fin del texto. Verifica si una cadena termina con un caracter específico o patrón.

let str = 'Ocean';
console.log(/n$/.test(str)); // Salida: true

Con frecuencias puede que uses ^ y $ juntos para verificar si una cadena coincide completamente un patrón.

let isValid = /^\d\d:\d\d$/.test('10:01');
console.log(isValid); // Salida: true
Este ejemplo verifica si la cadena de entrada coincide con un formato de tiempo como "10:01"
  • En el código de arriba, ^\d\d:\d\d$ se asegura que la cadena contiene exactamente dos dígitos, seguido de dos puntos, y luego dos dígitos más exactamente.

Modo Multílínea de Anclas (^ y $):

Por defecto, los anclas ^ y $ en expresiones regulares operan en modo de una sola línea, lo que significa que coinciden con el comienzo y fin de toda la cadena. Pero en algunos casos, puede que quieras coincidir con el comienzo y final de líneas individuales dentro de una cadena multilínea. Aquí es donde el modo multilínea, indicado con el argumento m, entra en juego.

Ya que el modo de una sola línea es el predeterminado, solamente coincide con el primer dígito "1" al comienzo de la cadena.

let str = `1st line
2nd line
3rd line`;

let re = /^\d/g; // "^\d" coincide con el dígito al comienzo de la cadena
let matches = str.match(re);

console.log(matches); // Salida: ["1"]
  • Modo multilínea (m): /^\d/gm es el patrón regex con el argumento m activado. Al utilizar el argumento m, puedes asegurarte que ^ y $ coincidan con el comienzo y final de líneas individuales dentro de una cadena multilínea, en vez de solo toda la cadena misma.

Como resultado, coincide con "1" de la primer línea, "2" de la segunda línea, y "3" de la tercer línea:

let str = `1st line
2nd line
3rd line`;

let re = /^\d/gm;
let matches = str.match(re);

console.log(matches); // Salida: ["1", "2", "3"]

Esto particularmente es útil cuando se trabaja con texto que contiene múltiples líneas o saltos de líneas.

Límites de una Palabra (\b):

El \b es un caracter especial en expresiones regulares llamada un ancla, justo como ^ y $. Se usa para coincidir una posición en la cadena, donde un caracter de una palabra (tal como un a letra, dígito, o un guión bajo) no le sigue o no es precedido de otro caracter de palabra. Por ejemplo:

  • \bword\b coincide con la palabra "word" en la cadena, pero no sub-cadenas como "wording" o "swordfish".
let pattern = /\bword\b/;
let pattern2 = /word/;
console.log(pattern.test("This is a word.")); // Salida: true
console.log(pattern.test("This is wording.")); // Salida: false (no coincide con "wording")
console.log(pattern2.test("This is wording")); // Salida: True

/word/ coincide con la sub-cadena "word" en cualquier lugar dentro de la cadena. coincide con la cadena "word" en "This is wording." porque no incluye ninguna afirmación de límites de palabras.

Otros ejemplos pueden ser:

  • \b\d+\b coincide con números enteros en la cadena, pero no incluye caracteres no-numéricos adyacentes a los números.
  • ^\bword\b$ coincide con una cadena que consiste solamente de la palabra "word".

Cuantificadores en Regex

En regex, los cuantificadores te permiten especificar la cantidad de caracteres o clases de caracteres para coincidir dentro de una cadena. Son símbolos o caracteres que definen cuántas instancias de un caracter o grupo estás buscando.

Cuenta exacta {n}:

El cuantificador más simple es {n}, el cual especifica una cuenta exacta de caracteres o clases de caracteres que quieres que coincidan. Digamos que tenemos una cadena "Year: 2022" y queremos extraerle el año:

let str = 'Year: 2022';
let re = /\d{4}/; // Coincide con un número de cuatro dígitos ; básicamente es conciso & una mejor forma de escribir \d\d\d\d

let result = str.match(re);

console.log(result); // Salida: ["2022"]

El Rango {n,m}:

El cuantificador rango {n,m} coincide con un caracter o clase de caracteres de n a m veces, inclusivamente. Ejemplo:

let str = "The meeting is scheduled for 10:30 AM and ends at 2 PM";
let re = /\d{2,4}/g; // Coincide con números de 2 a 4 dígitos

let result = str.match(re);
console.log(result); // Salida: [ '10', '30' ]
/\d{2,4}/g coincide con números con 2 a 4 dígitos consecutivos, es decir '10', '30'

{n,} y taquigrafías:

El cuantificador {n,} coincide con un caracter o clase de caracteres al menos n veces. Además, hay notaciones de taquigrafía para cuantificadores comunes. Ejemplo:

let str = 'The price of the item is $2500';
let re = /\d{2,}/g; // Coincide con números con 2 o más dígitos

let result = str.match(re);
console.log(result); // Salida: ["2500"]

Taquigrafías: +, ?, *:

Los cuantificadores +, ?, y * son notaciones de taquigrafía para casos de uso comunes. Usemos la taquigrafía + para coincidir uno o más dígitos en un número de teléfono:

let phone = "+1-(103)-777-0101";
let result = phone.match(/\d+/g); // Coincide con uno o más dígitos

console.log(result); // Salida: ["1", "103", "777", "0101"]
/\d+/g coincide con uno o más dígitos consecutivos en el número de teléfono.

Cuantificadores: Cero o Uno (?):

El cuantificador ? en expresiones regulares significa cero o un caso del caracter o grupo precedente. Es equivalente a {0,1}. Ejemplo:

let str = 'The sky is blue in color, but the ocean is blue in colour';
let result = str.match(/colou?r/g); // Coincide con "color" y "colour"

console.log(result); // Salida: ["color", "colour"]

En este ejemplo, la expresión regular /colou?r/g coincide con "color" y con "colour" en la cadena dada, permitiendo a cero o una ocurrencia de la letra "u".

Cuantificadores: Cero o Más (*):

El cuantificador * en expresiones regulares significa cero o más ocurrencias del caracter o grupo precedente. Es equivalente a {0,}. Ejemplo:

let str = 'Computer science is fascinating, but computational engineering is equally interesting';
let re = /comput\w*/g; // Coincide con "computer" y "computational"

let results = str.match(re);

console.log(results); // Salida: ["computer", "computational"]

Cuantificadores Ambiciosos:

En expresiones regulares, los cuantificadores determinan cuántas veces un elemento en particular puede ocurrir en una coincidencia.

Por defecto, los cuantificadores operan en lo que se llama un modo "ambicioso". Esto significa que intentan coincidir con el elemento precedente tanto como sea posible. Por ejemplo:

let regexp = /".+"/g;
let str = 'The "Boy" and his "Friends" were here';
console.log( str.match(regexp) ); // "Boy" and his "Friends"

En vez de encontrar dos coincidencias separadas ("Boy" y "Friends"), encuentra una coincidencia abarcando ambos ("Boy" and his "Friends").

Entendiendo Búsqueda Ambiciosa

Para entender por qué el intento inicial falló, profundicemos en cómo el motor de las expresiones regulares conduce su búsqueda.

  1. El motor comienza desde el principio de la cadena y encuentra las comillas iniciales.
  2. Procede a coincidir caracteres siguiente las comillas iniciales. Ya que el patrón es ".+", donde . coincide con cualquier caracter y + lo cuantifica para coincidir uno o más veces, el motor continua coincidiendo caracteres hasta que alcanza el final de la cadena.
  3. El motor luego regresa para encontrar las comillas finales " que completaría la coincidencia. Comienza por asumir por el máximo de caracteres posibles coincididos por ".+" y gradualmente reduce el número de caracteres hasta que encuentra una coincidencia válida.
  4. Eventualmente, el motor encuentra una coincidencia abarcando toda la sub-cadena "Boy" and his "Friends".

Este comportamiento de coincidir ambiciosamente la mayoría de caracteres como fuese posible es el modo por defecto de los cuantificadores en las expresiones regulares y no siempre proveen los resultado deseados. Puedes ver esto en el ejemplo donde resulta en una sola coincidencia en vez de múltiples coincidencias separadas para cadenas con comillas.

Cuantificadores No Ambiciosos (Modo Lazy):

Para abarcar las limitaciones del modo ambicioso, las expresiones regulares también soportan un modo perezoso para los cuantificadores. En modo perezoso, los caracteres cuantificados son repetidos el número mínimo de veces necesarias para satisfacer el patrón.

Podemos activa el modo perezoso anexando un signo de pregunta ? después del cuantificador. Por ejemplo, *? o +? denota repetición perezosa.

let regexp = /".+?"/g;
let str = 'The "Boy" and his "Friends" were here';
console.log( str.match(regexp) ); // "Boy" "Friends"

En este ejemplo, el cuantificador perezoso ".+?" se asegura que cada cadena entre comillas coincidan separadamente al minimizar el número de caracteres coincididos entre las comillas abiertas y cerradas.

Rastreemos el proceso de búsqueda paso a paso para entender cómo funciona el cuantificador perezoso:

  • El motor comienza desde el principio de la cadena y encuentra las comillas abiertas.
  • En vez de coincidir ambiciosamente todos los caracteres hasta el final de la cadena, el cuantificador perezoso ".+?" coincide solamente con los caracteres necesarios para satisfacer el patrón. Se detiene tan pronto como encuentre las comillas de cierre ".
  • El motor repite este proceso para cada cadena entre comillas en el texto, resultando en coincidencias separadas para "Boy" y "Friends".

Conjuntos y Rangos en Regex

En las expresiones regulares, usas conjuntos y rangos para coincidir caracteres específicos o un rango de caracteres dentro de un patrón dado.

Conjuntos:

Un conjunto es definido usando corchetes `[...]`. Te permite coincidir cualquier caracter dentro del conjunto. Por ejemplo, `[aeiou]` coincide con cualquiera de las vocales 'a', 'e', 'i'. 'o' o 'u'.

Ejemplo: Supón que tenemos una cadena `'The quick brown fox jumpts over the lazy dog.'`. Para coincidir todas las vocales en esta cadena, podemos usar la expresión regular `/[aeiou]/g`.

let str = 'The quick brown fox jumps over the lazy dog.';
let re = /[aeiou]/g;
let results = str.match(re);

console.log(results); // Salida: ['e', 'u', 'i', 'o', 'o', 'u', 'o', 'e', 'e', 'a', 'o']
Esto coincide todos los casos de vocales en la cadena.
let str = 'The cat chased the rats in the backyard';;
let re = /[cr]at/g;
let results = str.match(re);

console.log(results); // Salida: ['cats', 'rats']

Aquí, el RegEx [cr]at coincide con todas las palabras que comienzan con 'c', o con 'r' y son seguidos de 'at'.

Rangos:

Los rangos te permiten especificar un rango de caracteres dentro de un conjunto. Por ejemplo, [a-z] coincide con cualquier letra minúscula del "a" a la "z", y [0-9] coincide con cualquier dígito del '0' al '9'. Ejemplo:

let str = 'Hello World!';
let re = /[a-z]/g;
let results = str.match(re);

console.log(results); // Salida: ['e', 'l', 'l', 'o', 'o', 'r', 'l', 'd']
Here, regex [a-z] matches all lowercase letters in the string.

Negando / Excluyendo Rangos:

Para excluir ciertos caracteres de un conjunto, puedes usar el símbolo ^ dentro de los corchetes. Ejemplo:

let str = 'The price is $19.99';
let re = /[^0-9]/g;
let results = str.match(re);

console.log(results); // Salida: ['T', 'h', 'e', ' ', 'p', 'r', 'i', 'c', 'e', ' ', 'i', 's', ' ', '$', '.'] 
Here, [^0-9] matches any character that is not a digit in the string

Del mismo modo [^a-z] coincidirá con cualquier caracter que no es una letra minúscula:

let str = 'The price is $19.99';
let results2 = str.match(/[^a-z]/g);

console.log(results2); // Salida: ['T', ' ', ' ', ' ', '$', '1', '9', '.', '9', '9']

Clases de Caracteres Predefinidos:

Algunas clases de caracteres tienen notaciones taquigráficas predefinidas para rangos comunes de caracteres.

Clase \d: Coincide con cualquier caracter dígito, equivalente al rango [0-9]. Ejemplo:

let phone = '+1-(103)-777-0101';
let re = /\d/g;
let numbers = phone.match(re);
let phoneNo = numbers.join('');
console.log(phoneNo); // Salida: 11037770101

Usamos los métodos match() y join() para formatear el número de teléfono. Este enfoque simplifica el proceso de formateo y limpieza de los datos, haciéndolo apropiado para varias aplicaciones de procesamiento de textos.

Del mismo modo, **\s** coincide con un solo caracter de espacio en blanco, incluyendo espacios, tabulaciones, y caracteres de nueva línea, y **\w** coincide con cualquier caracter de palabra (caracter alfanumérico o guión bajo), equivalente al rango [a-zA-Z0-9_].

Combinando estas clases nos permite más coincidencias de patrón flexibles y precisos, permitiendo un rango amplio de tareas de procesamiento de texto. Ejemplo:

let str = 'O2 is oxygen';
let re = /\w\d/g;
console.log(str.match(re)); // Salida: ["O2"]

Estas clases de caracteres predefinidos proveen atajos convenientes para rangos de caracteres usados comúnmente.

Clases inversas, denotados por letras mayúsculas (por ejemplo, \D), coincide con cualquier caracter que no está incluido en la clase de minúsculas correspondiente. Esto provee una manera conveniente para coincidir caracteres fuera de conjuntos específicos, tales como caracteres sin dígitos, caracteres sin espacios, o caracteres sin palabras. Ejemplo:

let phone = '+1-(103)-777-0101';
let re = /\D/g;
console.log(phone.replace(re,'')); // Salida: 11037770101

Caracteres Especiales y Escaping en Regex

Metacaracteres:

Los metacaracteres son caracteres que tienen significados especiales en las Expresiones Regulares y son usados para construir patrones para coincidir texto.

Los Anclas (^ y $), Alternación (|), cuantificadores (+, ?, {}), y clases de caracteres predefinidos (\d, \w, \s) son todos considerados metacaracteres, cada uno con propósitos distintos en la definición de patrón. También unos pocos más, los cuales lo cubriremos ahora.

Punto (.) es un metacaracter con un significado especial. Se usa para coincidir cualquier único excepto caracteres de nueva línea (\n). Funciona como un comodín, permitiendo coincidencias de patrón flexibles cuando el caracter exacto es desconocido o irrelevante.

Si necesitas el punto para coincidir caracteres de nuevas líneas también, puedes usar el argumento /s en JavaScript, el cual permite el modo "línea única", haciendo que el punto coincida con cualquier caracter incluyendo caracteres de nueva línea. Ejemplo:

const regex = /a.b/; 

console.log(regex.test('acb')); // true
console.log(regex.test('aXb')); // true
console.log(regex.test('a\nb')); // false (caracter de nueva línea no coincidido)
console.log(regex.test('a\nb', 's')); // true (con argumento 's', caracter de nueva línea coincidido)
console.log(regex.test('ab')); // false (falta un caracter entre 'a' y 'b')
/a.b/ matches any string that starts with 'a', followed by any single character (except newline), and ends with 'b'

El punto (.) puede ser combinado con otros elementos de regex para formar patrones más complejos. Por ejemplo, /.at/ coincide con cualquier secuencia de tres caracteres terminando en 'at', tales como 'cat', 'bat', o 'hat'.

Escapar Caracteres Especiales:

Escapar caracteres especiales es esencial cuando quieres buscar o coincidir estos caracteres en cadenas de entrada sin invocar sus significados de regex especiales.

Para coincidir con un caracteres especial literalmente en un patrón regex, necesitas escaparlo al pre-cederlo con una barra invertida (). Esto le dice al motor de regex que trate al caracter especial como un caracter especial. Ejemplo:

let str = 'This ^ symbol is called Caret ';
let re = /[\^]/g;
let results = str.match(re);

console.log(results); // Salida: ['^']
Sin \, ^ será interpretado como un símbolo caret literal.

Dato curioso: el / que usamos para escapar metacaracteres es en sí mismo un metacaracter y puede ser escapado con otra barra invertida así //.

Agrupamientos en RegEx

Capturando Grupos:

En JavaScript las expresiones regulares, capturando grupos son usados para extraer partes específicas de una cadena coincidida. Imagina que tienes un path como "resource/id", por ejemplo, "posts/123". Para coincidir este path, puedes usar una expresión regular como /\w+\/\d+/.

  • \w+ coincide con uno o más caracteres de palabras.
  • \/ coincide con la barra inclinada /.
  • \d+ coincide con uno o más dígitos.

Digamos que tienes un path como "posts/123" y quieres capturar la parte id (123). Podemos usar captura de grupos para esto.

Para crear un captura de grupos, puedes encerrar la parte del patrón regex que quieres capturar en paréntesis. Por ejemplo, (\d+) captura uno o más dígitos.

Así es como funciona:

const path = 'posts/123';
const pattern = /\w+\/(\d+)/;

const match = path.match(pattern);
console.log(match);

Salida:

[ 'posts/123', '123', index: 0, input: 'posts/123', groups: undefined ]

Aquí, '123' es capturado por el grupo de captura (\d+).

Usando Múltiples Grupos de captura: Puedes tener múltiples grupos de captura en un patrón regex. Por ejemplo, para capturar el recurso (como "posts") y el id (como "123") del path "posts/123", puedes usar /(\w+)\/(\d+)/.

const path = 'posts/123';
const pattern = /(\w+)\/(\d+)/;

const match = path.match(pattern);
console.log(match);

Salida:

['posts/123', 'posts', '123', index: 0, input: 'posts/123', groups: undefined]

Aquí, 'posts' y '123' son capturados por los dos grupos de captura (\w+) y (\d+) respectivamente.

Grupos de Captura Nombrados te permiten asignar nombres a grupos de captura, lo cual hace más fácil referenciarlos más tarde en tu código.

La sintaxis para grupos de captura nombrados es (?<name>rule), donde:

  • () indica un grupo de captura.
  • ?<name> especifica el nombre del grupo de captura.
  • rule es una regla en el patrón.

Por ejemplo, supón que queremos capturar el recurso (como "posts") y el id (como "123") del path "posts/123" usando grupos de captura nombrados.

const path = 'posts/123';
const pattern = /(?<resource>\w+)\/(?<id>\d+)/;

const match = path.match(pattern);
console.log(match);

Salida:

[
  'posts/123',
  'posts',
  '123',
  index: 0,
  input: 'posts/123',
  groups: [Object: null prototype] { resource: 'posts', id: '10' }
]

Aquí, resource e id son los nombres asignados a los grupos de captura. Podemos accederlos usando match.groups.

Otro Ejemplo: Digamos que tenemos un path como "posts/2022/02/18" y queremos capturar el recurso (como "posts"), año (como "2022"), mes (como "02"), y día (como "18") usando grupos de captura nombrados.

El patrón regex para esto sería:

const path = 'posts/2024/02/22';
const pattern =
  /(?<resource>\w+)\/(?<year>\d{4})\/(?<month>\d{2})\/(?<day>\d{2})/;

const match = path.match(pattern);
console.log(match.groups);

Salida:

{resource: 'posts', year: '2024', month: '02', day: '22'}

Aquí, cada parte del path es capturado usando grupos de captura nombrados, haciéndolo fácil para accederlos por sus nombres respectivos.

Grupos de no-captura:

En expresiones regulares, grupos de no-captura son usados para agrupar partes de un patrón juntos para aplicar cuantificadores o alternación, sin capturar la sub-cadena coincidida.

Para crear un grupo de no-captura, agregas ?: al principio de los paréntesis. Así que /(?:\d)+/ es la versión de no-captura del ejemplo anterior. El ?: le dice al motor de regex que no capture la sub-cadena coincidida.

Veamos la diferencia entre grupos de captura y de no-captura con un ejemplo:

// grupo de captura
const regexWithCapture = /(\d{2})\/(\d{2})\/(\d{4})/;
const matchWithCapture = regexWithCapture.exec('02/26/2024');

console.log(matchWithCapture); // ["02/26/2024", "02", "26", "2024"]
// grupo de no-captura
const regexWithoutCapture = /(?:\d{2})\/(?:\d{2})\/(?:\d{4})/;
const matchWithoutCapture = regexWithoutCapture.exec('02/26/2024');

console.log(matchWithoutCapture); // ["02/26/2024"]

En resumen, grupos de no-captura (?:pattern) se comparte como grupos de captura regulares () en términos de patrones de coincidencia, pero no almacenan el texto coincidido en memoria para su posterior recuperación. Esto los hace útil cuando no necesitas extraer partes específicas del texto coincidido.

Referencias anteriores:

Las referencias anteriores te permiten referirte a grupos capturados previamente dentro de una expresión regular. Imagínatelos como variables que almacenan patrones coincididos.

En JavaScript, la sintaxis para una referencia anterior es \N, donde N es un entero representando el número de grupo de captura.

Por ejemplo, considera una cadena con una palabra duplicada "Lion" y queremos quitar la palabra duplicada para obtener 'Lion is the king':

const s = 'Lion Lion is the King';
  • Primero, coincidimos una palabra usando \w+\s+.
  • Luego, creamos un grupo de captura para capturar la palabra usando (\w+)\s+.
  • Luego, usamos una referencia anterior (\1) para referenciar el primero grupo de captura.
  • Finalmente, reemplazamos la coincidencia entera con el primero grupo de captura usando String.replace().
const pattern = /(\w+)\s+\1/;
const result = s.replace(pattern, '$1');
console.log(result); // Salida: 'Lion is the King'

Alternancia de Regex:

La Alternación de Regex es una característica que te permite coincidir diferentes patrones dentro de una expresión regular. Funciona de forma similar como el operador lógico OR. En regex, usas el símbolo pipe `|` para denotar alternación, donde puedes coincidir A o B.

A | B // Esto significa que puedes coincidir el patrón A o el patrón B.

Ahora, exploremos algunas aplicaciones prácticas de la alternación de regex:

Coincidiendo la Cadena de Tiempo en el Formato hh:mm: Supón que queremos coincidir cadenas de tiempo en el formato hh:mm, donde hh representa horas y mm representa minutos. Una expresión regular para coincidir este formato sería /\d{2}:\d{2}/.

Sin embargo, este patrón básico coincide tiempos inválidos como "99:99". Para asegurarnos que coincidimos tiempos válidos (horas empezando de 00 a 23 y minutos empezando de 00 a 59), necesitamos refinar nuestro regex usando alternación.

Para coincidir horas válidas (00 a 23), podemos usar el siguiente patrón:

  • [01]\d coincide con números de 00 a 19.
  • 2[0-3] coincide con números de 20 a 23.

Así que, el patrón para las horas se vuelve [01]\d|2[0-3].

Para coincidir minutos válidos (00 a 59), podemos usar el patrón [0-5]\d.

Ahora, podemos combinar los patrones de hora y minuto usando alternación para obtener el patrón de regex final:

/([01]\d|2[0-3]):[0-5]\d/g

En este patrón:

  • ([01]\d|2[0-3]) coincide con horas válidas.
  • : coincide con el dos puntos.
  • [0-5]\d coincide con minutos válidos.

Este patrón de regex se asegura que solamente coincidamos cadenas de tiempo válidos en el formato hh:mm. Ejemplo:

const timeString = '07:23 33:71 21:17 25:81';
const pattern = /([01]\d|2[0-3]):[0-5]\d/g;
const matches = timeString.match(pattern);

console.log(matches);

Salida esperada:

['07:23', '21:17']

Anticipación y Retrospectiva en Regex

Anticipación:

La Anticipación en expresiones regulares permiten coincidir un patrón (X) solamente si le sigue otro patrón específico (Y). La sintaxis es X(?=Y), donde:

  • X es el patrón que quieres coincidir.
  • (?=Y) es la afirmación de anticipación indicando que X debería ser seguido por Y.

Ejemplo: Digamos que tenemos una cadena describiendo varias distancias, y queremos identificar números seguidos de las unidades "millas" pero no "kilómetros". Podemos usar anticipación en un patrón de regex:

const dist = "He ran 5 miles, but not 10 kilometers.";

const regex = /\d+(?=\s*miles)/g;

console.log(dist.match(regex)); // Salida: ["5"]

Múltiples Anticipaciones: Es posible tener múltiples anticipaciones en una expresión regular usando la sintaxis X(?=Y)(?=Z). Esto nos permite imponer múltiples condiciones para coincidir.

Ejemplo: Digamos que queremos coincidir cadenas que contengan "foo" y "bar", pero en cualquier orden:

const regex = /(?=.*foo)(?=.*bar)/;

console.log(regex.test("foobar")); // Verdadero
console.log(regex.test("barfoo")); // Verdadero
console.log(regex.test("foo"));    // falso
console.log(regex.test("bar"));    // falso

Anticipación Negativa:

Para negar una anticipación, use una anticipación negativa con la sintaxis (?!Y), donde el motor de regex coincide con X solamente no es seguido de Y.

Ejemplo: Supón que queremos coincidir números pero no si son seguidos de "millas":

const text = "He ran 5 miles, but not 10 kilometers.";

const regex = /\d+(?!\s*miles)/g;

console.log(text.match(regex)); // Salida: ["10"]
(?!\s*miles) is the negative lookahead that ensures the number is not followed by zero or more whitespaces and the word "miles"

Retrospectiva:

Las retrospectivas proveen una forma de coincidir patrones basado en lo que les precede, esencialmente coincidiendo con un elemento si hay otro elemento específico antes.

Ejemplo: Supón que tenemos una cadena que contiene precios, y queremos coincidir los números precedidos por el símbolo de moneda "$" pero precedidos de "€". Podemos usar una retrospectiva en un patrón regex:

const priceString = "The price is $100, but €200.";

const regex = /(?<=\$)\d+/g;

console.log(priceString.match(regex)); // Salida: ["100"]

Explicación: (?<=\$ coincide con un elemento si hay una cadena literal "$" antes. La barra invertida \ se usa para escapar el caracter especial "$", tratándolo como un caracter literal.

Retrospectiva Negativa:

La Retrospectivas Negativas te permiten coincidir con un patrón solamente si no es precedido de un patrón específico. Esto es útil para excluir ciertos patrones de coincidencias basado en lo que les precede.

Ejemplo: Supón que tenemos una cadena que contiene varios precios en diferentes monedas, y queremos que coincidan los números que no son precedidos por el símbolo de moneda "$". Podemos usar la retrospectiva negativa en un patrón regex:

const priceString = "The price is $50, but not €100.";

const regex = /(?<!\$)\b\d+\b/g;

console.log(priceString.match(regex)); // Salida: ["100"]

Explicación: (?<!\$) es la sintaxis de retrospectiva negativa, el cual coincide con el siguiente patrón solamente si no es precedido de la cadena literal "$".

Ejemplos Prácticos y Casos de Uso de Regex

Ahora, exploremos algunos ejemplos prácticos de expresiones regulares en aplicaciones de JavaScript para resolver problemas comunes y realizar tareas de manipulación de texto.

Verificación de la Seguridad de Contraseña:

Puedes usar expresiones regulares para reforzar los requerimientos de la seguridad de contraseña, tales como la longitud mínima y la presencia de caracteres especiales.

function checkPasswordStrength(password) {
    let pattern = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%^&*]).{8,}$/;
    return pattern.test(password);
}

console.log(checkPasswordStrength("Passw0rd!"));    // Output: true
console.log(checkPasswordStrength("weakpassword")); // Output: false
Aquí, el regex se asegura que la contraseña contenga al menos 1 dígito, 1 letra minúscula, 1 letra mayúscula, 1 caracter especial, y es al menos de 8 caracteres de largo.

Esto es lo que este patrón hace:

  • (?=.*\d): Requiere al menos un dígito.
  • (?=.*[a-z]): Requiere al menos una letra minúscula.
  • (?=.*[A-Z]): Requiere al menos una letra mayúscula.
  • (?=.*[!@#$%^&*]): Requiere al menos un caracter especial.
  • .{8,}: Requiere una longitud mínima de 8 caracteres.

Validación de Email:

La Validación de Email es crucial para asegurar la integridad y la seguridad de los datos en aplicaciones web. Con métodos regex, podemos fácilmente implementar mecanismos de validación de email robustas.

function validateEmail(email) {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(email);
}

console.log(validateEmail("example@email.com")); // true
console.log(validateEmail("invalid-email"));      // false

Esto es lo que este patrón hace:

  • ^: Asierta el comienzo de la cadena.
  • [^\s@]+: Coincide con uno o más caracteres que no son espacios en blanco o '@'.
  • @: Coincide con el símbolo '@'.
  • [^\s@]+: Coincide con uno o más caracteres que no son espacios en blanco o '@'.
  • \.: Coincide con el símbolo '.' (escapado porque '.' tiene un significado especial en RegEx).
  • [^\s@]+: Coincide con uno o más caracteres que no son espacios en blanco o '@'.
  • $: Asserts el final de la cadena.

Formateo de Número de Teléfono:

El formateo de números de teléfono mejora la experiencia de usuario y la legibilidad en aplicaciones que involucran entrada y visualización de entrada de números de teléfono.

Al definir un patrón regex que coincide con componentes de números de teléfono, podemos fácilmente formatear número de teléfono en un patrón deseado usando el método replace().

function formatPhoneNumber(phoneNumber) {
    const phoneRegex = /(\d{3})(\d{3})(\d{4})/;
    return phoneNumber.replace(phoneRegex, "($1) $2-$3");
}

const formattedNumber = formatPhoneNumber("9876543210");
console.log(formattedNumber); // (987) 654-3210
This function takes a phone number string as input and returns it formatted in the standard (XXX) XXX-XXXX format.

En el método replace(), $1, $2, y $3 representan los grupos capturados en el patrón RegEx, correspondiendo a los tres conjuntos de dígitos en el número de teléfono.

Consejos y Buenas Prácticas para usar Expresiones Regulares

1. Entender la Sintaxis de la Expresión Regular:

Entender la sintaxis y los metacaracteres de las expresiones regulares para un uso efectivo.

2. Probar las Expresiones Regulares:

Las expresiones regulares a veces pueden comportarse de manera inesperada debido a los patrones complejos o caracteres especiales. Siempre prueba tus expresiones regulares con diferentes cadenas de entrada para asegurarte que se comportan como se espera en varios escenarios.

3. Optimiza el Rendimiento:

Considera optimizar tus expresiones regulares para el rendimiento al simplificar patrones o usar alternativas más eficientes donde fuera posible.

4. Usa Métodos Adheridos:

JavaScript provee métodos adheridos como  String.prototype.match(), String.prototype.replace(), y String.prototype.split() para tareas de manipulación de cadenas comunes. Evalúa si estos métodos pueden cumplir tu tarea sin la necesidad de expresiones regulares.

5. Comenta tus Expresiones Regulares:

Agrega comentarios dentro de tu regex usando la sintaxis (?#comment) para explicar partes de los patrones complejos. Ejemplo:

const regex = /(\d{3})-(\d{3})-(\d{4})\s(?# Coincide con un número de teléfono en el formato de XXX-XXX-XXXX)/;

6. Descompone Patrones Complejos:

Si tu expresión regular se vuelve demasiado complejo de entender o de mantener, considera descomponerlo en partes más pequeñas y más manejables. Usa variables para almacenar componentes individuales del patrón y combínalos según sea necesario.

7. Usa Recursos en línea y sigue practicando:

Hay varios recursos en línea y herramientas disponibles para probar y aprender expresiones regulares. Sitios web como Regex101 y RegExr proveen plataformas interactivas para probar y depurar expresiones regulares. También aprovecha los tutoriales en línea y la documentación para aprender conceptos de regex..

La Documentación Web de MDN tiene una guía útil para las Expresiones Regulares aquí. Y aquí hay una guía de inicio rápido para las expresiones regulares en JavaScript: Tutorial RegExp (en inglés).

Conclusión

Las expresiones regulares son herramientas versátiles para coincidencia y manipulación de patrones en JavaScript.

Al entender sus métodos, característica avanzados, y uso con argumentos, aprovechando los recursos en línea y depurando herramientas, puedes aprender y aplicarlos efectivamente en varios escenarios, desde coincidencias de patrones simples a tareas de procesamiento de texto complejos.