Jak znaleźć indeksy wszystkich wystąpień jednego ciągu w innym w JavaScript?

105

Próbuję znaleźć pozycje wszystkich wystąpień ciągu w innym ciągu, bez uwzględniania wielkości liter.

Na przykład, biorąc pod uwagę ciąg:

Nauczyłem się grać na ukulele w Libanie.

i ciąg wyszukiwania le, chcę uzyskać tablicę:

[2, 25, 27, 33]

Oba łańcuchy będą zmiennymi - tj. Nie mogę zakodować ich wartości na stałe.

Pomyślałem, że to łatwe zadanie w przypadku wyrażeń regularnych, ale po dłuższej walce ze znalezieniem takiego, który zadziała, nie miałem szczęścia.

Znalazłem przykład, jak to zrobić .indexOf(), ale z pewnością musi być bardziej zwięzły sposób, aby to zrobić?

Klecić
źródło

Odpowiedzi:

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);
}

AKTUALIZACJA

W pierwotnym pytaniu nie udało mi się zauważyć, że szukany ciąg musi być zmienną. Napisałem inną wersję, aby poradzić sobie z tą sprawą, która używa indexOf, więc wróciłeś do początku. Jak zauważył Wrikken w komentarzach, aby zrobić to dla ogólnego przypadku z wyrażeniami regularnymi, należałoby uciec od specjalnych znaków regex, w którym to momencie myślę, że rozwiązanie regex staje się bardziej bolesne niż jest warte.

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
źródło
2
Jak wyglądałby letutaj ciąg zmiennych? Nawet podczas korzystania new Regexp(str);z niebezpieczeństwa znaków specjalnych czai się $2.50na przykład szukając . Coś takiego regex = new Regexp(dynamicstring.replace(/([\\.+*?\\[^\\]$(){}=!<>|:])/g, '\\$1'));byłoby bliższe IMHO. Nie jestem pewien, czy js ma wbudowany mechanizm ucieczki wyrażeń regularnych.
Wrikken
new RegExp(searchStr)byłoby sposobem, i tak, w ogólnym przypadku musiałbyś uciec od znaków specjalnych. Nie warto tego robić, chyba że potrzebujesz tego poziomu ogólności.
Tim Down
1
Świetna odpowiedź i bardzo pomocna. Wielkie dzięki, Tim!
Bungle
1
Jeśli szukany ciąg jest pustym ciągiem, otrzymasz nieskończoną pętlę ... sprawdziłby ją.
HelpMeStackOverflowMyOnlyHope
2
Przypuśćmy searchStr=aaai to str=aaaaaa. Wtedy zamiast znaleźć 4 wystąpienia, twój kod znajdzie tylko 2, ponieważ wykonujesz pomijanie searchStr.lengthw pętli.
płonie
18

Oto darmowa wersja 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")

EDYTUJ : a jeśli chcesz dopasować ciągi, takie jak „aaaa” i „aa”, aby znaleźć [0, 2], użyj tej wersji:

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
źródło
7
+1. Przeprowadziłem kilka testów w celu porównania z rozwiązaniem używającym Regex. Najszybszą metodą była metoda Regex: jsperf.com/javascript-find-all
StuR
1
Najszybszą metodą jest użycie IndexOf jsperf.com/find-o-substrings
Ethan Yanjia Li
@LiEthan będzie to miało znaczenie tylko wtedy, gdy ta funkcja jest wąskim gardłem i może jeśli ciąg wejściowy jest długi.
jcubic
@jcubic Twoje rozwiązanie wydaje się dobre, ale zawiera tylko małe zamieszanie. Co jeśli wywołam taką funkcję var result = indexes('aaaa', 'aa')? Oczekiwany wynik powinien być [0, 1, 2]lub [0, 2]?
Cao Mạnh Quang
@ CaoMạnhQuang patrząc na kod pierwszy wynik. Jeśli chcesz drugi, musisz utworzyć pętlę while i wewnątrz, jeśli umieścisz i+=find.length;i w innymi++
jcubic
15

