Ordering a Collection
Sorting means putting the elements of a collection in a certain order.
Some types have a natural order. Numbers are ordered by value: 1 comes before 2. Strings are ordered alphabetically: "four" comes before "one". For everything else, or when you want a different order, you give a custom order: a compare function that looks at two elements and says which one comes first.
Arrays have one sorting method, sort(), and two things about it are easy to miss:
sort()changes the array in place and returns that same array, so the original order is gone. To keep it, sort a copy:[...words].sort().- With no compare function,
sort()compares the string form of the values. That is fine for words, but[10, 9, 2]comes out as[ 10, 2, 9 ], because the text"10"comes before"2". For numbers, always pass a compare function.
A compare function gets two elements, a and b, and returns a number: negative when a comes first, positive when b comes first, and 0 when they are equal. a - b does that for numbers and a.localeCompare(b) does it for strings.
sort() is stable: elements that the compare function calls equal keep their original order.
Newer versions of Node.js also have toSorted() and toReversed(), which return a new array and leave the original alone. The examples below copy the array by hand with [...words], which works everywhere.
The examples below use the words one, two, three, four.
Natural Order
Natural order is the order a type has on its own: numbers by value, strings letter by letter.
[...words].sort() gives a new array in alphabetical order. For descending order there is no separate method: sort first, then turn the array around with reverse(). For numbers, the compare function (a, b) => b - a does it in one step.
Your own object types have no natural order. You give them one by writing a compare function that returns a negative number, zero or a positive number, the same rule as any other compare function. You can then pass it to sort() or use it directly to compare two values.
Sorted copy
[...words].sort() sorts a copy, so words itself is printed unchanged afterwards. The last two lines show why numbers need a compare function: without one, [10, 9, 2] becomes [ 10, 2, 9 ]; with (a, b) => a - b it becomes [ 2, 9, 10 ].
Output: TypeScript file and captured output
const words = ['one', 'two', 'three', 'four']
// sort() changes the array it is called on, so sort a copy of it
const sorted = [...words].sort()
console.log(sorted)
console.log(words)
// with no compare function sort() compares the string form of every value
const numbers = [10, 9, 2]
console.log([...numbers].sort())
console.log([...numbers].sort((a, b) => a - b))
Sorted descending
The copy is sorted, then reverse() flips it, so the words go from "two" down to "four". The numbers show the shorter way for values you can subtract: (a, b) => b - a sorts [10, 9, 2] straight into [ 10, 9, 2 ].
Output: TypeScript file and captured output
const words = ['one', 'two', 'three', 'four']
// sort the copy into ascending order, then turn it around
const desc = [...words].sort().reverse()
console.log(desc)
// for numbers one compare function does the same job in one step
const numbers = [10, 9, 2]
console.log([...numbers].sort((a, b) => b - a))
A type with its own order
Version has a major and a minor number. compareVersion() compares major first; only when that result is 0 does minor decide, because || takes the first result that is not 0. Checking compareVersion(...) > 0 answers "is the first version greater?".
Output: TypeScript file and captured output
type Version = {
major: number
minor: number
}
// compareVersion returns a negative number, zero or a positive number,
// exactly like the compare functions that sort() takes.
function compareVersion(a: Version, b: Version): number {
// || uses the first result that is not 0: major first, then minor
return a.major - b.major || a.minor - b.minor
}
const v12: Version = { major: 1, minor: 2 }
const v13: Version = { major: 1, minor: 3 }
const v20: Version = { major: 2, minor: 0 }
const v15: Version = { major: 1, minor: 5 }
// Is 1.2 greater than 1.3?
console.log(compareVersion(v12, v13) > 0)
// Is 2.0 greater than 1.5?
console.log(compareVersion(v20, v15) > 0)
Custom Orders
A custom order is any order you describe with a compare function.
sort() takes that function as its only argument. Because sort() is stable, elements the function calls equal stay in their original order, which is what you want when sorting words by length.
A common wish is to sort by one property of each element. There is no special method for that: write a compare function that compares that property, with - for numbers or localeCompare() for strings.
For descending order, swap a and b in the comparison.
To sort by one thing and then by another, join two comparisons with ||. It takes the first result that is not 0, so when the first comparison calls the elements equal, the second one breaks the tie.
Sort with a compare function
The compare function returns a.length - b.length. A negative result means a is shorter and comes first, so the words end up from shortest to longest: [ 'c', 'bb', 'aaa' ].
Output: TypeScript file and captured output
const words = ['aaa', 'bb', 'c']
// the compare function returns a.length - b.length, so shorter words come first
const sorted = [...words].sort((a, b) => a.length - b.length)
console.log(sorted)
Sort by length
The words are compared with a.length - b.length. "one" and "two" are both 3 letters long; because sort() is stable, "one" stays before "two".
Output: TypeScript file and captured output
const words = ['one', 'two', 'three', 'four']
// sort() is stable: words of equal length keep their original order
const byLength = [...words].sort((a, b) => a.length - b.length)
console.log(byLength)
Sort by last letter, descending
a[a.length - 1] is the last letter of a word. The letters of b and a are compared in swapped order, so the word with the largest last letter comes first: "four" (r), then "two" (o), then the two words ending in "e" in their original order.
Output: TypeScript file and captured output
const words = ['one', 'two', 'three', 'four']
// compare the last letters, with b before a for descending order
const byLastLetter = [...words].sort((a, b) =>
b[b.length - 1].localeCompare(a[a.length - 1]))
console.log(byLastLetter)
Sort by length, then alphabetically
|| compares the lengths first. When two words are equally long, it falls back to localeCompare(), which orders them alphabetically. The result is printed with join(' '), on one line: a b c aa bb aaa ccc.
Output: TypeScript file and captured output
const words = ['aaa', 'bb', 'c', 'b', 'a', 'aa', 'ccc']
// || uses the first comparison that is not 0: length first, then alphabet
const sorted = [...words].sort((a, b) => a.length - b.length || a.localeCompare(b))
console.log(sorted.join(' '))
Check Sorted Order
Sometimes you only want to know whether an array is already sorted, without sorting it.
There is no built-in check, but every() writes one in a single line: every element must come after the element before it. The small isSorted() helper below takes a compare function, so the same helper can check ascending order, descending order, or the order of one property. For descending order you pass a compare function with a and b swapped.
Ascending and descending check
isSorted() confirms that 1, 2, 3, 4 are ascending. The same helper with (a, b) => b - a confirms that 4, 3, 2, 1 are descending. The element at index 0 is always accepted, because it has no element before it.
Output: TypeScript file and captured output
// isSorted checks every neighbouring pair with a compare function.
function isSorted<T>(items: T[], compare: (a: T, b: T) => number): boolean {
return items.every((value, i) => i === 0 || compare(items[i - 1], value) <= 0)
}
const ascending = [1, 2, 3, 4]
const descending = [4, 3, 2, 1]
console.log(isSorted(ascending, (a, b) => a - b))
// swapped arguments check for descending order
console.log(isSorted(descending, (a, b) => b - a))
Check order by a field
The users are Alice (24), Bob (31) and Charlie (29). The compare function looks only at age. Bob is older than Charlie, so the array is not sorted by age and the answer is false.
Output: TypeScript file and captured output
type User = {
name: string
age: number
}
function isSorted<T>(items: T[], compare: (a: T, b: T) => number): boolean {
return items.every((value, i) => i === 0 || compare(items[i - 1], value) <= 0)
}
const users: User[] = [
{ name: 'Alice', age: 24 },
{ name: 'Bob', age: 31 },
{ name: 'Charlie', age: 29 },
]
// are the users in order of age?
console.log(isSorted(users, (a, b) => a.age - b.age))
Reverse Order
Reversing puts the last element first and the first element last.
reverse() reverses an array in place, just like sort() does. To get a reversed copy instead, reverse a copy: [...words].reverse(). The original keeps its order.
Reversed copy
The copy is reversed and printed, then the original is printed to show it is unchanged.
Output: TypeScript file and captured output
const words = ['one', 'two', 'three', 'four']
// reverse() also works in place, so reverse a copy
const reversed = [...words].reverse()
console.log(reversed)
console.log(words)
Random Order
Shuffling puts the elements in a random order.
There is no shuffle method. The usual way is a small loop that walks the array from the last position down and swaps each element with an element at an earlier position. The loop changes the array it is given, so shuffle a copy when you still need the original.
Math.random() would give a different order on every run. This example therefore seeds its own generator: two lines of arithmetic that turn a seed into a stream of numbers from 0 up to 1. The same seed always gives the same order, which is useful for tests and for examples like this one.
Shuffled copy
A generator with seed 9 shuffles a copy of the words and prints [ 'two', 'four', 'three', 'one' ]. Running the program again prints the same order, because the generator starts from the same seed.
Output: TypeScript file and captured output
// randomGenerator returns a function that gives numbers from 0 up to 1.
// The same seed always gives the same numbers, so the output never changes.
function randomGenerator(seed: number): () => number {
let state = seed
return () => {
state = (state * 48271) % 2147483647
return state / 2147483647
}
}
const words = ['one', 'two', 'three', 'four']
const random = randomGenerator(9)
// Walk the copy from the last position down and swap with an earlier one.
const shuffled = [...words]
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(random() * (i + 1))
const kept = shuffled[i]
shuffled[i] = shuffled[j]
shuffled[j] = kept
}
console.log(shuffled)