Wie man Strings ohne Berücksichtigung der Groß- und Kleinschreibung vergleicht

Verwenden Sie lokalisierungsbewusste Vergleiche, um die Übereinstimmung ohne Berücksichtigung der Groß- und Kleinschreibung über verschiedene Sprachen hinweg korrekt zu handhaben

Einführung

Groß- und Kleinschreibung ignorierende Zeichenkettenvergleiche kommen in Webanwendungen häufig vor. Benutzer geben Suchanfragen in gemischter Schreibweise ein, tragen Benutzernamen mit inkonsistenter Großschreibung ein oder füllen Formulare ohne Rücksicht auf die Groß- und Kleinschreibung aus. Ihre Anwendung muss diese Eingaben korrekt abgleichen, unabhängig davon, ob Benutzer Groß-, Kleinbuchstaben oder gemischte Schreibweise verwenden.

Der einfache Ansatz konvertiert beide Zeichenketten in Kleinbuchstaben und vergleicht sie dann. Dies funktioniert für englische Texte, versagt jedoch bei internationalen Anwendungen. Verschiedene Sprachen haben unterschiedliche Regeln für die Umwandlung zwischen Groß- und Kleinschreibung. Eine Vergleichsmethode, die für Englisch funktioniert, kann für Türkisch, Deutsch, Griechisch oder andere Sprachen falsche Ergebnisse liefern.

JavaScript bietet die Intl.Collator-API, um den Vergleich ohne Berücksichtigung der Groß- und Kleinschreibung in allen Sprachen korrekt zu handhaben. Diese Lektion erklärt, warum einfache Kleinbuchstabenkonvertierung fehlschlägt, wie sprachbewusster Vergleich funktioniert und wann welcher Ansatz zu verwenden ist.

Der naive Ansatz mit toLowerCase

Die Umwandlung beider Zeichenketten in Kleinbuchstaben vor dem Vergleich ist der häufigste Ansatz für Vergleiche ohne Berücksichtigung der Groß- und Kleinschreibung:

const str1 = "Hello";
const str2 = "HELLO";

console.log(str1.toLowerCase() === str2.toLowerCase());
// true

Dieses Muster funktioniert für ASCII-Text und englische Wörter. Der Vergleich behandelt Groß- und Kleinbuchstabenversionen des gleichen Buchstabens als identisch.

Sie können diesen Ansatz für unscharfe Suche verwenden:

const query = "apple";
const items = ["Apple", "Banana", "APPLE PIE", "Orange"];

const matches = items.filter(item =>
  item.toLowerCase().includes(query.toLowerCase())
);

console.log(matches);
// ["Apple", "APPLE PIE"]

Der Filter findet alle Elemente, die die Suchanfrage enthalten, unabhängig von der Groß- und Kleinschreibung. Dies bietet das erwartete Verhalten für Benutzer, die Anfragen eingeben, ohne über die Großschreibung nachzudenken.

Warum der naive Ansatz bei internationalen Texten versagt

Die Methode toLowerCase() konvertiert Text gemäß Unicode-Regeln, aber diese Regeln funktionieren nicht in allen Sprachen gleich. Das bekannteste Beispiel ist das türkische i-Problem.

Im Englischen wird der Kleinbuchstabe i in den Großbuchstaben I umgewandelt. Im Türkischen gibt es zwei unterschiedliche Buchstaben:

  • Der Kleinbuchstabe mit Punkt i wird zum Großbuchstaben mit Punkt İ
  • Der Kleinbuchstabe ohne Punkt ı wird zum Großbuchstaben ohne Punkt I

Dieser Unterschied führt zu Problemen bei Vergleichen ohne Berücksichtigung der Groß-/Kleinschreibung:

const word1 = "file";
const word2 = "FILE";

// Im englischen Gebietsschema (korrekt)
console.log(word1.toLowerCase() === word2.toLowerCase());
// true

// Im türkischen Gebietsschema (inkorrekt)
console.log(word1.toLocaleLowerCase("tr") === word2.toLocaleLowerCase("tr"));
// false - "file" wird zu "fıle"

Bei der Umwandlung von FILE in Kleinbuchstaben nach türkischen Regeln wird I zu ı (ohne Punkt), was fıle ergibt. Dies stimmt nicht mit file (mit Punkt über dem i) überein, sodass der Vergleich false zurückgibt, obwohl die Zeichenketten dasselbe Wort darstellen.

Andere Sprachen haben ähnliche Probleme. Im Deutschen wird das Zeichen ß in Großbuchstaben zu SS. Im Griechischen gibt es mehrere Kleinbuchstabenformen für Sigma (σ und ς), die beide zu Σ werden. Einfache Umwandlungen der Groß-/Kleinschreibung können diese sprachspezifischen Regeln nicht korrekt behandeln.

