Comment trouver les indices de toutes les occurrences d'une chaîne dans une autre en JavaScript?

105

J'essaie de trouver les positions de toutes les occurrences d'une chaîne dans une autre chaîne, insensible à la casse.

Par exemple, étant donné la chaîne:

J'ai appris à jouer du ukulélé au Liban.

et la chaîne de recherche le, je veux obtenir le tableau:

[2, 25, 27, 33]

Les deux chaînes seront des variables - c'est-à-dire que je ne peux pas coder en dur leurs valeurs.

J'ai pensé que c'était une tâche facile pour les expressions régulières, mais après avoir lutté pendant un certain temps pour en trouver une qui fonctionnerait, je n'ai pas eu de chance.

J'ai trouvé cet exemple de la façon d'accomplir cela en utilisant .indexOf(), mais il doit sûrement y avoir une façon plus concise de le faire?

Bungle
la source

Réponses:

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

METTRE À JOUR

Je n'ai pas réussi à repérer dans la question initiale que la chaîne de recherche doit être une variable. J'ai écrit une autre version pour traiter ce cas qui utilise indexOf, vous êtes donc revenu à votre point de départ. Comme l'a souligné Wrikken dans les commentaires, pour le faire dans le cas général avec des expressions régulières, vous auriez besoin d'échapper aux caractères spéciaux de regex, à quel point je pense que la solution de regex devient plus un casse-tête qu'elle ne vaut la peine.

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
la source
2
Comment serait leune chaîne variable ici? Même lorsque vous utilisez new Regexp(str);le danger des caractères spéciaux se cache, la recherche $2.50par exemple. Quelque chose comme regex = new Regexp(dynamicstring.replace(/([\\.+*?\\[^\\]$(){}=!<>|:])/g, '\\$1'));serait plus proche IMHO. Je ne sais pas si js a un mécanisme d'échappement regex intégré.
Wrikken
new RegExp(searchStr)serait le moyen, et oui, dans le cas général, il faudrait échapper aux caractères spéciaux. Cela ne vaut vraiment la peine que si vous avez besoin de ce niveau de généralité.
Tim Down
1
Excellente réponse et très utile. Merci beaucoup, Tim!
Bungle
1
Si la chaîne de recherche est une chaîne vide, vous obtenez une boucle infinie ... le ferait une vérification.
HelpMeStackOverflowMyOnlyHope
2
Supposons searchStr=aaaet cela str=aaaaaa. Ensuite, au lieu de trouver 4 occurrences, votre code n'en trouvera que 2 parce que vous faites des sauts searchStr.lengthdans la boucle.
blazs
18

Voici la version gratuite de 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")

EDIT : et si vous voulez faire correspondre des chaînes comme 'aaaa' et 'aa' pour trouver [0, 2] utilisez cette version:

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
la source
7
+1. J'ai effectué des tests de comparaison avec une solution utilisant Regex. La méthode la plus rapide était celle utilisant Regex: jsperf.com/javascript-find-all
StuR
1
La méthode la plus rapide utilise indexOf jsperf.com/find-o-substrings
Ethan Yanjia Li
@LiEthan cela n'aura d'importance que si cette fonction est un goulot d'étranglement et peut-être si la chaîne d'entrée est longue.
jcubic
@jcubic Votre solution semble bonne, mais a juste une petite confusion. Et si j'appelle une fonction comme ça var result = indexes('aaaa', 'aa')? Le résultat attendu devrait être [0, 1, 2]ou [0, 2]?
Cao Mạnh Quang
@ CaoMạnhQuang regarde le code le premier résultat. Si vous voulez le second, vous devez créer une boucle while et à l'intérieur si vous mettez i+=find.length;et dans elsei++
jcubic
15

Vous pouvez certainement le faire!

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

Edit: apprenez à épeler RegExp

De plus, j'ai réalisé que ce n'était pas exactement ce que vous vouliez, comme lastIndexnous le dit la fin de l'aiguille, pas le début, mais c'est proche - vous pouvez pousser re.lastIndex-needle.lengthdans le tableau des résultats ...

Modifier: ajouter un lien

La réponse de @Tim Down utilise l'objet de résultats de RegExp.exec (), et toutes mes ressources Javascript passent sous silence son utilisation (en plus de vous donner la chaîne correspondante). Donc, quand il l'utilise result.index, c'est une sorte d'objet de correspondance sans nom. Dans la description MDC de exec , ils décrivent en fait cet objet avec des détails décents.

Ryley
la source
Ha! Merci d'avoir contribué, en tout cas - je l'apprécie!
Bungle
9

Une doublure utilisant String.protype.matchAll(ES2020):

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

En utilisant vos valeurs:

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]

Si vous craignez de faire un spread et un map()en une ligne, je l'ai exécuté avec une for...ofboucle pour un million d'itérations (en utilisant vos chaînes). La seule doublure mesure en moyenne 1420 ms tandis que lafor...of moyenne est de 1150 ms sur ma machine. Ce n'est pas une différence insignifiante, mais la seule doublure fonctionnera bien si vous ne faites que quelques matchs.

