Le piège de Array#sort
Par Delicious Insights • Publié le 29 août 2014

Bien sûr, les tableaux JavaScript savent déjà se trier : on a une méthode sort() pour ça. Ça marche bien.

Ou pas.

Revoyons la scène au ralenti

var ages = [18, 21, 9, 41, 35, 24]
ages.sort()
// => [18, 21, 24, 35, 41, 9]

NoooOoOOOoOOOOoo!

Eeeeeh si.

LOLWUT?

Si on lit la doc (ce n'est pas un gros mot), on s'aperçoit que…

Le tableau est trié selon la valeur de point de code Unicode de chaque caractère, d'après la conversion en chaine de caractères de chaque élément.

Ah ben voilà.

JavaScript étant dynamiquement typé, on n'a aucune garantie que les cellules du tableau renferment un type homogène, et même ainsi, aucune garantie que ce type ait un ordre naturel (qu'il réponde à < par exemple).

Du coup, Array traite tous les contenus, par défaut, comme le seul type vers lequel tous les autres ont un protocole de conversion : String. En gros, l'exemple de tout à l'heure est identique, fonctionnellement, à celui-ci :

var ages = ['18', '21', '9', '41', '35', '24']
ages.sort()
// => ['18', '21', '24', '35', '41', '9']

Sauf que là, ça ne vous choque pas (ou moins).

Alors je fais comment ?

Qu'à cela ne tienne Étienne, tu fournis ton propre comparateur externe, c'est-à-dire une fonction qui prend deux arguments--appelons-les a et b--et renvoie une valeur signée, avec la sémantique classique :

  • Du négatif signifie que a apparaît avant b
  • Du neutre (0) signifie qu'ils sont équivalents
  • Du positif signifie que a apparaît après b

Pour un tableau numérique, il nous suffit donc de produire le signe nous-mêmes :

var ages = [18, 21, 9, 41, 35, 24]
ages.sort(function(a, b) {
  return a - b
})
// => [9, 18, 21, 24, 35, 41]

Voilà qui est mieux. On peut bien sûr en faire une fonction utilitaire :

function numSort(arr) {
  return arr.sort(function(a, b) {
    return +a - +b
  })
}
numSort(ages)

(Les + préfixes garantissent le traitement en tant que Number des valeurs des cellules, comme ça, on trie numériquement même quand on a des String dans le tas, par exemple).

Si vous aimez l'approche « j'étends les types natifs », ça se fait bien aussi (j'ai ça dans tous mes projets) :

Array.prototype.numSort = function numSort() {
  return this.sort(function(a, b) {
    return +a - +b
  })
}
ages.numSort()

Du coup on est bons ?

Pour du numérique, oui. Si vous voulez trier correctement des textes, c'est autre chose. Regardez plutôt :

var names = ['alice', 'Bob', 'Claire', 'David', 'Élodie', 'Stuart', 'Stéphane']
names.sort()
// => ["Bob", "Claire", "David", "Stuart", "Stéphane", "alice", "Élodie"]

Ah. Voyez-vous, comme le disait la doc juste au-dessus, on compare les codepoints Unicode ; on parle de tri lexicographique.

Mais Unicode est une extension des jeux de caractère ISO, dans lesquels, pour des raisons historiques (toujours elles…) :

  • Les majuscules sont avant les minuscules
  • Les caractères nus sont avant les diacritiques (accentués, etc.)
  • Les caractères simples sont avant les ligatures (genre _œ_ ou _æ_)
  • Et plein d'autres classements débiles

Du coup, je vois plein de gens faire un putain d'aller-retour Ajax juste histoire de trier côté serveur une liste qui existait intégralement côté client ! Comme dirait un ami : « What the actual fuck? »

C'est d'autant plus dommage que String a depuis longtemps (ES3) une méthode localeCompare, laquelle est un comparateur interne qui renvoie une valeur signée, en respectant la locale (pour simplifier, disons la langue) active. Celle-ci n'est pas nécessairement celle des données ou même du document, mais en pratique ça marche plutôt bien tout le temps.

var names = ['alice', 'Bob', 'Claire', 'David', 'Élodie', 'Stuart', 'Stéphane']
names.sort(function(a, b) {
  return a.localeCompare(b)
})
// => ["alice", "Bob", "Claire", "David", "Élodie", "Stéphane", "Stuart"]

Aaaah, beaucoup mieux. Comme toujours, rien ne vous empêche d'étendre Array si, comme moi, vous aimez ce genre d'approche :

Array.prototype.localeSort = function localeSort() {
  return this.sort(function(a, b) {
    return a.localeCompare(b)
  })
}
names.localeSort()

Bons tris à tous !