Verwendung von Intl.Collator mit Basissensitivität für Vergleiche ohne Berücksichtigung der Groß-/Kleinschreibung

Die Intl.Collator-API bietet einen gebietsschemabasierten Zeichenkettenvergleich mit konfigurierbarer Sensitivität. Die Option sensitivity steuert, welche Unterschiede beim Vergleich berücksichtigt werden.

Für Vergleiche ohne Berücksichtigung der Groß-/Kleinschreibung verwenden Sie sensitivity: "base":

const collator = new Intl.Collator("en", { sensitivity: "base" });

console.log(collator.compare("Hello", "hello"));
// 0 (Zeichenketten sind gleich)

console.log(collator.compare("Hello", "HELLO"));
// 0 (Zeichenketten sind gleich)

console.log(collator.compare("Hello", "Héllo"));
// 0 (Zeichenketten sind gleich, Akzente werden ebenfalls ignoriert)

Die Basissensitivität ignoriert sowohl Unterschiede in der Groß-/Kleinschreibung als auch bei Akzenten. Nur die Grundbuchstaben sind relevant. Der Vergleich gibt 0 zurück, wenn Zeichenketten auf dieser Sensitivitätsebene äquivalent sind.

Dieser Ansatz behandelt das türkische i-Problem korrekt:

const collator = new Intl.Collator("tr", { sensitivity: "base" });

console.log(collator.compare("file", "FILE"));
// 0 (korrekte Übereinstimmung)

console.log(collator.compare("file", "FİLE"));
// 0 (korrekte Übereinstimmung, auch mit dem Punkt-İ)

Der Collator wendet automatisch die türkischen Regeln für die Groß-/Kleinschreibung an. Beide Vergleiche erkennen die Zeichenketten als äquivalent an, unabhängig davon, welches großgeschriebene I in der Eingabe erscheint.

Verwendung von localeCompare mit der sensitivity-Option

Die Methode localeCompare() bietet eine alternative Möglichkeit, Vergleiche ohne Berücksichtigung der Groß- und Kleinschreibung durchzuführen. Sie akzeptiert die gleichen Optionen wie Intl.Collator:

const str1 = "Hello";
const str2 = "HELLO";

console.log(str1.localeCompare(str2, "en", { sensitivity: "base" }));
// 0 (Zeichenketten sind gleich)

Dies erzeugt das gleiche Ergebnis wie die Verwendung von Intl.Collator mit Basis-Sensitivität. Der Vergleich ignoriert Unterschiede in der Groß- und Kleinschreibung und gibt 0 für äquivalente Zeichenketten zurück.

Sie können dies beim Filtern von Arrays verwenden:

const query = "apple";
const items = ["Apple", "Banana", "APPLE PIE", "Orange"];

const matches = items.filter(item =>
  item.localeCompare(query, "en", { sensitivity: "base" }) === 0 ||
  item.toLowerCase().includes(query.toLowerCase())
);

console.log(matches);
// ["Apple"]

Allerdings gibt localeCompare() nur 0 für exakte Übereinstimmungen auf der angegebenen Sensitivitätsebene zurück. Es unterstützt keine Teilübereinstimmungen wie includes(). Für die Teilstring-Suche müssen Sie weiterhin die Umwandlung in Kleinbuchstaben verwenden oder einen ausgefeilteren Suchalgorithmus implementieren.

Wahl zwischen Basis- und Akzent-Sensitivität

Die Option sensitivity akzeptiert vier Werte, die verschiedene Aspekte des Zeichenkettenvergleichs steuern:

Basis-Sensitivität

Die Basis-Sensitivität ignoriert sowohl Groß- und Kleinschreibung als auch Akzente:

const collator = new Intl.Collator("en", { sensitivity: "base" });

console.log(collator.compare("cafe", "café"));
// 0 (Akzente werden ignoriert)

console.log(collator.compare("cafe", "Café"));
// 0 (Groß-/Kleinschreibung und Akzente werden ignoriert)

console.log(collator.compare("cafe", "CAFÉ"));
// 0 (Groß-/Kleinschreibung und Akzente werden ignoriert)

Dies bietet die nachsichtigste Übereinstimmung. Benutzer, die keine akzentuierten Zeichen eingeben können oder diese aus Bequemlichkeit überspringen, erhalten trotzdem korrekte Übereinstimmungen.

Akzent-Sensitivität

