Comment trier des chaînes alphabétiquement par locale en JavaScript

Utilisez Intl.Collator et localeCompare() pour trier correctement les chaînes dans n'importe quelle langue

Introduction

Lorsque vous triez un tableau de chaînes en JavaScript, le comportement par défaut compare les chaînes par leurs valeurs d'unités de code UTF-16. Cela fonctionne pour le texte ASCII de base, mais échoue lors du tri de noms, de titres de produits ou de tout texte contenant des caractères accentués, des écritures non latines ou des lettres de casse mixte.

Différentes langues ont des règles différentes pour l'ordre alphabétique. Le suédois place å, ä et ö à la fin de l'alphabet après z. L'allemand traite ä comme équivalent à a dans la plupart des contextes. Le français ignore les accents dans certains modes de comparaison. Ces règles linguistiques déterminent comment les gens s'attendent à voir les listes triées dans leur langue.

JavaScript fournit deux API pour le tri de chaînes tenant compte de la locale. La méthode String.prototype.localeCompare() gère les comparaisons simples. L'API Intl.Collator offre de meilleures performances lors du tri de grands tableaux. Cette leçon explique comment les deux fonctionnent, quand utiliser chacune et comment configurer le comportement de tri pour différentes langues.

Pourquoi le tri par défaut échoue pour le texte international

La méthode Array.sort() par défaut compare les chaînes par leurs valeurs d'unités de code UTF-16. Cela signifie que les lettres majuscules viennent toujours avant les lettres minuscules, et les caractères accentués se trient après z.

const names = ['Åsa', 'Anna', 'Örjan', 'Bengt', 'Ärla'];
const sorted = names.sort();
console.log(sorted);
// Output: ['Anna', 'Bengt', 'Åsa', 'Ärla', 'Örjan']

Ce résultat est incorrect pour le suédois. En suédois, å, ä et ö sont des lettres distinctes qui appartiennent à la fin de l'alphabet. L'ordre correct devrait placer Anna en premier, puis Bengt, puis Åsa, Ärla et Örjan.

Le problème se produit parce que le tri par défaut compare les valeurs de points de code, pas la signification linguistique. La lettre Å a le point de code U+00C5, qui est supérieur au point de code pour z (U+007A). JavaScript n'a aucun moyen de savoir que les locuteurs suédois considèrent Å comme une lettre distincte avec une position spécifique dans l'alphabet.

La casse mixte crée un autre problème.

const words = ['zebra', 'Apple', 'banana', 'Zoo'];
const sorted = words.sort();
console.log(sorted);
// Output: ['Apple', 'Zoo', 'banana', 'zebra']

Toutes les lettres majuscules ont des valeurs de point de code inférieures aux lettres minuscules. Cela fait apparaître Apple et Zoo avant banana, ce qui n'est l'ordre alphabétique dans aucune langue.

Comment localeCompare trie les chaînes selon les règles linguistiques

La méthode localeCompare() compare deux chaînes selon les règles d'ordre de tri d'une locale spécifique. Elle renvoie un nombre négatif si la première chaîne vient avant la seconde, zéro si elles sont équivalentes, et un nombre positif si la première chaîne vient après la seconde.

const result = 'a'.localeCompare('b', 'en-US');
console.log(result);
// Output: -1 (negative means 'a' comes before 'b')

Vous pouvez utiliser localeCompare() directement avec Array.sort() en la passant comme fonction de comparaison.

const names = ['Åsa', 'Anna', 'Örjan', 'Bengt', 'Ärla'];
const sorted = names.sort((a, b) => a.localeCompare(b, 'sv-SE'));
console.log(sorted);
// Output: ['Anna', 'Bengt', 'Åsa', 'Ärla', 'Örjan']

La locale suédoise place Anna et Bengt en premier car ils utilisent des lettres latines standard. Viennent ensuite Åsa, Ärla et Örjan avec leurs lettres spéciales suédoises à la fin.

La même liste triée avec une locale allemande produit des résultats différents.

const names = ['Åsa', 'Anna', 'Örjan', 'Bengt', 'Ärla'];
const sorted = names.sort((a, b) => a.localeCompare(b, 'de-DE'));
console.log(sorted);
// Output: ['Anna', 'Ärla', 'Åsa', 'Bengt', 'Örjan']

L'allemand traite ä comme équivalent à a pour les besoins du tri. Cela place Ärla juste après Anna, au lieu de la fin comme le fait le suédois.

Quand utiliser localeCompare

Utilisez localeCompare() lorsque vous devez trier un petit tableau ou comparer deux chaînes. Elle fournit une API simple sans nécessiter la création et la gestion d'un objet collator.

