Javascript: ¿equivalente de lookbehind negativo?

142

¿Hay alguna manera de lograr el equivalente de una mirada hacia atrás negativa en las expresiones regulares de JavaScript? Necesito hacer coincidir una cadena que no comienza con un conjunto específico de caracteres.

Parece que no puedo encontrar una expresión regular que haga esto sin fallar si la parte coincidente se encuentra al comienzo de la cadena. Las respuestas negativas parecen ser la única respuesta, pero JavaScript no tiene una.

EDITAR: Esta es la expresión regular que me gustaría trabajar, pero no:

(?<!([abcdefg]))m

Por lo tanto, coincidiría con la 'm' en 'jim' o 'm', pero no con 'jam'

Andrew Ensley
fuente
Considere publicar la expresión regular como se vería con una mirada negativa detrás; eso puede hacer que sea más fácil responder.
Daniel LeCheminant
1
Aquellos que quieran hacer un seguimiento de la adopción de lookbehind, etc., consulte la tabla de compatibilidad ECMAScript 2016+
Wiktor Stribiżew
@ WiktorStribiżew: se agregaron retrospectivas en la especificación 2018. Chrome los admite, pero Firefox aún no ha implementado la especificación .
Lonnie Best
¿Esto incluso necesita mirar atrás? ¿Qué hay de (?:[^abcdefg]|^)(m)? Al igual que en"mango".match(/(?:[^abcdefg]|^)(m)/)[1]
slebetman

Respuestas:

58

Las afirmaciones retrospectivas fueron aceptadas en la especificación ECMAScript en 2018.

Uso positivo retrospectivo:

console.log(
  "$9.99  €8.47".match(/(?<=\$)\d+(\.\d*)?/) // Matches "9.99"
);

Uso negativo de lookbehind:

console.log(
  "$9.99  €8.47".match(/(?<!\$)\d+(?:\.\d*)/) // Matches "8.47"
);

Soporte de plataforma:

Okku
fuente
2
¿hay algún polyfill?
Killy
1
@Killy no hay hasta donde yo sé, y dudo que lo haya, ya que crear uno sería potencialmente muy poco práctico (es decir, escribir una implementación completa de Regex en JS)
Okku
¿Qué pasa con el uso de un complemento de babel, es posible compilarlo a ES5 o ya es compatible con ES6?
Stefan J
1
@IlpoOksanen Creo que te refieres a extender la implementación de RegEx ... que es lo que hacen los polyfills ... y no hay nada de malo en escribir la lógica en JavaScript
neaumusic
1
¿De qué estás hablando? Casi todas las propuestas están inspiradas en otros lenguajes y siempre preferirán combinar la sintaxis y la semántica de otros lenguajes donde tenga sentido en el contexto de JS idiomático y compatibilidad con versiones anteriores. Creo que dije con bastante claridad que las miradas positivas y negativas fueron aceptadas en la especificación de 2018 en 2017 y di enlaces a las fuentes. Además, describí en detalle qué plataformas implementan dicha especificación y cuál es el estado de otras plataformas, e incluso la he estado actualizando desde entonces. Naturalmente, esa no es la última característica de Regexp que veremos
Okku
83

Desde 2018, las afirmaciones de Lookbehind son parte de la especificación del lenguaje ECMAScript .

// positive lookbehind
(?<=...)
// negative lookbehind
(?<!...)

Respuesta pre-2018

Como Javascript admite la búsqueda anticipada negativa , una forma de hacerlo es:

  1. invertir la cadena de entrada

  2. coincidir con una expresión regular invertida

  3. revertir y reformatear los partidos


const reverse = s => s.split('').reverse().join('');

const test = (stringToTests, reversedRegexp) => stringToTests
  .map(reverse)
  .forEach((s,i) => {
    const match = reversedRegexp.test(s);
    console.log(stringToTests[i], match, 'token:', match ? reverse(reversedRegexp.exec(s)[0]) : 'Ø');
  });

Ejemplo 1:

Siguiendo la pregunta de @andrew-ensley:

test(['jim', 'm', 'jam'], /m(?!([abcdefg]))/)

Salidas:

jim true token: m
m true token: m
jam false token: Ø

Ejemplo 2

Siguiendo el comentario de @neaumusic (coincide max-heightpero no line-height, siendo el token height):

test(['max-height', 'line-height'], /thgieh(?!(-enil))/)

Salidas:

