1 de noviembre de 2022

Conjuntos y rangos [...]

Varios caracteres o clases de caracteres entre corchetes […] significa “buscar cualquier carácter entre los dados”.

Conjuntos

Por ejemplo, [eao] significa cualquiera de los 3 caracteres: 'a', 'e', o 'o'.

A esto se le llama conjunto. Los conjuntos se pueden usar en una expresión regular junto con los caracteres normales:

// encontrar [t ó m], y luego "op"
alert( "Mop top".match(/[tm]op/gi) ); // "Mop", "top"

Tenga en cuenta que aunque hay varios caracteres en el conjunto, corresponden exactamente a un carácter en la coincidencia.

Entonces, en el siguiente ejemplo no hay coincidencias:

// encuentra "V", luego [o ó i], luego "la"
alert( "Voila".match(/V[oi]la/) ); // null, sin coincidencias

El patrón busca:

  • V,
  • después una de las letras [oi],
  • después la.

Entonces habría una coincidencia para Vola o Vila.

Rangos

Los corchetes también pueden contener rangos de caracteres.

Por ejemplo, [a-z] es un carácter en el rango de a a z, y [0-5] es un dígito de 0 a 5.

En el ejemplo a continuación, estamos buscando "x" seguido de dos dígitos o letras de A a F:

alert( "Excepción 0xAF".match(/x[0-9A-F][0-9A-F]/g) ); // xAF

Aquí [0-9A-F] tiene dos rangos: busca un carácter que sea un dígito de 0 a 9 o una letra de A a F.

Si también queremos buscar letras minúsculas, podemos agregar el rango a-f: [0-9A-Fa-f]. O se puede agregar la bandera i.

También podemos usar clases de caracteres dentro de los […].

Por ejemplo, si quisiéramos buscar un carácter de palabra \w o un guion -, entonces el conjunto es [\w-].

También es posible combinar varias clases, p.ej.: [\s\d] significa “un carácter de espacio o un dígito”.

Las clases de caracteres son abreviaturas (o atajos) para ciertos conjuntos de caracteres.

Por ejemplo:

  • \d – es lo mismo que [0-9],
  • \w – es lo mismo que [a-zA-Z0-9_],
  • \s – es lo mismo que [\t\n\v\f\r ], además de otros caracteres de espacio raros de unicode.

Ejemplo: multi-idioma \w

Como la clase de caracteres \w es una abreviatura de [a-zA-Z0-9_], no puede coincidir con sinogramas chinos, letras cirílicas, etc.

Podemos escribir un patrón más universal, que busque caracteres de palabra en cualquier idioma. Eso es fácil con las propiedades unicode: [\p{Alpha}\p{M}\p{Nd}\p{Pc}\p{Join_C}].

Decifrémoslo. Similar a \w, estamos creando un conjunto propio que incluye caracteres con las siguientes propiedades unicode:

  • Alfabético (Alpha) – para letras,
  • Marca (M) – para acentos,
  • Numero_Decimal (Nd) – para dígitos,
  • Conector_Puntuación (Pc) – para guion bajo '_' y caracteres similares,
  • Control_Unión (Join_C) – dos códigos especiales 200c and 200d, utilizado en ligaduras, p.ej. en árabe.

Un ejemplo de uso:

let regexp = /[\p{Alpha}\p{M}\p{Nd}\p{Pc}\p{Join_C}]/gu;

let str = `Hola 你好 12`;

// encuentra todas las letras y dígitos:
alert( str.match(regexp) ); // H,o,l,a,你,好,1,2

Por supuesto, podemos editar este patrón: agregar propiedades unicode o eliminarlas. Las propiedades Unicode se cubren con más detalle en el artículo Unicode: bandera "u" y clase \p{...}.

Las propiedades Unicode no son soportadas por IE

Las propiedades Unicode p{…} no se implementaron en IE. Si realmente las necesitamos, podemos usar la biblioteca XRegExp.

O simplemente usa rangos de caracteres en el idioma de tu interés, p.ej. [а-я] para letras cirílicas.

Excluyendo rangos

Además de los rangos normales, hay rangos “excluyentes” que se parecen a [^…].

Están denotados por un carácter caret ^ al inicio y coinciden con cualquier carácter excepto los dados.

Por ejemplo:

  • [^aeyo] – cualquier carácter excepto 'a', 'e', 'y' u 'o'.
  • [^0-9] – cualquier carácter excepto un dígito, igual que \D.
  • [^\s] – cualquiere carácter sin espacio, igual que \S.

El siguiente ejemplo busca cualquier carácter, excepto letras, dígitos y espacios:

alert( "alice15@gmail.com".match(/[^\d\sA-Z]/gi) ); // @ y .

Escapando dentro de corchetes […]

Por lo general, cuando queremos encontrar exactamente un carácter especial, necesitamos escaparlo con \.. Y si necesitamos una barra invertida, entonces usamos \\, y así sucesivamente.

