← Fundamentals
Guided lesson 6 questions · 6 languages

Ordering, sorting, and comparators

Before, after, or either: what a sort asks, and what it trusts.

A comparator is a function that tells a sort which of two values comes first: a negative answer, zero, or a positive answer, or in some languages Less, Equal, or Greater. The sort trusts every answer, so the answers must fit together. Two values that compare equal keep their earlier order only in a stable sort, one that promises to keep them that way.

Before you begin

Why this matters

Sorting looks like one call, but the order comes from a function you write, or from a default you did not choose. Knowing both lets you predict the order a sort produces.

What this helps you reason about

  • Predict the order sort() gives for numbers, and write the compare function that fixes it.
  • Sort by a second key, or newest first, without losing the order of equal elements.
  • Spot a compare function that breaks the rules a sort relies on.

A library puts classic novels on a display table, oldest first. To sort them, the program needs a small function that compares two books. This lesson follows what that function promises, what sort() does without it, and what happens to two books from the same year.

Already know this? The short version
  • sort() with no argument converts every element to a string and compares character codes, so [9, 10, 100] becomes [10, 100, 9]; pass (a, b) => a - b for numbers.
  • A compare function returns a negative number, 0, or a positive number; only the sign counts. For several keys, compare the next key only when the first gives 0; for newest first, swap a and b instead of calling reverse(), which also reverses ties.
  • Array.prototype.sort is stable since ES2019: elements that compare equal keep their order.
  • The compare function must be consistent, antisymmetric, and transitive. One that never returns 0 breaks the contract, and the result then depends on the engine. a - b is safe for numbers, which do not overflow, but NaN makes it compare equal to everything.

Go to the final check· Go to the practice tickets

Words in this lesson (5)
compare function
A function passed to sort that receives two elements and returns a negative number when the first should come first, a positive number when the second should, and 0 when their order does not matter.
character code
The number a program stores for a character. The digit characters 0 to 9 have the codes 48 to 57, so the character 7 is stored as 55, not 7.
arrow function
A function written as a value, without a name, such as () => "stall " + i. The part before => lists its parameters, and the part after it is the value it returns.
stable sort
A sort that keeps elements that compare equal in the order they had before the sort.
comparator contract
The rules every comparator must follow: the same two elements always give the same answer; swapping them reverses the answer, and equal stays equal; and if a comes before b and b before c, then a comes before c.

Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.

01 / Compare

What does a comparator answer?

To sort, a program compares two elements at a time. A compare function, also called a comparator, answers for one pair. It receives two elements, a and b. It returns a negative number when a should come first, a positive number when b should come first, and 0 when neither has to come first.

sort(byYear) calls byYear many times, on pairs that sort chooses, and moves the elements by the sign of each answer. Only the sign counts: -150 and -1 mean the same. For two numbers, a - b has exactly the right sign. sort changes the array itself, and also returns it.

main.ts
function byYear(a: number, b: number): number {
  return a - b;
}

const years = [1965, 1815, 1901];
years.sort(byYear);
console.log(years); // [ 1815, 1901, 1965 ]

A comparator answers one question about two elements: first, second, or either. The sort does the rest.

Coming from Python: Python’s sort takes a key function of one element; TypeScript’s sort takes a compare function of two elements and has no key parameter.

Experiment

Watch sort call the compare function

The sort decides which pairs to compare. The comparator decides only the order of each pair. Swapping a and b swaps every answer, so the whole order turns around.

Why it matters To change the order of a sort, change the comparator. Do not depend on which pairs it receives, or in which order.

Predict the order of years after the sort. Then change a - b to b - a and predict again.

function byYear(a: number, b: number): number {
  const order = a - b;
  return order;
}

const years = [1965, 1815, 1901];
years.sort(byYear);
console.log(years);
What to look for

Step into the sort line. A new frame, byYear, appears on top with its own a and b, such as a=1815, b=1965. order shows "not initialized yet" until its line runs, then -150. byYear runs four times here, with pairs that sort chooses. years still shows [1965, 1815, 1901] during the calls: the panel shows the new order only after sort has finished. The run prints [ 1815, 1901, 1965 ]. After the edit, every order has the opposite sign, and the run prints [ 1965, 1901, 1815 ].

