Comment trier correctement les chaînes contenant des nombres

Utilisez la collation numérique pour trier les noms de fichiers, les numéros de version et autres chaînes contenant des nombres dans un ordre naturel

Introduction

Lorsque vous triez des chaînes contenant des nombres, vous vous attendez à ce que file1.txt, file2.txt et file10.txt apparaissent dans cet ordre. Cependant, la comparaison de chaînes par défaut de JavaScript produit file1.txt, file10.txt, file2.txt à la place. Cela se produit parce que les chaînes sont comparées caractère par caractère, et le caractère 1 dans 10 vient avant le caractère 2.

Ce problème apparaît chaque fois que vous triez des noms de fichiers, des numéros de version, des adresses, des codes de produits ou toute autre chaîne contenant des nombres intégrés. L'ordre incorrect confond les utilisateurs et rend les données difficiles à naviguer.

JavaScript fournit l'API Intl.Collator avec une option numérique qui résout ce problème. Cette leçon explique comment fonctionne le classement numérique, pourquoi la comparaison de chaînes par défaut échoue, et comment trier des chaînes avec des nombres intégrés dans un ordre numérique naturel.

Qu'est-ce que le classement numérique

Le classement numérique est une méthode de comparaison qui traite les séquences de chiffres comme des nombres plutôt que comme des caractères individuels. Lors de la comparaison de chaînes, le collateur identifie les séquences de chiffres et les compare par leur valeur numérique.

Lorsque le classement numérique est désactivé, la chaîne file10.txt vient avant file2.txt car la comparaison caractère par caractère trouve que 1 vient avant 2 à la première position différente. Le collateur ne considère jamais que 10 représente un nombre supérieur à 2.

Lorsque le classement numérique est activé, le collateur reconnaît 10 et 2 comme des nombres complets et les compare numériquement. Puisque 10 est supérieur à 2, file2.txt vient correctement avant file10.txt.

Ce comportement produit ce qu'on appelle le tri naturel ou l'ordre naturel, où les chaînes contenant des nombres se trient comme les humains s'y attendent plutôt que strictement par ordre alphabétique.

Pourquoi la comparaison de chaînes par défaut échoue pour les nombres

La comparaison de chaînes par défaut en JavaScript utilise un ordre lexicographique, qui compare les chaînes caractère par caractère de gauche à droite en utilisant les valeurs des points de code Unicode. Cela fonctionne correctement pour le texte alphabétique mais produit des résultats inattendus pour les nombres.

Considérons comment la comparaison lexicographique traite ces chaînes :

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

console.log(files);
// Résultat : ['file1.txt', 'file10.txt', 'file2.txt', 'file20.txt']

La comparaison examine chaque position de caractère indépendamment. À la première position différente après file, elle compare 1 à 2. Puisque 1 a une valeur Unicode inférieure à 2, toute chaîne commençant par file1 vient avant toute chaîne commençant par file2, indépendamment de ce qui suit.

Cela produit la séquence file1.txt, file10.txt, file2.txt, file20.txt, qui ne correspond pas aux attentes humaines concernant l'ordre numérique.

Utilisation d'Intl.Collator avec l'option numeric

Le constructeur Intl.Collator accepte un objet d'options avec une propriété numeric. Définir numeric: true active la collation numérique, ce qui fait que le collateur compare les séquences de chiffres par leur valeur numérique.

const collator = new Intl.Collator('en-US', { numeric: true });
const files = ['file1.txt', 'file10.txt', 'file2.txt', 'file20.txt'];

files.sort(collator.compare);

console.log(files);
// Résultat : ['file1.txt', 'file2.txt', 'file10.txt', 'file20.txt']

La méthode compare du collateur renvoie un nombre négatif lorsque le premier argument doit venir avant le second, zéro lorsqu'ils sont égaux, et un nombre positif lorsque le premier doit venir après le second. Cela correspond à la signature attendue par la méthode Array.sort() de JavaScript.

Le résultat trié place les fichiers dans un ordre numérique naturel. Le collateur reconnaît que 1 < 2 < 10 < 20, produisant la séquence attendue par les humains.

Tri de chaînes alphanumériques mixtes

Le tri numérique traite les chaînes où les nombres apparaissent à n'importe quelle position, pas seulement à la fin. Le comparateur compare les portions alphabétiques normalement et les portions numériques numériquement.

const collator = new Intl.Collator('en-US', { numeric: true });
const addresses = ['123 Oak St', '45 Oak St', '1234 Oak St', '5 Oak St'];

addresses.sort(collator.compare);

console.log(addresses);
// Résultat: ['5 Oak St', '45 Oak St', '123 Oak St', '1234 Oak St']

Le comparateur identifie les séquences de chiffres au début de chaque chaîne et les compare numériquement. Il reconnaît que 5 < 45 < 123 < 1234, même si la comparaison lexicographique produirait un ordre différent.

Tri des numéros de version

Les numéros de version sont un cas d'utilisation courant pour le tri numérique. Les versions logicielles comme 1.2.10 devraient venir après 1.2.2, mais la comparaison lexicographique produit un ordre incorrect.

const collator = new Intl.Collator('en-US', { numeric: true });
const versions = ['1.2.10', '1.2.2', '1.10.5', '1.2.5'];

versions.sort(collator.compare);