max-height true token: height
line-height false token: Ø
JBE
fuente
36
El problema con este enfoque es que no funciona cuando se mira hacia adelante y hacia atrás
kboom es el
3
¿podría mostrar un ejemplo de trabajo, decir que quiero coincidir max-heightpero no line-heighty solo quiero que el partido seaheight
neaumusic
No ayuda si la tarea es reemplazar dos símbolos idénticos consecutivos (y no más de 2) que no estén precedidos por algún símbolo. ''(?!\()reemplazará los apóstrofos ''(''test'''''''testdesde el otro extremo, dejando así en (''test'NNNtestlugar de (''testNNN'test.
Wiktor Stribiżew
61

Supongamos que desea encontrar todo lo que intno esté precedido por unsigned:

Con soporte para mirar hacia atrás negativo:

(?<!unsigned )int

Sin soporte para la retrospectiva negativa:

((?!unsigned ).{9}|^.{0,8})int

Básicamente, la idea es tomar n caracteres anteriores y excluir la coincidencia con anticipación negativa, pero también coincide con los casos en los que no hay n caracteres anteriores. (donde n es la longitud de mirar hacia atrás).

Entonces la expresión regular en cuestión:

(?<!([abcdefg]))m

se traduciría a:

((?!([abcdefg])).|^)m

Es posible que deba jugar con grupos de captura para encontrar el lugar exacto de la cadena que le interesa o si desea reemplazar una parte específica con otra.

Kamil Szot
fuente
2
Esta debería ser la respuesta correcta. Ver: "So it would match the 'm' in 'jim' or 'm', but not 'jam'".replace(/(j(?!([abcdefg])).|^)m/g, "$1[MATCH]") vuelve ¡ "So it would match the 'm' in 'ji[MATCH]' or 'm', but not 'jam'" Es bastante simple y funciona!
Asrail
41

La estrategia de Mijoja funciona para su caso específico, pero no en general:

js>newString = "Fall ball bill balll llama".replace(/(ba)?ll/g,
   function($0,$1){ return $1?$0:"[match]";});
Fa[match] ball bi[match] balll [match]ama

Aquí hay un ejemplo donde el objetivo es hacer coincidir un doble-l pero no si está precedido por "ba". Tenga en cuenta la palabra "balll": la verdadera mirada atrás debería haber suprimido los primeros 2 l, pero coincidió con el segundo par. Pero al hacer coincidir los primeros 2 l y luego ignorar esa coincidencia como falso positivo, el motor de expresiones regulares procede desde el final de esa coincidencia e ignora cualquier carácter dentro del falso positivo.

Jason S
fuente
55
Ah, tienes razón. Sin embargo, esto está mucho más cerca de lo que estaba antes. Puedo aceptar esto hasta que aparezca algo mejor (como que JavaScript realmente implemente lookbehind).
Andrew Ensley
33

Utilizar

newString = string.replace(/([abcdefg])?m/, function($0,$1){ return $1?$0:'m';});
Mijoja
fuente
10
Esto no hace nada: newStringsiempre será igual string. ¿Por qué tantos votos a favor?
MikeM
@MikeM: porque el punto es simplemente demostrar una técnica de emparejamiento.
error
57
@insecto. Una demostración que no hace nada es un tipo extraño de demostración. La respuesta aparece como si fuera solo copiar y pegar sin comprender cómo funciona. Por lo tanto, la falta de una explicación que lo acompañe y la incapacidad de demostrar que algo ha sido igualado.
MikeM
2
@MikeM: la regla de SO es que, si responde la pregunta tal como está escrita , es correcta. OP no especificó un caso de uso
error
77
El concepto es correcto, pero sí, no se demostró muy bien. Pruebe a ejecutar esto en la consola JS ... "Jim Jam Momm m".replace(/([abcdefg])?m/g, function($0, $1){ return $1 ? $0 : '[match]'; });. Debería volver Ji[match] Jam Mo[match][match] [match]. Pero también tenga en cuenta que, como Jason menciona a continuación, puede fallar en ciertos casos extremos.
Simon East
11

Puede definir un grupo que no sea de captura negando su conjunto de caracteres:

(?:[^a-g])m

... que coincidiría con todos los m NO precedidos por cualquiera de esas letras.

Klemen Slavič
fuente
2
Creo que el partido en realidad también cubriría el personaje anterior.
Sam
44
^ esto es cierto. Una clase de personaje representa ... un personaje! Todo lo que está haciendo su grupo sin captura no es hacer que ese valor esté disponible en un contexto de reemplazo. Su expresión no dice "cada m NO precedido por ninguna de esas letras" está diciendo "cada m precedido por un carácter que NO es ninguna de esas letras"
theflowersoftime
55
Para que la respuesta también resuelva el problema original (comienzo de la cadena), también debe incluir una opción, por lo que la expresión regular resultante sería (?:[^a-g]|^)m. Consulte regex101.com/r/jL1iW6/2 para ver un ejemplo en ejecución.
Johny Skovdal
El uso de la lógica de vacío no siempre tiene el efecto deseado.
GoldBishop
2

Así es como lo logré str.split(/(?<!^)@/)para Node.js 8 (que no admite mirar hacia atrás):

str.split('').reverse().join('').split(/@(?!$)/).map(s => s.split('').reverse().join('')).reverse()

¿Trabajos? Sí (unicode no probado). ¿Desagradable? Si.

Fishrock123
fuente
1

siguiendo la idea de Mijoja, y aprovechando los problemas expuestos por JasonS, tuve esta idea; Lo revisé un poco, pero no estoy seguro de mí mismo, por lo que una verificación realizada por alguien más experto que yo en js regex sería genial :)

var re = /(?=(..|^.?)(ll))/g
         // matches empty string position
         // whenever this position is followed by
         // a string of length equal or inferior (in case of "^")
         // to "lookbehind" value
         // + actual value we would want to match

,   str = "Fall ball bill balll llama"

,   str_done = str
,   len_difference = 0
,   doer = function (where_in_str, to_replace)
    {
        str_done = str_done.slice(0, where_in_str + len_difference)
        +   "[match]"
        +   str_done.slice(where_in_str + len_difference + to_replace.length)

        len_difference = str_done.length - str.length
            /*  if str smaller:
                    len_difference will be positive
                else will be negative
            */

    }   /*  the actual function that would do whatever we want to do
            with the matches;
            this above is only an example from Jason's */



        /*  function input of .replace(),
            only there to test the value of $behind
            and if negative, call doer() with interesting parameters */
,   checker = function ($match, $behind, $after, $where, $str)
    {
        if ($behind !== "ba")
            doer
            (
                $where + $behind.length
            ,   $after
                /*  one will choose the interesting arguments
                    to give to the doer, it's only an example */
            )
        return $match // empty string anyhow, but well
    }
str.replace(re, checker)
console.log(str_done)

mi salida personal:

Fa[match] ball bi[match] bal[match] [match]ama

El principio es llamar checkeren cada punto de la cadena entre dos caracteres, siempre que esa posición sea el punto de partida de:

--- cualquier subcadena del tamaño de lo que no se desea (aquí 'ba', por lo tanto ..) (si se conoce ese tamaño; de lo contrario, quizás sea más difícil de hacer)

--- --- o más pequeño que eso si es el comienzo de la cadena: ^.?

y, después de esto,

--- lo que se debe buscar realmente (aquí 'll').

En cada llamada de checker, habrá una prueba para verificar si el valor anterior llno es el que no queremos ( !== 'ba'); si ese es el caso, llamamos a otra función, y tendrá que ser esta ( doer) la que hará los cambios en str, si el propósito es este, o más genéricamente, ingresará los datos necesarios para procesar manualmente los resultados del escaneo de str.

aquí cambiamos la cadena por lo que necesitábamos mantener un rastro de la diferencia de longitud para compensar las ubicaciones dadas por replace, todas calculadas str, que en sí mismas nunca cambian.

Como las cadenas primitivas son inmutables, podríamos haber usado la variable strpara almacenar el resultado de toda la operación, pero pensé que el ejemplo, ya complicado por los reemplazos, sería más claro con otra variable ( str_done).

Supongo que, en términos de rendimiento, debe ser bastante duro: todos esos reemplazos inútiles de '' en '', this str.length-1veces, más el reemplazo manual por hacedor, lo que significa una gran cantidad de cortes ... probablemente en este caso específico anterior que podría agrupar, cortando la cuerda solo una vez en pedazos alrededor de donde queremos insertarla [match]e .join()ingiriéndola consigo [match]misma.

La otra cosa es que no sé cómo manejaría casos más complejos, es decir, valores complejos para la falsa mirada atrás ... la longitud es quizás la información más problemática para obtener.

y, en checkercaso de múltiples posibilidades de valores no deseados para $ atrás, tendremos que hacer una prueba con otra expresión regular (lo mejor es almacenar en caché (crear) en el exterior checker, para evitar que se cree el mismo objeto de expresión regular) en cada llamada para checker) saber si es o no lo que buscamos evitar.

espero haber sido claro; si no, no lo dudes, lo intentaré mejor. :)