Expected output

[ 1815, 1901, 1965 ]

02 / Default

What order does a sort use without a comparator?

Called with no compare function, sort() turns every element into a string and compares the strings character by character, by their character codes. For words, that is often what you want. For numbers, it is not: as text, "10" comes before "9", because the character 1 comes before the character 9. So [9, 10, 100].sort() gives [10, 100, 9].

Character codes also put every capital letter before every small letter, so "Dune" and "Stoner" come before "emma".

To sort numbers, always pass a compare function, such as the arrow function (a, b) => a - b.

main.ts
const shelves = [9, 10, 100];
shelves.sort();
console.log(shelves); // [ 10, 100, 9 ]
shelves.sort((a, b) => a - b);
console.log(shelves); // [ 9, 10, 100 ]

sort() without a compare function sorts text, even when the array holds numbers.

Coming from Java: Java’s Collections.sort sorts Integers as numbers; TypeScript’s sort() with no argument sorts the same numbers as text.

Precisely: words for people

a.localeCompare(b) compares two strings the way a dictionary of the language does, so "emma" and "Emma" end up next to each other. Its order depends on the language setting; new Intl.Collator("en") fixes one. sort() also moves undefined elements to the end without calling the compare function. Inference, coercion, and conversion, a later lesson, shows sort() among other places where JavaScript converts a value without asking.

Experiment

Sort numbers with no compare function

With no compare function, sort turns 9, 10, and 100 into the strings "9", "10", and "100" and compares them character by character. "1" comes before "9", so both "10" and "100" come before "9".

Why it matters Pass a compare function whenever you sort numbers in TypeScript.

Predict the order of shelves after shelves.sort(). Then change shelves.sort() to shelves.sort((a, b) => a - b) and predict again.

const shelves = [9, 10, 100];
shelves.sort();
console.log(shelves);
What to look for

Step past the sort line. shelves changes from [9, 10, 100] to [10, 100, 9], and the run prints [ 10, 100, 9 ]. No frame appears during the sort: there is no compare function of yours to call. After the edit, the stepper stops in a new frame, arrow function (line 2), with its own a and b, such as a=10, b=9, and the run prints [ 9, 10, 100 ].

Expected output

[ 10, 100, 9 ]

03 / Keys

How do you sort by several keys, or in reverse?

To sort by several keys, compare by the first key, and move on to the next key only when the first gives 0. byYearThenTitle below orders books by year, and books from the same year by title. Comparing two strings with < compares character codes, as sort() does.

To sort newest first, swap a and b in the comparison: b.year - a.year. Do not sort oldest first and then call reverse(). reverse() turns the whole array around, so books from the same year also come out in the opposite order.

main.ts
type Book = { title: string; year: number };

function byYearThenTitle(a: Book, b: Book): number {
  if (a.year !== b.year) {
    return a.year - b.year;
  }
  return a.title < b.title ? -1 : a.title > b.title ? 1 : 0;
}

Compare the first key, and the next key only on a tie. To reverse, swap the two elements in the comparison, not the sorted result.

04 / Ties

What happens to elements that compare equal?

Two elements compare equal when the comparator returns 0 for them: here, two books from the same year. A sort is free to put either one first, unless it is stable. A stable sort keeps elements that compare equal in the order they had before the sort.

TypeScript’s sort is stable: the ECMAScript specification has required it since 2019, so every current engine keeps Stoner before Dune below. This also lets you sort in steps: sort by title, then by year, and books from the same year stay in title order.

main.ts
const shelf: Book[] = [
  { title: "Stoner", year: 1965 },
  { title: "Emma", year: 1815 },
  { title: "Dune", year: 1965 },
];
shelf.sort((a, b) => a.year - b.year);
// Emma 1815, Stoner 1965, Dune 1965

A stable sort keeps equal elements in their earlier order; TypeScript’s sort is stable.

Precisely: before 2019