const items = ['Banana', 'apple', 'Cherry'];
const sorted = items.sort((a, b) => a.localeCompare(b, 'en-US'));
console.log(sorted);
// Output: ['apple', 'Banana', 'Cherry']

Cette approche fonctionne bien pour les tableaux de quelques dizaines d'éléments. L'impact sur les performances est négligeable pour les petits ensembles de données.

Vous pouvez également utiliser localeCompare() pour vérifier si une chaîne vient avant une autre sans trier un tableau entier.

const firstName = 'Åsa';
const secondName = 'Anna';

if (firstName.localeCompare(secondName, 'sv-SE') < 0) {
  console.log(`${firstName} comes before ${secondName}`);
} else {
  console.log(`${secondName} comes before ${firstName}`);
}
// Output: "Anna comes before Åsa"

Cette comparaison respecte l'ordre alphabétique suédois sans nécessiter de trier un tableau complet.

Comment Intl.Collator améliore les performances

L'API Intl.Collator crée une fonction de comparaison réutilisable optimisée pour une utilisation répétée. Lorsque vous triez de grands tableaux ou effectuez de nombreuses comparaisons, un collator est nettement plus rapide que d'appeler localeCompare() pour chaque comparaison.

const collator = new Intl.Collator('sv-SE');
const names = ['Åsa', 'Anna', 'Örjan', 'Bengt', 'Ärla'];
const sorted = names.sort(collator.compare);
console.log(sorted);
// Output: ['Anna', 'Bengt', 'Åsa', 'Ärla', 'Örjan']

La propriété collator.compare renvoie une fonction de comparaison qui fonctionne directement avec Array.sort(). Vous n'avez pas besoin de l'envelopper dans une fonction fléchée.

Créer un collator une fois et le réutiliser pour plusieurs opérations évite la surcharge de recherche des données de locale à chaque comparaison.

const collator = new Intl.Collator('de-DE');

const germanCities = ['München', 'Berlin', 'Köln', 'Hamburg'];
const sortedCities = germanCities.sort(collator.compare);

const germanNames = ['Müller', 'Schmidt', 'Schröder', 'Fischer'];
const sortedNames = germanNames.sort(collator.compare);

console.log(sortedCities);
// Output: ['Berlin', 'Hamburg', 'Köln', 'München']

console.log(sortedNames);
// Output: ['Fischer', 'Müller', 'Schmidt', 'Schröder']

Le même collator gère les deux tableaux sans nécessiter la création d'une nouvelle instance.

Quand utiliser Intl.Collator

Utilisez Intl.Collator lors du tri de tableaux contenant des centaines ou des milliers d'éléments. Le bénéfice en termes de performances augmente avec la taille du tableau car la fonction de comparaison est appelée de nombreuses fois pendant le tri.

const collator = new Intl.Collator('en-US');
const products = [/* array with 10,000 product names */];
const sorted = products.sort(collator.compare);

Pour les tableaux de plus de quelques centaines d'éléments, le collator peut être plusieurs fois plus rapide que localeCompare().

Utilisez également Intl.Collator lorsque vous devez trier plusieurs tableaux avec la même locale et les mêmes options. Créer le collator une fois et le réutiliser élimine les recherches répétées de données de locale.

const collator = new Intl.Collator('fr-FR');

const firstNames = ['Amélie', 'Bernard', 'Émilie', 'François'];
const lastNames = ['Dubois', 'Martin', 'Lefèvre', 'Bernard'];

const sortedFirstNames = firstNames.sort(collator.compare);
const sortedLastNames = lastNames.sort(collator.compare);

Ce modèle fonctionne bien lors de la création de vues de tableaux ou d'autres interfaces qui affichent plusieurs listes triées.

Comment spécifier la locale

Les deux localeCompare() et Intl.Collator acceptent un identifiant de locale comme premier argument. Cet identifiant utilise le format BCP 47, combinant généralement un code de langue et un code de région optionnel.

const names = ['Åsa', 'Anna', 'Ärla'];

// Swedish locale
const swedishSorted = names.sort((a, b) => a.localeCompare(b, 'sv-SE'));
console.log(swedishSorted);
// Output: ['Anna', 'Åsa', 'Ärla']

// German locale
const germanSorted = names.sort((a, b) => a.localeCompare(b, 'de-DE'));
console.log(germanSorted);
// Output: ['Anna', 'Ärla', 'Åsa']

La locale détermine les règles de classement à appliquer. Le suédois et l'allemand ont des règles différentes pour å et ä, ce qui produit des ordres de tri différents.