Voir matchAllsur caniuse

Benny Hinrichs
la source
3

Si vous voulez juste trouver la position de tous les matchs, je voudrais vous indiquer un petit 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);

Cela peut ne pas être applicable si vous avez une RegExp avec une longueur variable, mais pour certains, cela peut être utile.

Ceci est sensible à la casse. Pour l'insensibilité à la casse, utilisez la String.toLowerCasefonction avant.

Hoffmann
la source
Je pense que votre réponse est la meilleure, car l'utilisation de RegExp est dangereuse.
Bharata
1

Voici un simple extrait de code:

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
la source
0

Suivez la réponse de @jcubic, sa solution a causé une petite confusion pour mon cas
Par exemple var result = indexes('aaaa', 'aa')il reviendra à la [0, 1, 2]place de [0, 2]
Donc j'ai mis à jour un peu sa solution comme ci-dessous pour correspondre à mon cas

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
la source
0

Merci pour toutes vos réponses. Je les ai tous parcourus et j'ai trouvé une fonction qui donne au premier un dernier index de chaque occurrence de la sous-chaîne «aiguille». Je le poste ici au cas où cela aiderait quelqu'un.

Veuillez noter que ce n'est pas la même chose que la demande originale pour seulement le début de chaque occurrence. Cela convient mieux à mon cas d'utilisation car vous n'avez pas besoin de garder la longueur de l'aiguille.

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
la source
0

Vérifiez cette solution qui pourra également trouver la même chaîne de caractères, faites-moi savoir si quelque chose manque ou ne va pas.

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
la source
0

Voici ma solution sans regex.

const findOccurrences = (fullStr, searchStr) => {
    const fullString = fullStr.toLowerCase();
    const searchString = searchStr.toLowerCase();
    const possibleOccurrences = [];
    const occurrenceIndices = [];

    for (let i = 0; i <= fullString.length - searchString.length; i++) {
        possibleOccurrences.push(fullString.slice(i, i + searchString.length));
    }
    for (let k = 0; k < possibleOccurrences.length; k++) {
        if (possibleOccurrences[k] === searchString) {
            occurrenceIndices.push(k);
        }
    }

    return occurrenceIndices;
}
Yu Jie
la source
0

Je recommanderais la réponse de Tim. Cependant, @blazs déclare "Supposons searchStr = aaa et que str = aaaaaa. Ensuite, au lieu de trouver 4 occurrences, votre code n'en trouvera que 2 car vous faites des sauts par searchStr.length dans la boucle.", Ce qui est vrai en regardant Code de Tim, spécifiquement cette ligne ici: startIndex = index + searchStrLen; Le code de Tim ne serait pas en mesure de trouver une instance de la chaîne recherchée dans la longueur d'elle-même. Donc, j'ai modifié la réponse de Tim:

function getIndicesOf(searchStr, str, caseSensitive) {
    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 + 1;
    }
    return indices;
}

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

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

Le changer en "+ 1" au lieu de "+ searchStrLen" permettra à l'index 1 d'être dans le tableau des indices si j'ai une chaîne de "aaaa" et un searchStr de "aa".

Deuxième réponse

J'ai un autre extrait de code qui fonctionne également. Cela imite la réponse fournie par @YuJie, sauf que celle-ci est plus compacte que la réponse de Yu Jie:

function getIndicesOf(searchStr, str, caseSensitive) {
    var startIndex = 0, index, indices = [];
    if (!caseSensitive) {
        str = str.toLowerCase();
        searchStr = searchStr.toLowerCase();
    }
    for (var i=0; i<str.length-1; i++) {
        if (str.substr(i, searchStr.length) == searchStr) {
            indices.push(i);
        }
    }
    return indices;
}

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

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

Cependant, l'inconvénient de cet extrait de code est qu'il peut prendre un peu plus de temps que le premier, car le premier utilise la fonction intégrée JavaScript indexOf (), tandis que le second est un peu comme le vieil adage "re -inventer la roue. " Donc, dans l'ensemble, je recommanderais ma première réponse au lieu de celle-ci. PS Si quelqu'un souhaite des commentaires dans le code pour expliquer le fonctionnement du code, veuillez le dire, et je serai heureux de répondre à la demande.

Le gars expert en technologie
la source
-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
la source
Cela recherche les occurrences d'une chaîne dans une autre chaîne plutôt que des expressions régulières.
-1

le code ci-dessous fera le travail pour vous:

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
la source
-2

Utilisez String.prototype.match .

Voici un exemple tiré de la documentation MDN elle-même:

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
la source
C'est assez simple.
igaurav
11
La question est de savoir comment trouver des indices d'occurrences, pas d'occurrences elles-mêmes!
Luckylooke
1
malgré cette réponse ne correspond pas à la question, mais c'est ce que je cherchais :)
AlexNikonov