Die Akzent-Sensitivität ignoriert die Groß- und Kleinschreibung, berücksichtigt aber Akzente:

const collator = new Intl.Collator("en", { sensitivity: "accent" });

console.log(collator.compare("cafe", "café"));
// -1 (Akzente sind wichtig)

console.log(collator.compare("cafe", "Café"));
// -1 (Akzente sind wichtig, Groß-/Kleinschreibung wird ignoriert)

console.log(collator.compare("Café", "CAFÉ"));
// 0 (Groß-/Kleinschreibung wird ignoriert, Akzente stimmen überein)

Dies behandelt akzentuierte und nicht akzentuierte Buchstaben als unterschiedlich, während die Groß- und Kleinschreibung ignoriert wird. Verwenden Sie dies, wenn Akzentunterschiede signifikant sind, Unterschiede in der Groß- und Kleinschreibung jedoch nicht.

Die richtige Sensitivität für Ihren Anwendungsfall wählen

Für die meisten Anforderungen bei Vergleichen ohne Berücksichtigung der Groß- und Kleinschreibung bietet die Basissensitivität die beste Benutzererfahrung:

  • Suchfunktionalität, bei der Benutzer Abfragen ohne Akzente eingeben
  • Benutzernamenabgleich, bei dem Groß- und Kleinschreibung keine Rolle spielen sollte
  • Unscharfe Suche, bei der Sie maximale Flexibilität wünschen
  • Formularvalidierung, bei der Smith und smith übereinstimmen sollten

Verwenden Sie Akzentsensitivität, wenn:

  • Die Sprache eine Unterscheidung von akzentuierten Zeichen erfordert
  • Ihre Daten sowohl akzentuierte als auch nicht akzentuierte Versionen mit unterschiedlichen Bedeutungen enthalten
  • Sie einen Vergleich benötigen, der unabhängig von der Groß- und Kleinschreibung, aber akzentbewusst ist

Durchführung einer Suche ohne Berücksichtigung der Groß- und Kleinschreibung mit includes

Die Intl.Collator-API vergleicht vollständige Zeichenketten, bietet jedoch keine Teilstring-Übereinstimmung. Für eine Suche ohne Berücksichtigung der Groß- und Kleinschreibung müssen Sie den lokalisierungsbewussten Vergleich mit anderen Ansätzen kombinieren.

Eine Option ist die Verwendung von toLowerCase() für die Teilstring-Suche, wobei Sie deren Einschränkungen für internationalen Text akzeptieren:

function caseInsensitiveIncludes(text, query, locale = "en") {
  return text.toLowerCase().includes(query.toLowerCase());
}

const text = "The Quick Brown Fox";
console.log(caseInsensitiveIncludes(text, "quick"));
// true

Für eine anspruchsvollere Suche, die internationalen Text korrekt behandelt, müssen Sie durch mögliche Teilstring-Positionen iterieren und den Collator für jeden Vergleich verwenden:

function localeAwareIncludes(text, query, locale = "en") {
  const collator = new Intl.Collator(locale, { sensitivity: "base" });

  for (let i = 0; i <= text.length - query.length; i++) {
    const substring = text.slice(i, i + query.length);
    if (collator.compare(substring, query) === 0) {
      return true;
    }
  }

  return false;
}

const text = "The Quick Brown Fox";
console.log(localeAwareIncludes(text, "quick"));
// true

Dieser Ansatz überprüft jeden möglichen Teilstring der richtigen Länge und verwendet für jeden einen lokalisierungsbewussten Vergleich. Er behandelt internationalen Text korrekt, hat jedoch eine schlechtere Leistung als einfaches includes().

Leistungsüberlegungen bei der Verwendung von Intl.Collator

Das Erstellen einer Intl.Collator-Instanz beinhaltet das Laden von Gebietsschema-Daten und die Verarbeitung von Optionen. Wenn Sie mehrere Vergleiche durchführen müssen, erstellen Sie den Collator einmal und verwenden ihn wieder:

// Ineffizient: erstellt für jeden Vergleich einen Collator
function badCompare(items, target) {
  return items.filter(item =>
    new Intl.Collator("en", { sensitivity: "base" }).compare(item, target) === 0
  );
}

// Effizient: erstellt den Collator einmal, verwendet ihn wieder
function goodCompare(items, target) {
  const collator = new Intl.Collator("en", { sensitivity: "base" });
  return items.filter(item =>
    collator.compare(item, target) === 0
  );
}

Die effiziente Version erstellt den Collator einmal vor dem Filtern. Jeder Vergleich verwendet dieselbe Instanz und vermeidet wiederholten Initialisierungsaufwand.