Homero Simpson
fuente
1

Usando su caso, si desea reemplazar m con algo, por ejemplo, convertirlo a mayúsculas M, puede negar el conjunto en el grupo de captura.

emparejar ([^a-g])m, reemplazar con$1M

"jim jam".replace(/([^a-g])m/g, "$1M")
\\jiM jam

([^a-g])coincidirá con cualquier char not ( ^) dentro del a-grango y lo almacenará en el primer grupo de captura, para que pueda acceder a él con $1.

Así encontramos imen jimy reemplazarlo con iMlo que se traduce en jiM.

Traxo
fuente
1

Como se mencionó anteriormente, JavaScript permite mirar atrás ahora. En navegadores antiguos aún necesita una solución alternativa.

Apuesto a que no hay forma de encontrar una expresión regular sin mirar atrás que ofrezca el resultado exacto. Todo lo que puedes hacer es trabajar con grupos. Supongamos que tiene una expresión regular (?<!Before)Wanted, donde Wantedes la expresión regular que desea hacer coincidir y Beforees la expresión regular que cuenta lo que no debe preceder a la coincidencia. Lo mejor que puede hacer es negar la expresión regular Beforey usar la expresión regular NotBefore(Wanted). El resultado deseado es el primer grupo $1.

En su caso, Before=[abcdefg]que es fácil de negar NotBefore=[^abcdefg]. Así sería la expresión regular [^abcdefg](m). Si necesita la posición de Wanted, también debe agrupar NotBefore, para que el resultado deseado sea el segundo grupo.

