Imitando Lookbehind en JavaScript

A diferencia de las búsquedas anticipadas, JavaScript no admite la sintaxis de búsqueda posterior de expresiones regulares. Eso es desafortunado, pero no estoy contento con simplemente resignarme a ese hecho. A continuación se presentan tres formas que se me ocurrieron para imitar la búsqueda desde atrás en JavaScript.

Para aquellos que no están familiarizados con el concepto de mirar atrás, son afirmaciones de ancho cero que, como el \b más específico , ^ y $ metacaracteres, en realidad no consumir cualquier cosa:simplemente coinciden con una posición dentro del texto. Este puede ser un concepto muy poderoso. Lea esto primero si necesita más detalles.

Imitando mirar atrás con el método de reemplazo y grupos de captura opcionales

Este primer enfoque no se parece mucho a una mirada retrospectiva real, pero podría ser "suficientemente bueno" en algunos casos simples. Estos son algunos ejemplos:

// Mimic leading, positive lookbehind like replace(/(?<=es)t/g, 'x')
var output = 'testt'.replace(/(es)?t/g, function($0, $1){
	return $1 ? $1 + 'x' : $0;
});
// output: tesxt

// Mimic leading, negative lookbehind like replace(/(?<!es)t/g, 'x')
var output = 'testt'.replace(/(es)?t/g, function($0, $1){
	return $1 ? $0 : 'x';
});
// output: xestx

// Mimic inner, positive lookbehind like replace(/\w(?<=s)t/g, 'x')
var output = 'testt'.replace(/(?:(s)|\w)t/g, function($0, $1){
	return $1 ? 'x' : $0;
});
// output: text

Desafortunadamente, hay muchos casos en los que no se puede imitar la búsqueda posterior con esta construcción. He aquí un ejemplo:

// Trying to mimic positive lookbehind, but this doesn't work
var output = 'ttttt'.replace(/(t)?t/g, function($0, $1){
	return $1 ? $1 + 'x' : $0;
});
// output: txtxt
// desired output: txxxx

El problema es que las expresiones regulares se basan en consumir realmente los caracteres que deberían estar dentro de las afirmaciones de búsqueda posterior de ancho cero, y luego simplemente devolver la coincidencia sin violar (una no operación efectiva) si las referencias inversas contienen o no contienen un valor. Dado que el proceso de coincidencia real aquí no funciona como un verdadero lookbehinds, esto solo funciona en un número limitado de escenarios. Además, solo funciona con el replace método, ya que otros métodos relacionados con expresiones regulares no ofrecen un mecanismo para "deshacer" dinámicamente las coincidencias. Sin embargo, dado que puede ejecutar código arbitrario en la función de reemplazo, ofrece un grado limitado de flexibilidad.

Imitar la mirada hacia atrás a través de la inversión

El siguiente enfoque utiliza la búsqueda anticipada para imitar la búsqueda retrospectiva y se basa en la inversión manual de los datos y la escritura de la expresión regular al revés. También deberá escribir el valor de reemplazo al revés si usa esto con el replace método, voltea el índice de coincidencia si usas esto con el search método, etc. Si eso suena un poco confuso, lo es. Mostraré un ejemplo en un segundo, pero primero necesitamos una forma de invertir nuestra cadena de prueba, ya que JavaScript no proporciona esta capacidad de forma nativa.

String.prototype.reverse = function () {
	return this.split('').reverse().join('');
};

Ahora intentemos sacar esto adelante:

// Mimicking lookbehind like (?<=es)t
var output = 'testt'.reverse().replace(/t(?=se)/g, 'x').reverse();
// output: tesxt

Eso realmente funciona bastante bien, y permite imitar la mirada hacia atrás tanto positiva como negativa. Sin embargo, escribir una expresión regular más compleja con todos los nodos invertidos puede ser un poco confuso, y dado que la búsqueda anticipada se usa para imitar la búsqueda retroactiva, no puede mezclar lo que pretende como búsqueda anticipada real en el mismo patrón.

Tenga en cuenta que invertir una cadena y aplicar expresiones regulares con nodos invertidos puede abrir formas completamente nuevas de abordar un patrón y, en algunos casos, puede hacer que su código sea más rápido, incluso con la sobrecarga de invertir los datos . Tendré que dejar la discusión sobre la eficiencia para otro día, pero antes de pasar al tercer enfoque de mirar hacia atrás e imitar, aquí hay un ejemplo de un nuevo enfoque de patrón hecho posible a través de la inversión.

En mi última publicación, utilicé el siguiente código para agregar comas cada tres dígitos desde la derecha para todos los números que no están precedidos por un punto, una letra o un guión bajo:

String.prototype.commafy = function () {
	return this.replace(/(^|[^\w.])(\d{4,})/g, function($0, $1, $2) {
		return $1 + $2.replace(/\d(?=(?:\d\d\d)+(?!\d))/g, '$&,');
	});
}

