Una delle cose più divertenti della programmazione è trovare tutti i modi disponibili per risolvere un problema. Oggi voglio capire, e condividere, quali metodi esistono per verificare se una stringa contiene una sottostringa in JavaScript.

String.prototype.includes()

Cominciamo con la soluzione migliore, il metodo String.prototype.includes(). Permette di verificare se una stringa contiene una sottostringa, e ritorna un valore booleano.

const string = "Hello World!";

console.log(string.includes("Hello")); // true
console.log(string.includes("!")); // true
console.log(string.includes("Hello World!")); // true

console.log(string.includes("Hello World!!")); // false

Il suo limite principale è che è case-sensitive, quindi non funziona con le stringhe che contengono caratteri maiuscoli e minuscoli.

const string = "Hello World!";
console.log(string.includes("hello")); // false

Possiamo risolvere questo problema trasformando la stringa in minuscolo prima di eseguire il controllo.

const string = "Hello World!";
const substring = "HeLLo";
console.log(string.toLowerCase().includes(substring.toLowerCase())); // true

String.prototype.indexOf()

Un secondo metodo usa String.prototype.indexOf(). Questo metodo ritorna l’indice della prima occorrenza di una sottostringa all’interno di una stringa, o -1 se non viene trovata. Posso quindi convertire l’indice in un valore booleano.

const string = "Hello World!";

console.log(string.indexOf("Hello") !== -1); // true
console.log(!string.indexOf("Hello")); // true

Anche in questo caso, il metodo è case-sensitive.

const string = "Hello World!";
const substring = "HeLLo";

console.log(!string.indexOf(substring)); // false

console.log(!string.toLowerCase().indexOf(substring.toLowerCase())); // true

Polyfill

Se non è possibile usare un browser moderno (a me a volte capita ancora), è comunque possibile creare un polyfill per aggiungere il metodo String.prototype.includes(). Per farlo uso ancora una volta String.prototype.indexOf() ma lo nascondo in un metodo includes().

if (!String.prototype.includes) {
  String.prototype.includes = function (search, start) {
    "use strict";
    if (typeof start !== "number") {
      start = 0;
    }

    if (start + search.length > this.length) {
      return false;
    } else {
      return this.indexOf(search, start) !== -1;
    }
  };
}

Knuth–Morris–Pratt algorithm

Un’altra soluzione è usare l’algoritmo di Knuth–Morris–Pratt. Questo algoritmo, degli anni ‘70, permette una ricerca molto veloce, ed è spesso usato come base per altri algoritmi di ricerca di stringhe. Questa è la versione di Nayuki

function kmpSearch(pattern, text) {
  if (pattern.length == 0) return 0; // Immediate match

  // Compute longest suffix-prefix table
  var lsp = [0]; // Base case
  for (var i = 1; i < pattern.length; i++) {
    var j = lsp[i - 1]; // Start by assuming we're extending the previous LSP
    while (j > 0 && pattern[i] !== pattern[j]) j = lsp[j - 1];
    if (pattern[i] === pattern[j]) j++;
    lsp.push(j);
  }

  // Walk through text string
  var j = 0; // Number of chars matched in pattern
  for (var i = 0; i < text.length; i++) {
    while (j > 0 && text[i] != pattern[j]) j = lsp[j - 1]; // Fall back in the pattern
    if (text[i] == pattern[j]) {
      j++; // Next char matched, increment position
      if (j == pattern.length) return i - (j - 1);
    }
  }
  return -1; // Not found
}

console.log(kmpSearch("ays", "haystack") != -1); // true
console.log(kmpSearch("asdf", "haystack") != -1); // false

Altri metodi

Ma questi non sono le uniche strade che possiamo prendere. Esistono altre possibilità, a patto di forzare un po’ la mano a JavaScript. Non consiglio di usarle, ma sono interessante per capire come funziona JavaScript.

Nei prossimi esempi userò due variabili, string e substring, che conterranno rispettivamente la stringa e la sottostringa da cercare.

const string = "Hello World!";
const substring = "Hello";

Posso usare String.prototype.match() per trovare il risultato di una ricerca con una espressione regolare. Se la ricerca non trova nulla, ritorna null. Converto quindi il risultato in un valore booleano.

console.log(!!string.match(substring)); // true

In maniera simile String.prototype.search() restituisce -1 se la ricerca non trova nulla. Mi basta quindi controllare che il risultato sia maggiore o uguale a zero.

console.log(string.search(substring) >= 0); // true

Un altro metodo può essere di sostituire la sottostringa con un valore vuoto e controllare che la lunghezza della stringa sia cambiata. Oppure posso anche semplicemente verificare che non sia uguale a sé stessa. Per farlo uso il metodo String.prototype.replace()

console.log(string.replace(substring, "") != string); // true
console.log(string.replace(substring, "").length != string.length); // true

startWith() e endsWith()

Infine, se voglio controllare solamente l’inizio o la fine posso usare String.prototype.startsWith() e String.prototype.endsWith()

const string = "Hello World!";

console.log(string.startsWith("Hello")); // true
console.log(string.endsWith("!")); // true

Questi due metodi possono essere utili in casi particolari, e può capitare di doverli usare più di quanto uno possa pensare. Ma se si vuole fare una ricerca generica, è meglio usare un’altra soluzione.

Conclusioni

Bene, direi che queste dieci soluzioni possono bastare. Al di là del divertissement legato a questo caso specifico, vale sempre la pena dedicare un po’ di tempo per esplorare le varie soluzioni disponibili. Non è sempre detto che la prima idea sia la migliore. E, in generale, è un buon modo per approfondire un argomento.