Comment comparer des chaînes en ignorant les différences de casse

Utilisez une comparaison tenant compte de la locale pour gérer correctement la correspondance insensible à la casse dans toutes les langues

Introduction

La comparaison de chaînes insensible à la casse apparaît fréquemment dans les applications web. Les utilisateurs saisissent des requêtes de recherche en casse mixte, entrent des noms d'utilisateur avec une capitalisation incohérente ou remplissent des formulaires sans tenir compte de la casse des lettres. Votre application doit faire correspondre ces entrées correctement, que les utilisateurs saisissent en majuscules, minuscules ou casse mixte.

L'approche simple consiste à convertir les deux chaînes en minuscules et à les comparer. Cela fonctionne pour le texte anglais mais échoue pour les applications internationales. Différentes langues ont des règles différentes pour la conversion entre majuscules et minuscules. Une méthode de comparaison qui fonctionne pour l'anglais peut produire des résultats incorrects pour le turc, l'allemand, le grec ou d'autres langues.

JavaScript fournit l'API Intl.Collator pour gérer correctement la comparaison insensible à la casse dans toutes les langues. Cette leçon explique pourquoi la simple conversion en minuscules échoue, comment fonctionne la comparaison tenant compte de la locale et quand utiliser chaque approche.

L'approche naïve avec toLowerCase

Convertir les deux chaînes en minuscules avant la comparaison est l'approche la plus courante pour la correspondance insensible à la casse :

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

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

Ce modèle fonctionne pour le texte ASCII et les mots anglais. La comparaison traite les versions majuscules et minuscules de la même lettre comme identiques.

Vous pouvez utiliser cette approche pour une recherche floue :

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"]

Le filtre trouve tous les éléments contenant la requête de recherche quelle que soit la casse. Cela fournit le comportement attendu pour les utilisateurs qui saisissent des requêtes sans penser à la capitalisation.

Pourquoi l'approche naïve échoue pour le texte international

La méthode toLowerCase() convertit le texte selon les règles Unicode, mais ces règles ne fonctionnent pas de la même manière dans toutes les langues. L'exemple le plus célèbre est le problème du i turc.

En anglais, la lettre minuscule i se convertit en majuscule I. En turc, il existe deux lettres distinctes :

  • Le i minuscule avec point se convertit en İ majuscule avec point
  • Le ı minuscule sans point se convertit en I majuscule sans point

Cette différence casse la comparaison insensible à la casse :

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

// In English locale (correct)
console.log(word1.toLowerCase() === word2.toLowerCase());
// true

// In Turkish locale (incorrect)
console.log(word1.toLocaleLowerCase("tr") === word2.toLocaleLowerCase("tr"));
// false - "file" becomes "fıle"

Lors de la conversion de FILE en minuscules selon les règles turques, le I devient ı (sans point), produisant fıle. Cela ne correspond pas à file (avec i pointé), donc la comparaison retourne false même si les chaînes représentent le même mot.

D'autres langues présentent des problèmes similaires. L'allemand possède le caractère ß qui se convertit en majuscule en SS. Le grec possède plusieurs formes minuscules du sigma (σ et ς) qui se convertissent toutes deux en majuscule en Σ. La conversion de casse simple ne peut pas gérer correctement ces règles spécifiques aux langues.

Utilisation d'Intl.Collator avec sensibilité de base pour la comparaison insensible à la casse

L'API Intl.Collator fournit une comparaison de chaînes tenant compte de la locale avec une sensibilité configurable. L'option sensitivity contrôle quelles différences comptent lors de la comparaison.

Pour une comparaison insensible à la casse, utilisez sensitivity: "base" :

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

console.log(collator.compare("Hello", "hello"));
// 0 (strings are equal)

console.log(collator.compare("Hello", "HELLO"));
// 0 (strings are equal)

console.log(collator.compare("Hello", "Héllo"));
// 0 (strings are equal, accents ignored too)

