Skip to content

Trier une liste d'objets selon un champ snippet

Trier une liste d'objets selon un champ — le tri du quotidien.

Trier une liste d'objets selon un champ — le tri du quotidien. Le piège, c'est la discipline du comparateur : un comparateur doit être un ordre total (renvoyer 0 pour les égaux, cohérent dans les deux sens), sinon les implémentations de tri de tous les langages corrompent silencieusement la sortie ou bouclent à l'infini. Le second piège, c'est la stabilité : les lignes de même clé conservent leur ordre d'entrée seulement si le tri est stable (Timsort en Python/Java, stable_sort en C++) — sort.Slice de Go n'est PAS stable, sort.SliceStable l'est ; et le tri de chaînes sensible à la locale (noms accentués) exige un collator, pas une comparaison d'octets brute.

Recette exécutable · 12 langagesOpen the sort-lines tool →
Files & Datasortcomparatororderingstabilityarrayrecords

Every language

12 langages, copy-ready. One at a time with syntax highlighting, or all inline.

JSJavaScript
// copy first — sort MUTATES the receiver:
const sorted = [...items].sort((a, b) => a.name.localeCompare(b.name));

// machine keys, or when locale rules are wrong for the data:
const byTitle = [...items].sort((a, b) => (a.title < b.title ? -1 : a.title > b.title ? 1 : 0));

// multi-key tiebreak: 0 (equal) falls through to the next comparator
const byTitleThenDate = [...items].sort(
  (a, b) => a.title.localeCompare(b.title) || new Date(a.date) - new Date(b.date),
);

Array.prototype.sort MUTATES — spread into a new array first (or use toSorted(), ES2023) or the input order is silently gone. localeCompare for human-facing strings (it handles accents and locale rules); the < / > ? -1 : 1 form for machine keys. The || chain is the tiebreak idiom: only 0 falls through to the next key.

TSTypeScript
interface Item {
  title: string;
  date: string; // ISO — strings compare chronologically
}

const byTitle = (a: Item, b: Item): number => a.title.localeCompare(b.title);

// multi-key: compose comparators with || — zero (equal) falls through
const byTitleThenDate = (a: Item, b: Item): number =>
  byTitle(a, b) || b.date.localeCompare(a.date); // newest first on ties

The typed comparator (a: Item, b: Item) => number is the whole contract — return a negative for less, 0 for equal, positive for greater; any magnitude works. Keep comparators as named pure functions: they compose with || (0 is falsy), unit-test trivially, and reuse across sort call sites.

GoGo
import "sort"

// fast — but NOT stable: equal-Title rows may arrive in any order
sort.Slice(items, func(i, j int) bool {
	return items[i].Title < items[j].Title
})

// equal-key rows keep their input order:
sort.SliceStable(items, func(i, j int) bool {
	return items[i].Title < items[j].Title
})

sort.Slice is NOT stable — sort.SliceStable is the one that preserves input order for equal keys; pick deliberately, not by habit. The contract is a less-than bool, not a three-way int. Go 1.21+ adds the generic slices.SortFunc / slices.SortStableFunc (slices package) as the modern API — same semantics.

RsRust
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct Item {
    title: String,
}

// stable — equal-key rows keep their input order
items.sort_by(|a, b| a.title.cmp(&b.title));

// multi-key: tuples compare field-by-field, the free tiebreak chain
items.sort_by(|a, b| (&a.last, &a.first).cmp(&(&b.last, &b.first)));

// faster, no stability guarantee — fine when keys are unique
items.sort_unstable_by(|a, b| a.title.cmp(&b.title));

sort_by is stable; sort_unstable_by is faster — choose on purpose. Deriving PartialOrd/Ord on the struct gives the free a < b ordering; a.title.cmp(&b.title) returns the three-way Ordering (Less/Equal/Greater), and tuple comparison is the multi-key idiom with no chaining needed.

PHPPHP
// <=> (spaceship) is the comparator primitive: -1, 0, or 1, no branches
usort($items, fn($a, $b) => $a['title'] <=> $b['title']);

// multi-key: compare fields left to right until one differs
usort($items, fn($a, $b) => [$a['last'], $a['first']] <=> [$b['last'], $b['first']]);

// usort re-indexes numerically — make the reset explicit, not accidental:
$items = array_values($items);

usort resets the keys: the result is re-indexed 0..n-1 and any string keys are lost — array_values() documents that intent. uasort preserves keys, uksort sorts by them. PHP's sort is stable since PHP 8.0 (pre-8.0 it was not — a real upgrade trap for equal-key rows).

PyPython
sorted_items = sorted(items, key=lambda x: x['title'])  # new list