The 2019 edition of the specification made sort stable. Older engines, such as V8 before version 7.0 (Chrome 70 and Node.js 11), used an unstable sort for arrays of more than 10 elements.

Experiment

Sort two books from the same year

Stoner and Dune compare equal, so the comparator cannot order them. A stable sort keeps them in the order they had in shelf, whichever that was.

Why it matters In a stable sort, the order before the sort decides the order of ties. When ties must come out in one particular order, put the list in that order first, or add a second key.

Predict whether Stoner or Dune is printed first. Then swap the titles "Stoner" and "Dune" in shelf, keeping both years 1965, and predict again.

type Book = { title: string; year: number };

const shelf: Book[] = [
  { title: "Stoner", year: 1965 },
  { title: "Emma", year: 1815 },
  { title: "Dune", year: 1965 },
];
shelf.sort((a, b) => a.year - b.year);
for (const book of shelf) {
  console.log(book.year, book.title);
}
What to look for

Step into the sort line. The comparator runs in a frame named arrow function (line 8), and a and b show two books. At the last call they are a={title: "Dune", year: 1965} and b={title: "Stoner", year: 1965}: the years are equal, so the answer is 0. The stepper can stop on the same line twice; that is normal for this debugger. The run prints 1815 Emma, 1965 Stoner, 1965 Dune. After the edit, Dune is printed before Stoner.

Expected output

1815 Emma
1965 Stoner
1965 Dune

05 / Contract

What must a comparator promise?

A sort calls the comparator on many pairs and trusts every answer, so the answers must fit together. Every comparator must follow three rules, called the comparator contract. The same two elements always give the same answer. Swapping the two elements reverses the answer, and equal stays equal. And if a comes before b, and b comes before c, then a comes before c.

A compare function that breaks a rule gives an order that depends on the engine and on the input. One common mistake breaks the second rule: (a, b) => (a < b ? -1 : 1) never returns 0, so for two equal elements both orders say “b first”. Another, (a, b) => a > b, returns a boolean. TypeScript rejects it, because a compare function must return a number: Type 'boolean' is not assignable to type 'number'.

In several other languages, a - b can overflow: the true difference does not fit in the integer type, and its sign comes out wrong. TypeScript numbers are floating-point, so a - b has the right sign for numbers, as Integers, signs, and overflow explains. NaN is the exception: NaN - 5 is NaN, which sort treats as 0, so NaN compares equal to every number.

main.ts
// Breaks the contract: never says 0.
const careless = (a: number, b: number) => (a < b ? -1 : 1);
// Keeps it: a negative number, 0, or a positive number.
const careful = (a: number, b: number) => a - b;

A comparator must give answers that fit together; a sort cannot check them all, and gives a wrong order instead.

Coming from Java: Java’s a - b comparator can overflow; in TypeScript a - b on numbers cannot, but NaN, undefined, or a string that is not a number gives NaN.

Precisely: sorted collections and equality

A sort uses only the comparator. A sorted map or set, which keeps its keys in order, also treats two keys as the same key when the comparator says they are equal. Then the comparator must agree with equality, as Equality and comparison explains.

06 / Try it

Can you explain a fresh example?

Decide which sort compares text and which compares lengths, and what each does with ties.

main.ts
const codes = [30, 4, 200];
const labels = ["b2", "B1", "a3", "c"];
codes.sort();
labels.sort((a, b) => a.length - b.length);
console.log(codes, labels);

What is printed?

Reveal the reasoning

[ 200, 30, 4 ] [ 'c', 'b2', 'B1', 'a3' ]. With no compare function, sort compares "30", "4", and "200" as text, and "2" comes before "3", which comes before "4". The second sort compares lengths: "c" is shortest, and the three labels of length 2 compare equal, so the stable sort keeps them in their old order.

Practice tickets

  1. ORDER-101 · The pickup screen lists slot 10 before slot 9 → Free
  2. ORDER-102 · A school group that booked later enters before one of the same size → Sign in · Pro
  3. ORDER-103 · Objects with an unknown year appear in the middle of the timeline → Sign in · Pro

Keep exploring

Stack and heap →

Primary references