Si las coincidencias del Beforepatrón tienen una longitud fija n, es decir, si el patrón no contiene tokens repetitivos, puede evitar negar el Beforepatrón y usar la expresión regular (?!Before).{n}(Wanted), pero aún así tiene que usar el primer grupo o usar la expresión regular (?!Before)(.{n})(Wanted)y usar el segundo grupo. En este ejemplo, el patrón Beforeen realidad tiene una longitud fija, es decir, 1, a fin de utilizar la expresión regular (?![abcdefg]).(m)o (?![abcdefg])(.)(m). Si está interesado en todas las coincidencias, agregue la gbandera, vea mi fragmento de código:

function TestSORegEx() {
  var s = "Donald Trump doesn't like jam, but Homer Simpson does.";
  var reg = /(?![abcdefg])(.{1})(m)/gm;
  var out = "Matches and groups of the regex " + 
            "/(?![abcdefg])(.{1})(m)/gm in \ns = \"" + s + "\"";
  var match = reg.exec(s);
  while(match) {
    var start = match.index + match[1].length;
    out += "\nWhole match: " + match[0] + ", starts at: " + match.index
        +  ". Desired match: " + match[2] + ", starts at: " + start + ".";   
    match = reg.exec(s);
  }
  out += "\nResulting string after statement s.replace(reg, \"$1*$2*\")\n"
         + s.replace(reg, "$1*$2*");
  alert(out);
}
Dietrich Baumgarten
fuente
0

Esto efectivamente lo hace

"jim".match(/[^a-g]m/)
> ["im"]
"jam".match(/[^a-g]m/)
> null

Buscar y reemplazar ejemplo

"jim jam".replace(/([^a-g])m/g, "$1M")
> "jiM jam"

Tenga en cuenta que la cadena de búsqueda negativa debe tener 1 carácter para que esto funcione.

Curtis Yallop
fuente
1
No exactamente. En "jim", no quiero la "i"; solo ellos". Y "m".match(/[^a-g]m/)yeilds nulltambién. Quiero la "m" en ese caso también.
Andrew Ensley
-1

/(?![abcdefg])[^abcdefg]m/gi Sí, esto es un truco.

Techsin
fuente
55
La verificación (?![abcdefg])es totalmente redundante, [^abcdefg]ya que hace su trabajo para evitar que esos caracteres coincidan.
nhahtdh
2
Esto no coincidirá con una 'm' sin caracteres anteriores.
Andrew Ensley