Vous pouvez omettre la locale pour utiliser la locale par défaut de l'utilisateur depuis le navigateur.

const collator = new Intl.Collator();
const names = ['Åsa', 'Anna', 'Ärla'];
const sorted = names.sort(collator.compare);

Cette approche respecte les préférences linguistiques de l'utilisateur sans coder en dur une locale spécifique. L'ordre de tri correspondra à ce que l'utilisateur attend en fonction des paramètres de son navigateur.

Vous pouvez également passer un tableau de locales pour fournir des options de repli.

const collator = new Intl.Collator(['sv-SE', 'sv', 'en-US']);

L'API utilise la première locale prise en charge du tableau. Si le suédois de Suède n'est pas disponible, elle essaie le suédois générique, puis se replie sur l'anglais américain.

Comment contrôler la sensibilité à la casse

L'option sensitivity détermine comment la comparaison traite les différences de casse et d'accents. Elle accepte quatre valeurs : base, accent, case et variant.

La sensibilité base ignore à la fois la casse et les accents, en comparant uniquement les caractères de base.

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

console.log(collator.compare('a', 'A'));
// Output: 0 (equal)

console.log(collator.compare('a', 'á'));
// Output: 0 (equal)

console.log(collator.compare('a', 'b'));
// Output: -1 (different base characters)

Ce mode traite a, A et á comme identiques car ils partagent le même caractère de base.

La sensibilité accent prend en compte les accents mais ignore la casse.

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

console.log(collator.compare('a', 'A'));
// Output: 0 (equal, case ignored)

console.log(collator.compare('a', 'á'));
// Output: -1 (different, accent matters)

La sensibilité case prend en compte la casse mais ignore les accents.

const collator = new Intl.Collator('en-US', { sensitivity: 'case' });

console.log(collator.compare('a', 'A'));
// Output: -1 (different, case matters)

console.log(collator.compare('a', 'á'));
// Output: 0 (equal, accent ignored)

La sensibilité variant (par défaut) prend en compte toutes les différences.

const collator = new Intl.Collator('en-US', { sensitivity: 'variant' });

console.log(collator.compare('a', 'A'));
// Output: -1 (different)

console.log(collator.compare('a', 'á'));
// Output: -1 (different)

Ce mode fournit la comparaison la plus stricte, traitant toute différence comme significative.

Comment trier des chaînes avec des nombres intégrés

L'option numeric active le tri numérique pour les chaînes contenant des nombres. Lorsqu'elle est activée, la comparaison traite les séquences de chiffres comme des valeurs numériques au lieu de les comparer caractère par caractère.

const files = ['file1.txt', 'file10.txt', 'file2.txt', 'file20.txt'];

// Default sorting (wrong order)
const defaultSorted = [...files].sort();
console.log(defaultSorted);
// Output: ['file1.txt', 'file10.txt', 'file2.txt', 'file20.txt']

// Numeric sorting (correct order)
const collator = new Intl.Collator('en-US', { numeric: true });
const numericSorted = files.sort(collator.compare);
console.log(numericSorted);
// Output: ['file1.txt', 'file2.txt', 'file10.txt', 'file20.txt']

Sans tri numérique, les chaînes sont triées caractère par caractère. La chaîne 10 vient avant 2 car le premier caractère 1 a un point de code inférieur à 2.

Avec le tri numérique activé, le collateur reconnaît 10 comme le nombre dix et 2 comme le nombre deux. Cela produit l'ordre de tri attendu où 2 vient avant 10.

Cette option est utile pour trier les noms de fichiers, les numéros de version ou toute chaîne qui mélange texte et nombres.

const versions = ['v1.10', 'v1.2', 'v1.20', 'v1.3'];
const collator = new Intl.Collator('en-US', { numeric: true });
const sorted = versions.sort(collator.compare);
console.log(sorted);
// Output: ['v1.2', 'v1.3', 'v1.10', 'v1.20']

Comment contrôler quelle casse vient en premier

L'option caseFirst détermine si les lettres majuscules ou minuscules sont triées en premier lors de la comparaison de chaînes qui ne diffèrent que par la casse. Elle accepte trois valeurs : upper, lower ou false.

const words = ['apple', 'Apple', 'APPLE'];

// Uppercase first
const upperFirst = new Intl.Collator('en-US', { caseFirst: 'upper' });
const upperSorted = [...words].sort(upperFirst.compare);
console.log(upperSorted);
// Output: ['APPLE', 'Apple', 'apple']

// Lowercase first
const lowerFirst = new Intl.Collator('en-US', { caseFirst: 'lower' });
const lowerSorted = [...words].sort(lowerFirst.compare);
console.log(lowerSorted);
// Output: ['apple', 'Apple', 'APPLE']