La sensibilité de base ignore à la fois les différences de casse et d'accent. Seules les lettres de base comptent. La comparaison retourne 0 lorsque les chaînes sont équivalentes à ce niveau de sensibilité.

Cette approche gère correctement le problème du i turc :

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

console.log(collator.compare("file", "FILE"));
// 0 (correctly matches)

console.log(collator.compare("file", "FİLE"));
// 0 (correctly matches, even with dotted İ)

Le collator applique automatiquement les règles de conversion de casse turques. Les deux comparaisons reconnaissent les chaînes comme équivalentes, quel que soit le I majuscule qui apparaît dans l'entrée.

Utiliser localeCompare avec l'option sensitivity

La méthode localeCompare() offre une alternative pour effectuer une comparaison insensible à la casse. Elle accepte les mêmes options que Intl.Collator :

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

console.log(str1.localeCompare(str2, "en", { sensitivity: "base" }));
// 0 (strings are equal)

Cela produit le même résultat que l'utilisation de Intl.Collator avec la sensibilité de base. La comparaison ignore les différences de casse et renvoie 0 pour les chaînes équivalentes.

Vous pouvez l'utiliser dans le filtrage de tableaux :

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"]

Cependant, localeCompare() ne renvoie 0 que pour les correspondances exactes au niveau de sensibilité spécifié. Elle ne prend pas en charge la correspondance partielle comme includes(). Pour la recherche de sous-chaînes, vous devez toujours utiliser la conversion en minuscules ou implémenter un algorithme de recherche plus sophistiqué.

Choisir entre sensibilité de base et sensibilité aux accents

L'option sensitivity accepte quatre valeurs qui contrôlent différents aspects de la comparaison de chaînes :

Sensibilité de base

La sensibilité de base ignore à la fois la casse et les accents :

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

console.log(collator.compare("cafe", "café"));
// 0 (accents ignored)

console.log(collator.compare("cafe", "Café"));
// 0 (case and accents ignored)

console.log(collator.compare("cafe", "CAFÉ"));
// 0 (case and accents ignored)

Cela offre la correspondance la plus permissive. Les utilisateurs qui ne peuvent pas saisir de caractères accentués ou qui les omettent par commodité obtiennent toujours des correspondances correctes.

Sensibilité aux accents

La sensibilité aux accents ignore la casse mais prend en compte les accents :

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

console.log(collator.compare("cafe", "café"));
// -1 (accents matter)

console.log(collator.compare("cafe", "Café"));
// -1 (accents matter, case ignored)

console.log(collator.compare("Café", "CAFÉ"));
// 0 (case ignored, accents match)

Cela traite les lettres accentuées et non accentuées comme différentes tout en ignorant la casse. Utilisez cette option lorsque les différences d'accents sont significatives mais pas les différences de casse.

Choisir la bonne sensibilité pour votre cas d'usage

Pour la plupart des besoins de comparaison insensible à la casse, la sensibilité de base offre la meilleure expérience utilisateur :

  • Fonctionnalité de recherche où les utilisateurs saisissent des requêtes sans accents
  • Correspondance de noms d'utilisateur où la casse ne doit pas avoir d'importance
  • Recherche floue où vous souhaitez une flexibilité maximale
  • Validation de formulaire où Smith et smith doivent correspondre

Utilisez la sensibilité aux accents lorsque :

  • La langue nécessite de distinguer les caractères accentués
  • Vos données contiennent à la fois des versions accentuées et non accentuées avec des significations différentes
  • Vous avez besoin d'une comparaison insensible à la casse mais sensible aux accents

Effectuer une recherche insensible à la casse avec includes

L'API Intl.Collator compare des chaînes complètes mais ne fournit pas de correspondance de sous-chaînes. Pour une recherche insensible à la casse, vous devez toujours combiner la comparaison tenant compte des paramètres régionaux avec d'autres approches.

Une option consiste à utiliser toLowerCase() pour la recherche de sous-chaînes, mais en acceptant ses limitations pour le texte international :

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

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

