¿Cómo encontrar índices de todas las apariciones de una cadena en otra en JavaScript?

105

Estoy tratando de encontrar las posiciones de todas las ocurrencias de una cadena en otra cadena, sin distinción entre mayúsculas y minúsculas.

Por ejemplo, dada la cadena:

Aprendí a tocar el ukelele en el Líbano.

y la cadena de búsqueda le, quiero obtener la matriz:

[2, 25, 27, 33]

Ambas cadenas serán variables, es decir, no puedo codificar sus valores.

Pensé que esta era una tarea fácil para las expresiones regulares, pero después de luchar durante un tiempo para encontrar una que funcionara, no tuve suerte.

Encontré este ejemplo de cómo lograr esto usando .indexOf(), pero seguramente tiene que haber una forma más concisa de hacerlo.

Estropear
fuente

Respuestas:

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

ACTUALIZAR

No pude detectar en la pregunta original que la cadena de búsqueda debe ser una variable. Escribí otra versión para tratar este caso que usa indexOf, por lo que ha vuelto al punto de partida. Como señaló Wrikken en los comentarios, para hacer esto para el caso general con expresiones regulares, necesitaría escapar de los caracteres especiales de expresiones regulares, momento en el que creo que la solución de expresiones regulares se convierte en más un dolor de cabeza de lo que vale la 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
fuente
2
¿Cómo sería leuna cadena variable aquí? Incluso cuando new Regexp(str);se acecha el peligro de los caracteres especiales, la búsqueda, $2.50por ejemplo. Algo así regex = new Regexp(dynamicstring.replace(/([\\.+*?\\[^\\]$(){}=!<>|:])/g, '\\$1'));sería más cercano en mi humilde opinión. No estoy seguro de si js tiene un mecanismo de escape de expresiones regulares incorporado.
Wrikken
new RegExp(searchStr)sería el camino, y sí, en el caso general tendrías que escapar de los caracteres especiales. Realmente no vale la pena hacerlo a menos que necesite ese nivel de generalidad.
Tim Down
1
Gran respuesta y muy útil. ¡Muchas gracias, Tim!
Bungle
1
Si la cadena de búsqueda es una cadena vacía, obtienes un bucle infinito ... lo comprobaría.
HelpMeStackOverflowMyOnlyHope
2
Supongamos searchStr=aaay eso str=aaaaaa. Luego, en lugar de encontrar 4 ocurrencias, su código encontrará solo 2 porque está haciendo saltos searchStr.lengthen el bucle.
blazs
18

Aquí está la versión gratuita de expresiones regulares:

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 : y si desea hacer coincidir cadenas como 'aaaa' y 'aa' para encontrar [0, 2] use esta versión:

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
fuente
7
+1. Ejecuté algunas pruebas para compararlas con una solución que usa Regex. El método más rápido fue el que usaba Regex: jsperf.com/javascript-find-all
StuR
1
El método más rápido es usar indexOf jsperf.com/find-o-substrings
Ethan Yanjia Li
@LiEthan, solo importará si esa función es un cuello de botella y tal vez si la cadena de entrada es larga.
jcubic
@jcubic Su solución parece buena, pero solo tiene una pequeña confusión. ¿Qué pasa si llamo a una función como esta var result = indexes('aaaa', 'aa')? ¿El resultado esperado debería ser [0, 1, 2]o [0, 2]?
Cao Mạnh Quang
@ CaoMạnhQuang mirando el código el primer resultado. Si desea el segundo, debe crear un bucle while y dentro si coloca i+=find.length;y en elsei++
jcubic
15

¡Seguro que puedes hacer esto!

//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: aprende a deletrear RegExp

Además, me di cuenta de que esto no es exactamente lo que quieres, ya que lastIndexnos dice que el final de la aguja no es el principio, pero está cerca.re.lastIndex-needle.length ingresar a la matriz de resultados ...

Editar: agregar enlace

La respuesta de @Tim Down usa el objeto de resultados de RegExp.exec (), y todos mis recursos de Javascript pasan por alto su uso (además de darle la cadena coincidente). Entonces, cuando lo usa result.index, es una especie de Match Object sin nombre. En la descripción MDC de exec , en realidad describen este objeto con un detalle decente.

Ryley
fuente
¡Decir ah! Gracias por contribuir, en cualquier caso, ¡se lo agradezco!
Bungle
9

Un revestimiento usando String.protype.matchAll(ES2020):

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

Usando sus 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]

Si le preocupa hacer una extensión y una map()en una línea, lo ejecuté con un for...ofbucle para un millón de iteraciones (usando sus cadenas). El trazador de líneas tiene un promedio de 1420 ms, mientras que el for...ofpromedio de 1150 ms en mi máquina. Esa no es una diferencia insignificante, pero el delineador funcionará bien si solo estás haciendo un puñado de coincidencias.

Ver matchAllen caniuse

Benny Hinrichs
fuente
3

Si solo desea encontrar la posición de todas las coincidencias, me gustaría señalarle un pequeño truco:

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

Puede que no se pueda aplicar si tiene una expresión regular con longitud variable, pero para algunos podría ser útil.

Esto distingue entre mayúsculas y minúsculas. Para la insensibilidad a mayúsculas y minúsculas, utilice la String.toLowerCasefunción antes.

Hoffmann
fuente
Creo que su respuesta es la mejor, porque el uso de RegExp es peligroso.
Bharata
1

Aquí hay un código simple

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
fuente
0

Siga la respuesta de @jcubic, su solución causó una pequeña confusión en mi caso.Por
ejemplo var result = indexes('aaaa', 'aa'), volverá en [0, 1, 2]lugar de. [0, 2]
Así que actualicé un poco su solución como se muestra a continuación para que coincida con mi 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
fuente
0

Gracias por todas las respuestas. Los revisé todos y se me ocurrió una función que le da al primero un último índice de cada aparición de la subcadena 'aguja'. Lo estoy publicando aquí en caso de que ayude a alguien.

Tenga en cuenta que no es lo mismo que la solicitud original solo para el comienzo de cada aparición. Se adapta mejor a mi caso de uso porque no es necesario mantener la longitud de la aguja.

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
fuente
0

Verifique esta solución que también podrá encontrar la misma cadena de caracteres, avíseme si falta algo o no está bien.

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
fuente
-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
fuente
Esto busca apariciones de una cadena dentro de otra cadena en lugar de expresiones regulares.
-1

el siguiente código hará el trabajo por usted:

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
fuente
-2

Utilice String.prototype.match .

Aquí hay un ejemplo de los propios documentos de 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
fuente
Esto es bastante sencillo.
igaurav
11
La pregunta es cómo encontrar índices de ocurrencias, ¡no ocurrencias en sí mismas!
Luckylooke
1
a pesar de que esta respuesta no coincide con la pregunta, pero eso es lo que estaba buscando :)
AlexNikonov