Entre corchetes podemos usar la gran mayoría de caracteres especiales sin escaparlos:

  • Los símbolos . + ( ) nunca necesitan escape.
  • Un guion - no se escapa al principio ni al final (donde no define un rango).
  • Un carácter caret ^ solo se escapa al principio (donde significa exclusión).
  • El corchete de cierre ] siempre se escapa (si se necesita buscarlo).

En otras palabras, todos los caracteres especiales están permitidos sin escapar, excepto cuando significan algo entre corchetes.

Un punto . dentro de corchetes significa solo un punto. El patrón [.,] Buscaría uno de los caracteres: un punto o una coma.

En el siguiente ejemplo, la expresión regular [-().^+] busca uno de los caracteres -().^+:

// no es necesario escaparlos
let regexp = /[-().^+]/g;

alert( "1 + 2 - 3".match(regexp) ); // Coincide +, -

…Pero si decides escaparlos “por si acaso”, no habría daño:

// Todo escapado
let regexp = /[\-\(\)\.\^\+]/g;

alert( "1 + 2 - 3".match(regexp) ); // funciona también: +, -

Rangos y la bandera (flag) “u”

Si hay pares sustitutos en el conjunto, se requiere la flag u para que funcionen correctamente.

Por ejemplo, busquemos [𝒳𝒴] en la cadena 𝒳:

alert( '𝒳'.match(/[𝒳𝒴]/) ); // muestra un carácter extraño, como [?]
// (la búsqueda se realizó incorrectamente, se devolvió medio carácter)

El resultado es incorrecto porque, por defecto, las expresiones regulares “no saben” sobre pares sustitutos.

El motor de expresión regular piensa que la cadena [𝒳𝒴] no son dos, sino cuatro caracteres:

  1. mitad izquierda de 𝒳 (1),
  2. mitad derecha de 𝒳 (2),
  3. mitad izquierda de 𝒴 (3),
  4. mitad derecha de 𝒴 (4).

Sus códigos se pueden mostrar ejecutando:

for(let i = 0; i < '𝒳𝒴'.length; i++) {
  alert('𝒳𝒴'.charCodeAt(i)); // 55349, 56499, 55349, 56500
};

Entonces, el ejemplo anterior encuentra y muestra la mitad izquierda de 𝒳.

Si agregamos la flag u, entonces el comportamiento será correcto:

alert( '𝒳'.match(/[𝒳𝒴]/u) ); // 𝒳

Ocurre una situación similar cuando se busca un rango, como[𝒳-𝒴].

Si olvidamos agregar la flag u, habrá un error:

'𝒳'.match(/[𝒳-𝒴]/); // Error: Expresión regular inválida

La razón es que sin la bandera u los pares sustitutos se perciben como dos caracteres, por lo que [𝒳-𝒴] se interpreta como [<55349><56499>-<55349><56500>] (cada par sustituto se reemplaza con sus códigos). Ahora es fácil ver que el rango 56499-55349 es inválido: su código de inicio 56499 es mayor que el último 55349. Esa es la razón formal del error.

Con la bandera u el patrón funciona correctamente:

// buscar caracteres desde  𝒳  a 𝒵
alert( '𝒴'.match(/[𝒳-𝒵]/u) ); // 𝒴

Tareas

Tenemos una regexp /Java[^script]/.

¿Coincide con algo en la cadena Java? ¿Y en la cadena JavaScript?

Respuestas: no, si.

  • En el script Java no coincide con nada, porque [^script] significa “cualquier carácter excepto los dados”. Entonces, la expresión regular busca "Java" seguido de uno de esos símbolos, pero hay un final de cadena, sin símbolos posteriores.

    alert( "Java".match(/Java[^script]/) ); // null
  • Sí, porque la sección [^script] en parte coincide con el carácter "S". No está en script. Como el regexp distingue entre mayúsculas y minúsculas (sin flag i), procesa a "S" como un carácter diferente de "s".

    alert( "JavaScript".match(/Java[^script]/) ); // "JavaS"

La hora puede estar en el formato horas:minutos u horas-minutos. Tanto las horas como los minutos tienen 2 dígitos: 09:00 ó 21-30.

Escribe una regexp que encuentre la hora:

let regexp = /tu regexp/g;
alert( "El desayuno es a las 09:00. La cena es a las 21-30".match(regexp) ); // 09:00, 21-30

En esta tarea asumimos que el tiempo siempre es correcto, no hay necesidad de filtrar cadenas malas como “45:67”. Más tarde nos ocuparemos de eso también.

Respuesta: \d\d[-:]\d\d.

let regexp = /\d\d[-:]\d\d/g;
alert( "El desayuno es a las 09:00. La cena es a las 21-30".match(regexp) ); // 09:00, 21-30

Tenga en cuenta que el guión '-' tiene un significado especial entre corchetes, pero solo entre otros caracteres, no al principio o al final, por lo que no necesitamos escaparlo.

Mapa del Tutorial