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?

Estragar
fonte

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>

Tim Down
fonte
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;
}
jcubic
fonte
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.

Ryley
fonte
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

Benny Hinrichs
fonte
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.

Hoffmann
fonte
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, []));

Kapil Tiwari
fonte
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;
}
Cao Mạnh Quang
fonte
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
}
Roei Bahumi
fonte
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'))

Jignesh Sanghani
fonte
-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;

}
gaby de wilde
fonte
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")
G.Nader
fonte
-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']
tejasbubane
fonte
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