console.log(versions);
// Résultat: ['1.2.2', '1.2.5', '1.2.10', '1.10.5']

Le comparateur compare correctement chaque composant numérique. Dans la séquence 1.2.2, 1.2.5, 1.2.10, il reconnaît que le troisième composant augmente numériquement. Dans 1.10.5, il reconnaît que le deuxième composant est 10, qui est supérieur à 2.

Travailler avec des codes produits et des identifiants

Les codes produits, numéros de facture et autres identifiants mélangent souvent des lettres et des chiffres. Le tri numérique garantit qu'ils sont classés dans un ordre logique.

const collator = new Intl.Collator('en-US', { numeric: true });
const products = ['PROD-1', 'PROD-10', 'PROD-2', 'PROD-100'];

products.sort(collator.compare);

console.log(products);
// Résultat: ['PROD-1', 'PROD-2', 'PROD-10', 'PROD-100']

Le préfixe alphabétique PROD- correspond dans toutes les chaînes, donc le comparateur compare le suffixe numérique. Le résultat reflète l'ordre numérique croissant plutôt que l'ordre lexicographique.

Tri avec différentes locales

L'option numeric fonctionne avec n'importe quelle locale. Bien que différentes locales puissent avoir des règles de tri différentes pour les caractères alphabétiques, le comportement de comparaison numérique reste cohérent.

const enCollator = new Intl.Collator('en-US', { numeric: true });
const deCollator = new Intl.Collator('de-DE', { numeric: true });

const items = ['item1', 'item10', 'item2'];

console.log(items.sort(enCollator.compare));
// Résultat: ['item1', 'item2', 'item10']

console.log(items.sort(deCollator.compare));
// Résultat: ['item1', 'item2', 'item10']

Les deux locales produisent le même résultat car les chaînes ne contiennent que des caractères ASCII et des chiffres. Lorsque les chaînes incluent des caractères spécifiques à une locale, la comparaison alphabétique suit les règles de la locale tandis que la comparaison numérique reste cohérente.

Comparer des chaînes sans tri

Vous pouvez utiliser la méthode compare du collator directement pour déterminer la relation entre deux chaînes sans trier un tableau entier.

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

console.log(collator.compare('file2.txt', 'file10.txt'));
// Résultat: -1 (un nombre négatif signifie que le premier argument vient avant le second)

console.log(collator.compare('file10.txt', 'file2.txt'));
// Résultat: 1 (un nombre positif signifie que le premier argument vient après le second)

console.log(collator.compare('file2.txt', 'file2.txt'));
// Résultat: 0 (zéro signifie que les arguments sont égaux)

Ceci est utile lorsque vous devez vérifier l'ordre sans modifier un tableau, comme lors de l'insertion d'un élément dans une liste triée ou pour vérifier si une valeur se situe dans une plage.

Comprendre la limitation avec les nombres décimaux

La collation numérique compare des séquences de chiffres, mais elle ne reconnaît pas les points décimaux comme faisant partie des nombres. Le caractère point est traité comme un séparateur, et non comme un séparateur décimal.

const collator = new Intl.Collator('en-US', { numeric: true });
const measurements = ['0.5', '0.05', '0.005'];

measurements.sort(collator.compare);

console.log(measurements);
// Résultat: ['0.005', '0.05', '0.5']

Le collator traite chaque mesure comme trois composants numériques distincts : la partie avant le point, le point lui-même, et la partie après le point. Il compare 0 contre 0 (égal), puis compare les parties après le point comme des nombres séparés : 5, 5 et 5 (égal). Ensuite, il compare la deuxième décimale : rien, 5 et rien. Cela produit un ordre incorrect pour les nombres décimaux.

Pour trier des nombres décimaux, convertissez-les en nombres réels et triez-les numériquement, ou utilisez le remplissage de chaînes pour assurer un ordre lexicographique correct.

Combiner la collation numérique avec d'autres options

L'option numeric fonctionne conjointement avec d'autres options de collation comme sensitivity et caseFirst. Vous pouvez contrôler comment le collateur gère la casse et les accents tout en maintenant le comportement de comparaison numérique.

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

const items = ['Item1', 'item10', 'ITEM2'];

items.sort(collator.compare);

console.log(items);
// Résultat: ['Item1', 'ITEM2', 'item10']

L'option sensitivity: 'base' rend la comparaison insensible à la casse. Le collateur traite Item1, item1, et ITEM1 comme équivalents tout en comparant correctement les portions numériques.

Réutiliser les collateurs pour améliorer les performances

La création d'une nouvelle instance Intl.Collator implique le chargement des données de locale et le traitement des options. Lorsque vous devez trier plusieurs tableaux ou effectuer de nombreuses comparaisons, créez le collateur une seule fois et réutilisez-le.

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

const files = ['file1.txt', 'file10.txt', 'file2.txt'];
const versions = ['1.2.10', '1.2.2', '1.10.5'];
const products = ['PROD-1', 'PROD-10', 'PROD-2'];

files.sort(collator.compare);
versions.sort(collator.compare);
products.sort(collator.compare);

Cette approche est plus efficace que de créer un nouveau collateur pour chaque opération de tri. La différence de performance devient significative lors du tri de nombreux tableaux ou de l'exécution fréquente de comparaisons.