Pour une recherche plus sophistiquée qui gère correctement le texte international, vous devez itérer à travers les positions possibles de sous-chaînes et utiliser le collator pour chaque comparaison :

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

Cette approche vérifie chaque sous-chaîne possible de la longueur correcte et utilise une comparaison tenant compte des paramètres régionaux pour chacune. Elle gère correctement le texte international mais a des performances inférieures à un simple includes().

Considérations de performance lors de l'utilisation d'Intl.Collator

La création d'une instance Intl.Collator implique le chargement des données de paramètres régionaux et le traitement des options. Lorsque vous devez effectuer plusieurs comparaisons, créez le collator une fois et réutilisez-le :

// Inefficient: creates collator for every comparison
function badCompare(items, target) {
  return items.filter(item =>
    new Intl.Collator("en", { sensitivity: "base" }).compare(item, target) === 0
  );
}

// Efficient: creates collator once, reuses it
function goodCompare(items, target) {
  const collator = new Intl.Collator("en", { sensitivity: "base" });
  return items.filter(item =>
    collator.compare(item, target) === 0
  );
}

La version efficace crée le collator une fois avant le filtrage. Chaque comparaison utilise la même instance, évitant ainsi la surcharge d'initialisation répétée.

Pour les applications qui effectuent des comparaisons fréquentes, créez des instances de collator au démarrage de l'application et exportez-les pour une utilisation dans l'ensemble de votre base de code :

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

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

// In your application code
import { caseInsensitiveCollator } from "./utils/collation";

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

Ce modèle maximise les performances et maintient un comportement de comparaison cohérent dans toute votre application.

Quand utiliser toLowerCase plutôt qu'Intl.Collator

Pour les applications exclusivement en anglais où vous contrôlez le contenu textuel et savez qu'il ne contient que des caractères ASCII, toLowerCase() fournit des résultats acceptables :

// Acceptable for English-only, ASCII-only text
const isMatch = str1.toLowerCase() === str2.toLowerCase();

Cette approche est simple, rapide et familière pour la plupart des développeurs. Si votre application ne gère vraiment jamais de texte international, la complexité supplémentaire de la comparaison tenant compte des paramètres régionaux peut ne pas apporter de valeur.

Pour les applications internationales ou les applications où les utilisateurs saisissent du texte dans n'importe quelle langue, utilisez Intl.Collator avec une sensibilité appropriée :

// Required for international text
const collator = new Intl.Collator(userLocale, { sensitivity: "base" });
const isMatch = collator.compare(str1, str2) === 0;

Cela garantit un comportement correct quelle que soit la langue parlée ou saisie par les utilisateurs. Le léger coût en performances de l'utilisation de Intl.Collator vaut la peine pour éviter des comparaisons incorrectes.

Même si votre application ne prend actuellement en charge que l'anglais, l'utilisation de la comparaison tenant compte des paramètres régionaux dès le départ facilite l'internationalisation future. L'ajout de la prise en charge de nouvelles langues ne nécessite aucune modification de la logique de comparaison.

Cas d'usage pratiques pour la comparaison insensible à la casse

La comparaison insensible à la casse apparaît dans de nombreux scénarios courants :

Correspondance de nom d'utilisateur et d'e-mail

Les utilisateurs saisissent les noms d'utilisateur et les adresses e-mail avec une capitalisation incohérente :

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" }

Cela trouve l'utilisateur quelle que soit la façon dont il capitalise son adresse e-mail.

Autocomplétion de recherche

Les suggestions d'autocomplétion doivent correspondre à la saisie partielle sans tenir compte de la casse :

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"]

Cela fournit des suggestions quelle que soit la casse saisie par les utilisateurs.

Correspondance de balises et de catégories

Les utilisateurs attribuent des balises ou des catégories au contenu sans capitalisation cohérente :

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

Cela fait correspondre les balises quelles que soient les différences de capitalisation.