Obtener todas las coincidencias solapadas en expresiones regulares

publicado por: Anonymous

Tengo este string de ejemplo en Javascript, "246126667", y quiero obtener todas las subcadenas que tengan 2 números cualquiera a la izquierda y un 6 al final, es decir, ["246","126","266","666"], y para eso le aplico un match con la expresión regular [0-9]{2}6, y el parámetro g para obtener los resultados, pero no funciona como yo quiero.

_x000D_

_x000D_

console.log(_x000D_
  "246126667".match(/[0-9]{2}6/g)_x000D_
)

_x000D_

_x000D_

_x000D_

El problema es que en vez de devolver los 4 números, solo devuelve ["246","126"]. ¿Es posible resolver esto mediante expresiones regulares? ¿Hay alguna otra manera?

solución

Problema

La expresión que utilizaste ([0-9]{2}6) está muy bien. El tema principal de por qué no coincide con caracteres solapados es porque el motor de expresiones regulares consume los caracteres con los que coincide. Por lo tanto, luego de coincidir con "126" en

246126667
   ^^^

la posición para intentar la siguiente coincidencia será:

246126|667
      ^
     acá

y no encontrará otros 3 dígitos terminados en 6.

Solución

Una inspección positiva (o positive lookahead) intenta una coincidencia pero, si coincide, vuelve a la posición en la que se encontraba antes de intentarlo. Es decir, no consume caracteres. La sintaxis es

(?=expresión)

Entonces, este regex coincidirá con lo que estás buscando

/(?=d{2}6)/g

d es lo mismo que [0-9] en JavaScript.

Pero hay un agregado: como no consume caracteres, coincide con la posición, y no con los 3 caracteres que estás queriendo obtener. Para eso, usamos un grupo (lo rodeamos entre paréntesis) para capturar el texto.

Además, es necesario usar .exec() para luego obtener el texto capturado en match[1], y una condición extra para evitar loops infinitos que se deben a un bug conocido de JavaScript.

Regex

/(?=(d{2}6))/g

Código

_x000D_

_x000D_

const texto = "246126667",_x000D_
      regex = /(?=(d{2}6))/g;_x000D_
let   match;_x000D_
_x000D_
while ((match = regex.exec(texto)) !== null) {_x000D_
    if (match.index === regex.lastIndex) { //evitar loop infinito_x000D_
        regex.lastIndex++;_x000D_
    }_x000D_
    _x000D_
    //imprimir el texto capturado por el grupo 1_x000D_
    console.log(match[1]);_x000D_
}

_x000D_

_x000D_

_x000D_


¿Por qué es necesario evitar un loop infinito?

En la solución, agregué una condición para ver si el inicio de la
coincidencia (match.index) es la misma posición que donde se
iniciará el siguiente intento (regex.lastIndex). En caso de que sea
la misma, se incrementa en 1 la posición.

if (match.index === regex.lastIndex) { //evitar loop infinito
    regex.lastIndex++;
}

Esto se debe a que, como la coincidencia es en realidad de largo
cero
, ya que la aserción no consume caracteres, y el resultado del
método realmente es una cadena vacía (""), la posición desde donde
se intenta el regex no avanza ninguna posición. Por eso, se modifica
manualmente para que no siga intentando coincidir desde la misma posición,
entrando en un loop infinito.

Esta condición es una buena práctica a utilizar en todos los casos en
los que se utilice el modificador /g (global) y un bucle
con RegExp.prototype.exec(). Además, no todos los navegadores se
comportan igual. Incluso en expresiones que no deberían jamás devolver
una cadena vacía, es una buena salvaguarda, y recomiendo utilizar
siempre esta construcción.

JavaScript tiene una de las peores implementaciones de expresiones
regulares dentro de los lenguajes comúnmente utilizados (ver más
info
). Si bien este comportamiento no es un “bug” per se
(RESOLVED INVALID en Bugzilla), utilicé
el término porque es un concepto erróneo en la implementación de
Oniguruma (el motor de RegExp) sobre este comportamiento. Todos
los otros motores de expresiones regulares, luego de coincidir con una
cadena vacía muestran uno de estos dos comportamientos:

Una nota de color es que versiones previas de IE incrementaban automáticamente
a .lastIndex luego de una coincidencia de largo cero con expresiones
globales.
En el artículo An IE lastIndex Bug with Zero-Length Regex Matches
se describe con más detalle, y aunque Steven Levithan lo menciona como “bug”,
en mi opinión era en realidad la forma correcta de realizarlo, más en
concordancia con lo que expone Jan Goyvaerts en
Watch Out for Zero-Length Matches.

Sin embargo, IE terminó dejando a .lastIndex sin incrementar para mostrar
el mismo comportamiento que la mayoría de los navegadores desde IE9
(sólo si se especifica un DOCTYPE de HTML 4.01 o HTML5) o en IE10+
(sin importar el DOCTYPE).

JavaScript tiene un error de concepción en no utilizar una de
estas dos estrategias, rompe la norma del resto de los dialectos
(flavors) de regex, y en ese sentido es un bug.

El problema radica originalmente en el estándar. En ECMA-262 se
define a regexp.lastIndex como “el índice en el String en donde
iniciar el próximo intento de coincidencia
” (en 21.2.6.1) (no
dejen que el nombre los engañe, no es el final de la coincidencia),
y luego:

La diferencia entre ambos métodos es inconsistente. Incluso, se
reafirma en las notas de compatibilidad hacia atrás, pero sólo para
String.prototype.match() y String.prototype.replace():

El comportamiento correcto es que lastIndex debe ser incrementado en 1, sólo si el patrón coincidió con una cadena vacía.
(en D).

Y, si bien, sólo sucede con RegExp.prototype.exec(), este método
es el único que aporta la información completa de una coincidencia.
Por ejemplo, es la única forma de obtener el texto capturado por
grupos en una expresión global.

En el estándar queda claro que la falta de una definición consistente
con el uso de expresiones regulares en cualquier otro lenguaje hace
que sea necesario agregar está condición extra para incrementar el
índice manualmente, algo totalmente ilógico e innecesario. Este IF
es una buena práctica recomendada siempre que se haga un bucle de este
estilo.

Respondido por: Anonymous

Leave a Reply

Your email address will not be published. Required fields are marked *