Für Anwendungen, die häufige Vergleiche durchführen, erstellen Sie Collator-Instanzen beim Anwendungsstart und exportieren sie zur Verwendung in Ihrem gesamten Codebase:

// utils/collation.js
export const caseInsensitiveCollator = new Intl.Collator("en", {
  sensitivity: "base"
});

export const accentInsensitiveCollator = new Intl.Collator("en", {
  sensitivity: "accent"
});

// In Ihrem Anwendungscode
import { caseInsensitiveCollator } from "./utils/collation";

const isMatch = caseInsensitiveCollator.compare(input, expected) === 0;

Dieses Muster maximiert die Leistung und behält ein konsistentes Vergleichsverhalten in Ihrer gesamten Anwendung bei.

Wann toLowerCase versus Intl.Collator verwenden

Für rein englischsprachige Anwendungen, bei denen Sie den Textinhalt kontrollieren und wissen, dass er nur ASCII-Zeichen enthält, liefert toLowerCase() akzeptable Ergebnisse:

// Akzeptabel für rein englischen, nur ASCII-Text
const isMatch = str1.toLowerCase() === str2.toLowerCase();

Dieser Ansatz ist einfach, schnell und den meisten Entwicklern vertraut. Wenn Ihre Anwendung wirklich niemals internationalen Text verarbeitet, bietet die zusätzliche Komplexität des gebietsschema-bewussten Vergleichs möglicherweise keinen Mehrwert.

Für internationale Anwendungen oder Anwendungen, bei denen Benutzer Text in beliebigen Sprachen eingeben, verwenden Sie Intl.Collator mit angemessener Sensitivität:

// Erforderlich für internationalen Text
const collator = new Intl.Collator(userLocale, { sensitivity: "base" });
const isMatch = collator.compare(str1, str2) === 0;

Dies gewährleistet korrektes Verhalten unabhängig davon, welche Sprache Benutzer sprechen oder tippen. Die geringe Leistungseinbuße bei der Verwendung von Intl.Collator ist es wert, um falsche Vergleiche zu vermeiden.

Selbst wenn Ihre Anwendung derzeit nur Englisch unterstützt, erleichtert die Verwendung von gebietsschema-bewussten Vergleichen von Anfang an die zukünftige Internationalisierung. Die Unterstützung neuer Sprachen erfordert keine Änderungen an der Vergleichslogik.

Praktische Anwendungsfälle für die Vergleiche ohne Berücksichtigung der Groß- und Kleinschreibung

Vergleiche ohne Berücksichtigung der Groß- und Kleinschreibung kommen in vielen gängigen Szenarien vor:

Benutzername und E-Mail-Abgleich

Benutzer geben Benutzernamen und E-Mail-Adressen mit inkonsistenter Groß- und Kleinschreibung ein:

const collator = new Intl.Collator("en", { sensitivity: "base" });

function findUserByEmail(users, email) {
  return users.find(user =>
    collator.compare(user.email, email) === 0
  );
}

const users = [
  { email: "[email protected]", name: "John" },
  { email: "[email protected]", name: "Jane" }
];

console.log(findUserByEmail(users, "[email protected]"));
// { email: "[email protected]", name: "John" }

Dies findet den Benutzer unabhängig davon, wie er seine E-Mail-Adresse kapitalisiert hat.

Suchautovervollständigung

Autovervollständigungsvorschläge müssen teilweise Eingaben ohne Berücksichtigung der Groß- und Kleinschreibung abgleichen:

const collator = new Intl.Collator("en", { sensitivity: "base" });

function getSuggestions(items, query) {
  const queryLower = query.toLowerCase();

  return items.filter(item =>
    item.toLowerCase().startsWith(queryLower)
  );
}

const items = ["Apple", "Apricot", "Banana", "Cherry"];
console.log(getSuggestions(items, "ap"));
// ["Apple", "Apricot"]

Dies liefert Vorschläge unabhängig von der Groß- und Kleinschreibung, die Benutzer eingeben.

Tag- und Kategorieabgleich

Benutzer weisen Inhalten Tags oder Kategorien ohne einheitliche Groß- und Kleinschreibung zu:

const collator = new Intl.Collator("en", { sensitivity: "base" });

function hasTag(item, tag) {
  return item.tags.some(itemTag =>
    collator.compare(itemTag, tag) === 0
  );
}

const article = {
  title: "My Article",
  tags: ["JavaScript", "Tutorial", "Web Development"]
};

console.log(hasTag(article, "javascript"));
// true

Dies gleicht Tags unabhängig von Unterschieden in der Groß- und Kleinschreibung ab.