Na pewno możesz to zrobić!

//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);
}

Edycja: naucz się pisać RegExp

Zrozumiałem też, że to nie jest dokładnie to, czego chcesz, ponieważ lastIndexmówi nam, że koniec igły nie jest początkiem, ale jest blisko - możesz wcisnąć re.lastIndex-needle.lengthdo tablicy wyników ...

Edycja: dodawanie linku

Odpowiedź @Tim Down wykorzystuje obiekt wyników z RegExp.exec (), a wszystkie moje zasoby JavaScript prześlizgują się nad jego użyciem (poza podaniem dopasowanego ciągu). Więc kiedy używa result.index, jest to jakiś nienazwany obiekt dopasowania. W opisie exec w MDC , faktycznie opisują ten obiekt z przyzwoitymi szczegółami.

Ryley
źródło
Ha! W każdym razie dziękuję za wkład - doceniam to!
Bungle
9

Jedna wkładka przy użyciu String.protype.matchAll(ES2020):

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

Używając swoich wartości:

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]

Jeśli martwisz się o zrobienie spreadu i a map()w jednej linii, uruchomiłem go z for...ofpętlą na milion iteracji (używając twoich ciągów znaków). Jedna liner zajmuje średnio 1420 ms, podczas gdy średnia for...ofwynosi 1150 ms na moim komputerze. Nie jest to nieznacząca różnica, ale jedna wkładka będzie działać dobrze, jeśli wykonujesz tylko kilka meczów.

Zobacz matchAllna caniuse

Benny Hinrichs
źródło
3

Jeśli chcesz tylko znaleźć pozycję wszystkich dopasowań, chciałbym wskazać ci mały 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);

Może nie nadawać się do zastosowania, jeśli masz wyrażenie RegExp o zmiennej długości, ale dla niektórych może być pomocne.

Rozróżniana jest wielkość liter. W przypadku niewrażliwości na wielkość liter użyj String.toLowerCasewcześniej funkcji.

Hoffmann
źródło
Myślę, że twoja odpowiedź jest najlepsza, ponieważ używanie RegExp jest niebezpieczne.
Bharata
1

Oto prosty kod

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
źródło
0

Postępuj zgodnie z odpowiedzią @jcubic, jego rozwiązanie spowodowało małe zamieszanie w mojej sprawie
Na przykład var result = indexes('aaaa', 'aa')zwróci [0, 1, 2]zamiast tego [0, 2]
Więc zaktualizowałem nieco jego rozwiązanie jak poniżej, aby pasowało do mojej sprawy

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
źródło
0

Dzięki za wszystkie odpowiedzi. Przejrzałem je wszystkie i wymyśliłem funkcję, która daje pierwszemu i ostatniemu indeksowi każdego wystąpienia podciągu „igłowego”. Wrzucam to tutaj na wypadek, gdyby to komuś pomogło.

Należy pamiętać, że to nie to samo, co pierwotne żądanie dotyczące tylko początku każdego wystąpienia. Bardziej pasuje do mojego przypadku, ponieważ nie musisz utrzymywać długości igły.

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
źródło
0

Sprawdź to rozwiązanie, które będzie w stanie znaleźć ten sam ciąg znaków, daj mi znać, jeśli czegoś brakuje lub nie jest dobrze.

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
źródło
-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
źródło
Wyszukuje wystąpienia łańcucha wewnątrz innego ciągu zamiast wyrażeń regularnych.
-1

poniższy kod wykona zadanie za Ciebie:

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
źródło
-2

Użyj String.prototype.match .

Oto przykład z samej dokumentacji 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
źródło
To jest całkiem proste.
igaurav
11
Pytanie brzmi, jak znaleźć indeksy zdarzeń, a nie same wystąpienia!
Luckylooke
1
mimo wszystko ta odpowiedź nie pasuje do pytania, ale właśnie tego szukałem :)
AlexNikonov