Aquí hay una implementación alternativa:

String.prototype.commafy = function() {
	return this.
		reverse().
		replace(/\d\d\d(?=\d)(?!\d*[a-z._])/gi, '$&,').
		reverse();
};

Te dejo el análisis para tu tiempo libre.

Finalmente, llegamos al tercer enfoque de mirar atrás e imitar:

Imitando mirar atrás usando un ciclo while y regexp.lastIndex

Este último enfoque tiene las siguientes ventajas:

  • Es más fácil de usar (no es necesario invertir los datos y los nodos de expresiones regulares).
  • Permite el uso conjunto de lookahead y lookbehind.
  • Te permite automatizar más fácilmente el proceso de imitación.

Sin embargo, la contrapartida es que, para evitar interferir con el retroceso estándar de expresiones regulares, este enfoque solo le permite usar búsquedas posteriores (positivas o negativas) al principio y/o al final de sus expresiones regulares. Afortunadamente, es bastante común querer usar un lookbehind al comienzo de una expresión regular.

Si aún no está familiarizado con el exec método disponible para RegExp objetos, asegúrese de leer al respecto en el Centro de desarrolladores de Mozilla antes de continuar. En particular, mira los ejemplos que usan exec dentro de un while bucle.

Aquí hay una implementación rápida de este enfoque, en el que jugaremos con el mecanismo de avance del motor de expresiones regulares para que funcione como queremos:

var data = 'ttttt',
	regex = /t/g,
	replacement = 'x',
	match,
	lastLastIndex = 0,
	output = '';

regex.x = {
	gRegex: /t/g,
	startLb: {
		regex: /t$/,
		type: true
	}
};

function lookbehind (data, regex, match) {
	return (
		(regex.x.startLb ? (regex.x.startLb.regex.test(data.substring(0, match.index)) === regex.x.startLb.type) : true) &&
		(regex.x.endLb ? (regex.x.endLb.regex.test(data.substring(0, regex.x.gRegex.lastIndex)) === regex.x.endLb.type) : true)
	);
}

while (match = regex.x.gRegex.exec(data)) {
	/* If the match is preceded/not by start lookbehind, and the end of the match is preceded/not by end lookbehind */
	if (lookbehind(data, regex, match)) {
		/* replacement can be a function */
		output += data.substring(lastLastIndex, match.index) + match[0].replace(regex, replacement);
		if(!regex.global){
			lastLastIndex = regex.gRegex.lastIndex;
			break;
		}
	/* If the inner pattern matched, but the leading or trailing lookbehind failed */
	} else {
		output += match[0].charAt(0);
		/* Set the regex to try again one character after the failed position, rather than at the end of the last match */
		regex.x.gRegex.lastIndex = match.index + 1;
	}
	lastLastIndex = regex.x.gRegex.lastIndex;
}
output += data.substring(lastLastIndex);

// output: txxxx

Eso es bastante código, pero es bastante poderoso. Tiene en cuenta el uso de una mirada retrospectiva inicial y final, y permite usar una función para el valor de reemplazo. Además, esto podría convertirse con relativa facilidad en una función que acepte una cadena para la expresión regular utilizando la sintaxis de búsqueda posterior normal (por ejemplo, "(?<=x)x(?<!x) "), luego lo divide en varias partes según las necesidades antes de aplicarlo.

Notas:

  • regex.x.gRegex debe ser una copia exacta de regex , con la diferencia que debe usar el g marcar si regex hace (para que el exec método para interactuar con el while bucle como lo necesitamos).
  • regex.x.startLb.type y regex.x.endLb.type usa true para "positivo" y false para "negativo".
  • regex.x.startLb.regex y regex.x.endLb.regex son los patrones que desea usar para mirar atrás, pero deben contener un $ final . El signo de dólar en este caso no significa fin de los datos , sino al final del segmento de datos con el que se probarán .

Si se pregunta por qué no ha habido ninguna discusión sobre las búsquedas retrospectivas de longitud fija o variable, es porque ninguno de estos enfoques tiene tales limitaciones. Admiten búsqueda completa de longitud variable, que ningún motor de expresiones regulares que conozco que no sean .NET y JGsoft (utilizado por productos como RegexBuddy) es capaz de hacer.

En conclusión, si aprovecha todos los enfoques anteriores, la sintaxis regex lookbehind se puede imitar en JavaScript en la gran mayoría de los casos. Asegúrate de aprovechar el botón de comentarios si tienes comentarios sobre cualquiera de estas cosas.

Actualización 2012-04: Vea mi publicación de blog de seguimiento, JavaScript Regex Lookbehind Redux , donde publiqué una colección de funciones cortas que hacen que sea mucho más fácil simular la mirada hacia atrás líder.