Javascript: lookbehind negativo equivalente?

141

Existe uma maneira de obter o equivalente a um lookbehind negativo em expressões regulares javascript? Preciso combinar uma sequência que não comece com um conjunto específico de caracteres.

Parece que não consigo encontrar um regex que faça isso sem falhar se a parte correspondente for encontrada no início da string. Lookbehinds negativos parecem ser a única resposta, mas o javascript não tem uma.

EDIT: Este é o regex que eu gostaria de trabalhar, mas não:

(?<!([abcdefg]))m

Portanto, ele corresponderia ao 'm' em 'jim' ou 'm', mas não ao 'jam'

Andrew Ensley
fonte
Considere postar a regex como seria com uma aparência negativa; isso pode facilitar a resposta.
Daniel LeCheminant
1
Para aqueles que desejam acompanhar a adoção, consulte a tabela de compatibilidade do ECMAScript 2016+
Wiktor Stribitorew
@ WiktorStribiżew: Look-behinds foram adicionados na especificação de 2018. O Chrome os suporta, mas o Firefox ainda não implementou as especificações .
Lonnie Best
Isso precisa mesmo olhar para trás? Que tal (?:[^abcdefg]|^)(m)? Como"mango".match(/(?:[^abcdefg]|^)(m)/)[1]
slebetman

Respostas:

57

Lookbehind Assertions foi aceito na especificação ECMAScript em 2018.

Lookbehind positivo do uso:

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

Lookbehind negativo de uso:

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

Suporte de plataforma:

Okku
fonte
2
existe algum polyfill?
Killy
1
@Killy não é, tanto quanto eu sei, e eu duvido que sempre será, como a criação de um seria potencialmente muito pouco prático (IE escrever uma implementação completa Regex em JS)
Okku
Que tal usar um plug-in babel, é possível ser compilado no ES5 ou já suportado no ES6?
Stefan J
1
@IlpoOksanen Eu acho que você quer dizer estendendo a implementação RegEx .. que é o que polyfills fazer .... e não há nada de errado em escrever a lógica em JavaScript
neaumusic
1
Do que você está falando? Quase todas as propostas são inspiradas em outros idiomas e sempre preferem combinar sintaxe e semântica de outros idiomas, onde faz sentido no contexto de JS idiomático e compatibilidade retroativa. Acho que afirmei claramente que olhares negativos e positivos foram aceitos nas especificações de 2018 em 2017 e dei links para fontes. Além disso, descrevi em detalhes quais plataformas implementam a referida especificação e qual é o status de outras plataformas - e até a atualizamos desde então. Naturalmente isso não é o último Regexp apresentam vamos ver
Okku
83

Desde 2018, as Lookbehind Assertions fazem parte da especificação de idioma do ECMAScript .

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

Resposta pré-2018

Como o Javascript suporta lookahead negativo , uma maneira de fazer isso é:

  1. inverta a sequência de entrada

  2. combinar com uma regex invertida

  3. reverter e reformatar as correspondências


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]) : 'Ø');
  });

Exemplo 1:

Seguindo a pergunta de @ andrew-ensley:

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

Saídas:

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

Exemplo 2:

Após o comentário @neaumusic (corresponde, max-heightmas não line-height, ao token height):

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

Saídas:

max-height true token: height
line-height false token: Ø
JBE
fonte
36
O problema com esta abordagem é que ele não funciona quando você tem tanto lookahead e lookbehind
kboom
3
por favor você pode mostrar um exemplo de trabalho, dizer que eu quero corresponder max-height, mas não line-heighte eu só quero o jogo para serheight
neaumusic
Não ajuda se a tarefa é substituir dois símbolos idênticos consecutivos (e não mais que 2) que não sejam precedidos por algum símbolo. ''(?!\()irá substituir os apóstrofos no ''(''test'''''''testdo outro lado, deixando, assim, (''test'NNNtestao invés de (''testNNN'test.
usar o seguinte código
60

Vamos supor que você queira encontrar tudo que intnão é precedido por unsigned:

Com suporte para look-behind negativo:

(?<!unsigned )int

Sem suporte para look-behind negativo:

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

Basicamente, a idéia é pegar n caracteres anteriores e excluir a correspondência com uma previsão negativa, mas também corresponder aos casos em que não há n caracteres anteriores. (onde n é o comprimento do look-behind).

Então, o regex em questão:

(?<!([abcdefg]))m

traduziria para:

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

Pode ser necessário brincar com a captura de grupos para encontrar o ponto exato da sequência que lhe interessa ou você deseja substituir uma parte específica por outra.

Kamil Szot
fonte
2
Essa deve ser a resposta correta. Veja: "So it would match the 'm' in 'jim' or 'm', but not 'jam'".replace(/(j(?!([abcdefg])).|^)m/g, "$1[MATCH]") retorna "So it would match the 'm' in 'ji[MATCH]' or 'm', but not 'jam'" É bem simples e funciona!
Asrail 19/08/2015
41

A estratégia do Mijoja funciona para o seu caso específico, mas não em geral:

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

Aqui está um exemplo em que o objetivo é corresponder a um l duplo, mas não se for precedido por "ba". Observe a palavra "balll" - o lookbehind verdadeiro deveria ter suprimido os 2 primeiros ls, mas correspondido ao 2º par. Mas, combinando os 2 primeiros l e ignorando essa correspondência como um falso positivo, o mecanismo regexp continua a partir do final dessa correspondência e ignora todos os caracteres do falso positivo.

Jason S
fonte
5
Ah, você está certo. No entanto, isso é muito mais próximo do que eu estava antes. Eu posso aceitar isso até que algo melhor apareça (como javascript realmente implementando lookbehinds).
Andrew Ensley
33

Usar

newString = string.replace(/([abcdefg])?m/, function($0,$1){ return $1?$0:'m';});
Mijoja
fonte
10
Isso não faz nada: newStringsempre será igual string. Por que tantos votos positivos?
MikeM
@ MikeM: porque o objetivo é simplesmente demonstrar uma técnica de correspondência.
bug
57
@erro. Uma demonstração que não faz nada é um tipo estranho de demonstração. A resposta aparece como se tivesse sido apenas copiada e colada sem qualquer compreensão de como funciona. Assim, a falta de explicação que o acompanha e a incapacidade de demonstrar que algo foi correspondido.
MikeM
2
@ MikeM: a regra do SO é que, se responder à pergunta escrita , está correta. O OP não especificou um caso de uso
bug
7
O conceito está correto, mas sim, não foi demonstrado muito bem. Tente executar este no console JS ... "Jim Jam Momm m".replace(/([abcdefg])?m/g, function($0, $1){ return $1 ? $0 : '[match]'; });. Deve retornar Ji[match] Jam Mo[match][match] [match]. Mas também observe que, como Jason mencionou abaixo, ele pode falhar em certos casos extremos.
Simon East
11

Você pode definir um grupo que não captura, negando seu conjunto de caracteres:

(?:[^a-g])m

... que corresponderia a todos os m NÃO precedidos por qualquer uma dessas letras.

Klemen Slavič
fonte
2
Eu acho que a partida também cobriria o personagem anterior.
28413 Sam
4
^ isso é verdade. Uma classe de personagem representa ... um personagem! Tudo o que seu grupo de não captura está fazendo não está disponibilizando esse valor em um contexto de substituição. Sua expressão não está dizendo "todo M não precedida de qualquer dessas cartas" ele está dizendo "todo m precedido por um personagem que não é qualquer uma dessas cartas"
theflowersoftime
5
Para que a resposta também resolva o problema original (início da sequência), ela também deve incluir uma opção, para que o regex resultante seja (?:[^a-g]|^)m. Consulte regex101.com/r/jL1iW6/2 para obter um exemplo em execução.
Johny Skovdal
Usar lógica nula nem sempre tem o efeito desejado.
precisa saber é o seguinte
2

Foi assim que consegui o str.split(/(?<!^)@/)Node.js. 8 (que não suporta lookbehind):

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

Trabalho? Sim (unicode não testado). Desagradável? Sim.

Fishrock123
fonte
1

seguindo a idéia do Mijoja e tirando dos problemas expostos pelo JasonS, eu tive essa idéia; Eu verifiquei um pouco, mas não tenho certeza de mim mesmo, então uma verificação por alguém mais experiente do que eu em js regex seria ótimo :)

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)

minha saída pessoal:

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

o princípio é chamar checkerem cada ponto da cadeia entre dois caracteres, sempre que essa posição for o ponto inicial de:

--- qualquer substring do tamanho do que não é desejado (aqui 'ba', portanto ..) (se esse tamanho for conhecido; caso contrário, talvez seja mais difícil fazer isso)

--- --- ou menor que isso, se for o começo da string: ^.?

e, depois disso,

--- o que deve ser realmente procurado (aqui 'll').

A cada chamada de checker, haverá um teste para verificar se o valor anterior llnão é o que não queremos ( !== 'ba'); se for esse o caso, chamamos outra função, e terá que ser essa ( doer) que fará as alterações em str, se o objetivo for esse, ou mais genericamente, que entrará os dados necessários para processar manualmente os resultados da digitalização de str.

aqui, alteramos a sequência, de modo que precisamos manter um rastro da diferença de comprimento para compensar os locais dados por replace, todos calculados str, os quais nunca mudam.

Como as seqüências primitivas são imutáveis, poderíamos ter usado a variável strpara armazenar o resultado de toda a operação, mas pensei que o exemplo, já complicado pelas substituições, seria mais claro com outra variável ( str_done).

Eu acho que, em termos de desempenho, deve ser bem duro: todas essas substituições inúteis de '' into '', this str.length-1tempos, mais aqui a substituição manual por doer, o que significa muito fatiamento ... provavelmente neste caso específico acima ser agrupados, cortando a corda apenas uma vez em pedaços ao redor de onde queremos inseri -la [match]e inserindo - .join()a em [match]si mesma.

a outra coisa é que eu não sei como ele lidaria com casos mais complexos, ou seja, valores complexos para o lookback por trás ... o comprimento talvez seja o dado mais problemático a ser obtido.

e, no checkercaso de várias possibilidades de valores indesejados para $ behind, teremos que fazer um teste com mais uma regex (ser armazenado em cache (criado) fora checkeré o melhor, para evitar o mesmo objeto de regex a ser criado a cada pedido checker) para saber se é ou não o que procuramos evitar.

espero ter sido claro; se não, não hesite, tentarei melhor. :)

