Como encontrar índices de todas as ocorrências de uma string em outra em JavaScript?


105

Estou tentando encontrar as posições de todas as ocorrências de uma string em outra string, sem distinção entre maiúsculas e minúsculas.

Por exemplo, dada a string:

Aprendi a tocar Ukulele no Líbano.

e a string de pesquisa le, desejo obter a matriz:

[2, 25, 27, 33]

Ambas as strings serão variáveis ​​- ou seja, não posso codificar seus valores.

Achei que fosse uma tarefa fácil para expressões regulares, mas depois de lutar por um tempo para encontrar uma que funcionasse, não tive sorte.

Achei esse exemplo de como fazer isso usando .indexOf(), mas com certeza tem que haver uma maneira mais concisa de fazer isso?

Respostas:


165
var str = "I learned to play the Ukulele in Lebanon."
var regex = /le/gi, result, indices = [];
while ( (result = regex.exec(str)) ) {
    indices.push(result.index);
}

ATUALIZAR

Não consegui identificar na pergunta original que a string de pesquisa precisa ser uma variável. Escrevi outra versão para lidar com esse caso que usa indexOf, então você está de volta ao ponto de partida. Conforme apontado por Wrikken nos comentários, para fazer isso no caso geral com expressões regulares, você precisaria escapar caracteres regex especiais, ponto em que acho que a solução regex se torna mais uma dor de cabeça do que vale a pena.

function getIndicesOf(searchStr, str, caseSensitive) {
    var searchStrLen = searchStr.length;
    if (searchStrLen == 0) {
        return [];
    }
    var startIndex = 0, index, indices = [];
    if (!caseSensitive) {
        str = str.toLowerCase();
        searchStr = searchStr.toLowerCase();
    }
    while ((index = str.indexOf(searchStr, startIndex)) > -1) {
        indices.push(index);
        startIndex = index + searchStrLen;
    }
    return indices;
}

var indices = getIndicesOf("le", "I learned to play the Ukulele in Lebanon.");

document.getElementById("output").innerHTML = indices + "";
<div id="output"></div>


2
Como seria leuma string variável aqui? Mesmo quando usando new Regexp(str);o perigo de caracteres especiais está à espreita, procurando $2.50por exemplo. Algo como regex = new Regexp(dynamicstring.replace(/([\\.+*?\\[^\\]$(){}=!<>|:])/g, '\\$1'));seria IMHO mais perto. Não tenho certeza se js tem um mecanismo de escape regex integrado.
Wrikken

new RegExp(searchStr)seria o caminho, e sim, no caso geral você teria que escapar de caracteres especiais. Realmente não vale a pena fazer, a menos que você precise desse nível de generalidade.
Tim Down

1
Ótima resposta e muito útil. Muito obrigado, Tim!
Bungle

1
Se a string de pesquisa for uma string vazia, você obtém um loop infinito ... faria uma verificação para ela.
HelpMeStackOverflowMyOnlyHope

2
Suponha searchStr=aaae isso str=aaaaaa. Então, em vez de encontrar 4 ocorrências, seu código encontrará apenas 2, porque você está pulando searchStr.lengthno loop.
blazs

18

Aqui está a versão gratuita do regex:

function indexes(source, find) {
  if (!source) {
    return [];
  }
  // if find is empty string return all indexes.
  if (!find) {
    // or shorter arrow function:
    // return source.split('').map((_,i) => i);
    return source.split('').map(function(_, i) { return i; });
  }
  var result = [];
  for (i = 0; i < source.length; ++i) {
    // If you want to search case insensitive use 
    // if (source.substring(i, i + find.length).toLowerCase() == find) {
    if (source.substring(i, i + find.length) == find) {
      result.push(i);
    }
  }
  return result;
}

indexes("I learned to play the Ukulele in Lebanon.", "le")

EDITAR : e se você quiser combinar strings como 'aaaa' e 'aa' para encontrar [0, 2], use esta versão:

function indexes(source, find) {
  if (!source) {
    return [];
  }
  if (!find) {
      return source.split('').map(function(_, i) { return i; });
  }
  var result = [];
  var i = 0;
  while(i < source.length) {
    if (source.substring(i, i + find.length) == find) {
      result.push(i);
      i += find.length;
    } else {
      i++;
    }
  }
  return result;
}

7
+1. Fiz alguns testes de comparação com uma solução usando Regex. O método mais rápido foi o que usa Regex: jsperf.com/javascript-find-all
StuR

1
O método mais rápido é usar indexOf jsperf.com/find-o-substrings
Ethan

@LiEthan isso só importará se a função for um gargalo e talvez se a string de entrada for longa.
jcubic de

@jcubic Sua solução parece boa, mas tem apenas uma pequena confusão. E se eu chamar uma função como esta var result = indexes('aaaa', 'aa')? O resultado esperado deve ser [0, 1, 2]ou [0, 2]?
Cao Mạnh Quang

@ CaoMạnhQuang olhando para o código do primeiro resultado. Se você quiser o segundo, você precisa criar um loop while e dentro se você colocar i+=find.length;e em elsei++
jcubic

15

Você com certeza pode fazer isso!

//make a regular expression out of your needle
var needle = 'le'
var re = new RegExp(needle,'gi');
var haystack = 'I learned to play the Ukulele';

var results = new Array();//this is the results you want
while (re.exec(haystack)){
  results.push(re.lastIndex);
}

Editar: aprenda a soletrar RegExp

Além disso, percebi que isso não é exatamente o que você quer, pois lastIndexnos diz que o fim da agulha não é o começo, mas está perto - você pode empurrar re.lastIndex-needle.lengthpara a matriz de resultados ...

Editar: adicionar link

A resposta de @Tim Down usa o objeto de resultados de RegExp.exec (), e todos os meus recursos Javascript encobrem seu uso (além de fornecer a string correspondente). Então, quando ele usa result.index, é algum tipo de Match Object sem nome. Na descrição de exec do MDC , eles realmente descrevem esse objeto com detalhes decentes.


Ha! Obrigado por contribuir, em qualquer caso - agradeço!
Bungle

9

Um liner usando String.protype.matchAll(ES2020):

[...sourceStr.matchAll(new RegExp(searchStr, 'gi'))].map(a => a.index)

Usando seus valores:

const sourceStr = 'I learned to play the Ukulele in Lebanon.';
const searchStr = 'le';
const indexes = [...sourceStr.matchAll(new RegExp(searchStr, 'gi'))].map(a => a.index);
console.log(indexes); // [2, 25, 27, 33]

Se você está preocupado em fazer um spread e um map()em uma linha, executei com um for...ofloop para um milhão de iterações (usando suas strings). O único liner tem uma média de 1420ms, enquanto as for...ofmédias de 1150ms na minha máquina. Essa não é uma diferença insignificante, mas o forro funcionará bem se você estiver fazendo apenas um punhado de fósforos.

Veja matchAllno caniuse


3

Se você apenas deseja encontrar a posição de todas as correspondências, gostaria de apontar um pequeno hack:

var haystack = 'I learned to play the Ukulele in Lebanon.',
    needle = 'le',
    splitOnFound = haystack.split(needle).map(function (culm)
    {
        return this.pos += culm.length + needle.length
    }, {pos: -needle.length}).slice(0, -1); // {pos: ...} – Object wich is used as this

console.log(splitOnFound);

Pode não ser aplicável se você tiver um RegExp com comprimento variável, mas para alguns pode ser útil.

Isso é sensível a maiúsculas e minúsculas. Para insensibilidade ao caso, use a String.toLowerCasefunção antes.


Acho que sua resposta é a melhor, porque usar o RegExp é perigoso.
Bharata

1

Aqui está um código simples

function getIndexOfSubStr(str, searchToken, preIndex, output){
		 var result = str.match(searchToken);
     if(result){
     output.push(result.index +preIndex);
     str=str.substring(result.index+searchToken.length);
     getIndexOfSubStr(str, searchToken, preIndex, output)
     }
     return output;
  };