# multi-key via tuple — ties break on the second field:
by_last_first = sorted(items, key=lambda x: (x['last'], x['first']))

# descending: reverse=True, not negation (only works on numbers)
by_title_desc = sorted(items, key=lambda x: x['title'], reverse=True)

# items.sort(key=...) sorts in place instead

Timsort is stable — equal-key rows keep input order — and sorted() returns a new list while list.sort() mutates. The tuple key (x['last'], x['first']) is the multi-key idiom. reverse=True beats key negation (impossible on strings); for mixed directions use two passes, sorting by the LAST key first — stability makes it work.

C#C#
// LINQ — lazy, stable, never mutates the source:
var sorted = items.OrderBy(i => i.Title)
                  .ThenBy(i => i.Date);

// in place, on List<T> — UNSTABLE:
items.Sort((a, b) => string.CompareOrdinal(a.Title, b.Title));

List<T>.Sort is UNSTABLE — equal-key rows can reorder; LINQ's OrderBy is documented stable and never mutates the source, but it is lazy — nothing sorts until enumeration. string.CompareOrdinal for machine keys; string.Compare(strA, strB, cultureInfo) when the sort is human-facing (accents, locale rules).

JvJava
import java.util.Comparator;

items.sort(Comparator.comparing(Item::title)
                      .thenComparing(Item::date));

// nullable field — decide where the nulls land, or get an NPE mid-sort:
items.sort(Comparator.comparing(
        Item::title,
        Comparator.nullsLast(Comparator.naturalOrder())));

Comparator.comparing(...).thenComparing(...) replaced the old anonymous-class comparators — the chain reads in sort order and each stage has a Descending twin. List.sort and Stream.sorted run TimSort: stable. comparing with a second argument is where nullsLast/nullsFirst goes; without it a null key throws NullPointerException during the sort.

SwSwift
let sorted = items.sorted { $0.title < $1.title }

// explicit multi-key tiebreak:
let full = items.sorted {
    $0.title == $1.title ? $0.date < $1.date : $0.title < $1.title
}

// mutating form, on a var array:
var mutable = items
mutable.sort { $0.title < $1.title }

sorted(by:) returns a new array; sort() mutates in place. The stdlib documents the sort as guaranteed stable (an adaptive merge sort), but an explicit tiebreak chain still reads clearer than leaning on it — tuples ($0.title, $0.date) < ($1.title, $1.date) also compare lexicographically. Conform the element to Comparable and the closure-free sorted() comes free.

KtKotlin
val sorted = items.sortedBy { it.title } // RETURNS a new list

// multi-key, mixed directions:
val full = items.sortedWith(
    compareBy<Item> { it.title }.thenByDescending { it.date })

// in place, on a MutableList — returns Unit, not a list:
items.sortBy { it.title }

The trap is the two spellings: sortedBy/sortedWith RETURN a new list; sortBy/sortWith MUTATE in place and return Unit — assigning the result of sortBy compiles to Unit and silently loses the sorted reference. compareBy({ it.title }, { it.date }) is the multi-key vararg form. Uses the same stable TimSort as Java.

RbRuby
# key block — the key is computed once per element:
sorted = items.sort_by { |i| i[:title] }

# array keys compare element-wise — the multi-key form:
by_last_first = items.sort_by { |i| [i[:last], i[:first]] }

# two-arg comparator block, when you need full control (descending on ties):
items.sort { |a, b| [a[:title], b[:date]] <=> [b[:title], a[:date]] }

sort_by takes a KEY block (decorate-sort-undecorate — the key runs once per element); sort takes a two-arg comparator block on <=>, the three-way operator. Both return a new array (sort! / sort_by! mutate in place). MRI's sort is stable in practice but the spec does not promise it — add an explicit tiebreak when equal-key order matters.

ZigZig
const std = @import("std");

const Item = struct { title: []const u8 };

fn byTitle(_: void, a: Item, b: Item) bool {
    return std.mem.lessThan(u8, a.title, b.title);
}

// stable; the context arg ({} here) is mandatory even when unused:
std.mem.sort(Item, items, {}, byTitle);

// when equal-key order does not matter and speed does:
std.sort.pdq(Item, items, {}, byTitle);

The comparator is a comptime fn of (context, a, b) bool — less-than, not three-way — and the context parameter is mandatory even when you ignore it ({} for void). std.mem.sort is the stable block sort; std.sort.pdq / std.sort.insertion are the named unstable choices. lessThan is byte-wise: no collation, no case folding — normalize before sorting human strings.

Keep going

Try the interactive sort-lines tool →