Homer Simpson
fonte
1

Usando seu caso, se você deseja substituir m por algo, por exemplo, convertê-lo para maiúsculas M, você pode negar o conjunto no grupo de captura.

combinar ([^a-g])m, substitua por$1M

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

([^a-g])corresponderá a qualquer caractere que não esteja ( ^) no a-gintervalo e o armazenará no primeiro grupo de captura, para que você possa acessá-lo com $1.

Assim, encontramos imem jime substituí-lo com iMo que resulta em jiM.

Traxo
fonte
1

Como mencionado anteriormente, o JavaScript permite olhar para trás agora. Em navegadores mais antigos, você ainda precisa de uma solução alternativa.

Aposto que não há como encontrar uma expressão regular sem olhar para trás que produza exatamente o resultado. Tudo o que você pode fazer é trabalhar com grupos. Suponha que você tenha uma regex (?<!Before)Wanted, onde Wantedé a regex que você deseja corresponder e Beforeé a regex que conta o que não deve preceder a correspondência. O melhor que você pode fazer é negar a regex Beforee usá-la NotBefore(Wanted). O resultado desejado é o primeiro grupo $1.

No seu caso, Before=[abcdefg]é fácil negar NotBefore=[^abcdefg]. Então, o regex seria [^abcdefg](m). Se você precisar da posição de Wanted, também deverá agrupar NotBefore, para que o resultado desejado seja o segundo grupo.

Se as correspondências do Beforepadrão tiverem um comprimento fixo n, ou seja, se o padrão não contiver tokens repetitivos, você poderá evitar negar o Beforepadrão e usar a expressão regular (?!Before).{n}(Wanted), mas ainda precisará usar o primeiro grupo ou usar a expressão regular (?!Before)(.{n})(Wanted)e usar o segundo grupo. Neste exemplo, o padrão Beforerealmente tem um comprimento fixo, ou seja, 1; portanto, use o regex (?![abcdefg]).(m)ou (?![abcdefg])(.)(m). Se você estiver interessado em todas as correspondências, adicione a gsinalização, veja meu snippet 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
fonte
0

Isso efetivamente faz

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

Exemplo de pesquisa e substituição

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

Observe que a string look-behind negativa deve ter 1 caractere para que isso funcione.

Curtis Yallop
fonte
1
Não é bem assim. Em "jim", não quero o "i"; apenas o "m". E as "m".match(/[^a-g]m/)crianças nulltambém. Também quero o "m" nesse caso.
Andrew Ensley
-1

/(?![abcdefg])[^abcdefg]m/gi sim, isso é um truque.

Techsin
fonte
5
A verificação (?![abcdefg])é totalmente redundante, pois [^abcdefg]já faz seu trabalho para impedir a correspondência desses caracteres.
Nhhtdh
2
Isso não corresponderá a um 'm' sem caracteres anteriores.
Andrew Ensley