var str = "my name is 'xyz' and my school name is 'xyz' and my area name is 'xyz' ";
var  searchToken ="my";
var preIndex = 0;

console.log(getIndexOfSubStr(str, searchToken, preIndex, []));


0

Siga a resposta de @jcubic, a solução dele causou uma pequena confusão para o meu caso
Por exemplo var result = indexes('aaaa', 'aa')ele retornará ao [0, 1, 2]invés de [0, 2]
Então eu atualizei um pouco a solução dele conforme abaixo para corresponder ao meu caso

function indexes(text, subText, caseSensitive) {
    var _source = text;
    var _find = subText;
    if (caseSensitive != true) {
        _source = _source.toLowerCase();
        _find = _find.toLowerCase();
    }
    var result = [];
    for (var i = 0; i < _source.length;) {
        if (_source.substring(i, i + _find.length) == _find) {
            result.push(i);
            i += _find.length;  // found a subText, skip to next position
        } else {
            i += 1;
        }
    }
    return result;
}

0

Obrigado por todas as respostas. Passei por todos eles e descobri uma função que dá ao primeiro e ao último índice de cada ocorrência da substring 'agulha'. Estou postando aqui caso possa ajudar alguém.

Observe que não é o mesmo que a solicitação original apenas para o início de cada ocorrência. É mais adequado ao meu caso de uso porque você não precisa manter o comprimento da agulha.

function findRegexIndices(text, needle, caseSensitive){
  var needleLen = needle.length,
    reg = new RegExp(needle, caseSensitive ? 'gi' : 'g'),
    indices = [],
    result;

  while ( (result = reg.exec(text)) ) {
    indices.push([result.index, result.index + needleLen]);
  }
  return indices
}

0

Verifique esta solução que também conseguirá encontrar a mesma string de caracteres, diga-me se algo está faltando ou não está certo.

function indexes(source, find) {
    if (!source) {
      return [];
    }
    if (!find) {
        return source.split('').map(function(_, i) { return i; });
    }
    source = source.toLowerCase();
    find = find.toLowerCase();
    var result = [];
    var i = 0;
    while(i < source.length) {
      if (source.substring(i, i + find.length) == find)
        result.push(i++);
      else
        i++
    }
    return result;
  }
  console.log(indexes('aaaaaaaa', 'aaaaaa'))
  console.log(indexes('aeeaaaaadjfhfnaaaaadjddjaa', 'aaaa'))
  console.log(indexes('wordgoodwordgoodgoodbestword', 'wordgood'))
  console.log(indexes('I learned to play the Ukulele in Lebanon.', 'le'))


-1
function countInString(searchFor,searchIn){

 var results=0;
 var a=searchIn.indexOf(searchFor)

 while(a!=-1){
   searchIn=searchIn.slice(a*1+searchFor.length);
   results++;
   a=searchIn.indexOf(searchFor);
 }

return results;

}

Isso procura por ocorrências de uma string dentro de outra string em vez de expressões regulares.

-1

o código abaixo fará o trabalho por você:

function indexes(source, find) {
  var result = [];
  for(i=0;i<str.length; ++i) {
    // If you want to search case insensitive use 
    // if (source.substring(i, i + find.length).toLowerCase() == find) {
    if (source.substring(i, i + find.length) == find) {
      result.push(i);
    }
  }
  return result;
}

indexes("hello, how are you", "ar")

-2

Use String.prototype.match .

Aqui está um exemplo dos próprios documentos MDN:

var str = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
var regexp = /[A-E]/gi;
var matches_array = str.match(regexp);

console.log(matches_array);
// ['A', 'B', 'C', 'D', 'E', 'a', 'b', 'c', 'd', 'e']

Isso é muito simples.
igaurav

11
A questão é como encontrar índices de ocorrências, e não as ocorrências em si!
Luckylooke

1
embora essa resposta não corresponda à pergunta, mas é isso que eu estava procurando :)
AlexNikonov
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.