// Default (locale-dependent)
const defaultCase = new Intl.Collator('en-US', { caseFirst: 'false' });
const defaultSorted = [...words].sort(defaultCase.compare);
console.log(defaultSorted);
// Output depends on locale

La valeur false utilise l'ordre de casse par défaut de la locale. La plupart des locales traitent les chaînes qui ne diffèrent que par la casse comme égales lors de l'utilisation des paramètres de sensibilité par défaut.

Cette option n'a d'effet que lorsque l'option sensitivity permet aux différences de casse d'avoir de l'importance.

Comment ignorer la ponctuation dans le tri

L'option ignorePunctuation indique au collateur d'ignorer les signes de ponctuation lors de la comparaison de chaînes. Cela peut être utile lors du tri de titres ou de phrases qui peuvent ou non inclure de la ponctuation.

const titles = [
  'The Old Man',
  'The Old-Man',
  'The Oldman',
];

// Default (punctuation matters)
const defaultCollator = new Intl.Collator('en-US');
const defaultSorted = [...titles].sort(defaultCollator.compare);
console.log(defaultSorted);
// Output: ['The Old Man', 'The Old-Man', 'The Oldman']

// Ignore punctuation
const noPunctCollator = new Intl.Collator('en-US', { ignorePunctuation: true });
const noPunctSorted = [...titles].sort(noPunctCollator.compare);
console.log(noPunctSorted);
// Output: ['The Old Man', 'The Old-Man', 'The Oldman']

Lorsque la ponctuation est ignorée, la comparaison traite le trait d'union dans « Old-Man » comme s'il n'existait pas, faisant en sorte que les chaînes se comparent comme si elles étaient toutes « TheOldMan ».

Tri des noms d'utilisateurs de différents pays

Lors du tri de noms d'utilisateurs du monde entier, utilisez la locale préférée de l'utilisateur pour respecter ses attentes linguistiques.

const userLocale = navigator.language;
const collator = new Intl.Collator(userLocale);

const users = [
  { name: 'Müller', country: 'Germany' },
  { name: 'Martin', country: 'France' },
  { name: 'Andersson', country: 'Sweden' },
  { name: 'García', country: 'Spain' },
];

const sorted = users.sort((a, b) => collator.compare(a.name, b.name));

sorted.forEach(user => {
  console.log(`${user.name} (${user.country})`);
});

Ce code détecte la locale de l'utilisateur depuis le navigateur et trie les noms en conséquence. Un utilisateur allemand voit la liste triée selon les règles allemandes, tandis qu'un utilisateur suédois la voit triée selon les règles suédoises.

Tri avec changement de locale

Lorsque votre application permet aux utilisateurs de changer de langue, mettez à jour le collator lorsque la locale change.

let currentLocale = 'en-US';
let collator = new Intl.Collator(currentLocale);

function setLocale(newLocale) {
  currentLocale = newLocale;
  collator = new Intl.Collator(currentLocale);
}

function sortItems(items) {
  return items.sort(collator.compare);
}

// User switches to Swedish
setLocale('sv-SE');
const names = ['Åsa', 'Anna', 'Örjan'];
console.log(sortItems(names));
// Output: ['Anna', 'Åsa', 'Örjan']

// User switches to German
setLocale('de-DE');
const germanNames = ['Über', 'Uhr', 'Udo'];
console.log(sortItems(germanNames));
// Output: ['Udo', 'Uhr', 'Über']

Ce pattern garantit que les listes triées se mettent à jour pour correspondre à la langue sélectionnée par l'utilisateur.

Choisir entre localeCompare et Intl.Collator

Utilisez localeCompare() lorsque vous avez besoin d'une comparaison ponctuelle rapide ou que vous triez un petit tableau de moins de 100 éléments. La syntaxe plus simple est plus facile à lire et la différence de performance est négligeable pour les petits ensembles de données.

const items = ['banana', 'Apple', 'cherry'];
const sorted = items.sort((a, b) => a.localeCompare(b, 'en-US'));

Utilisez Intl.Collator lors du tri de grands tableaux, de l'exécution de nombreuses comparaisons ou du tri de plusieurs tableaux avec la même locale et les mêmes options. Créer le collator une fois et le réutiliser offre de meilleures performances.

const collator = new Intl.Collator('en-US', { sensitivity: 'base', numeric: true });

const products = [/* large array */];
const sorted = products.sort(collator.compare);

Les deux approches produisent les mêmes résultats. Le choix dépend de vos exigences de performance et de vos préférences d'organisation du code.