Chapter 14
Sorting and Searching Without Writing Either
sort, find, and reduce.
Chapter 8 ended by promising that the accumulator loops you’d just written by hand had one-line versions waiting in chapter 14. They do, and so does searching, and so does sorting.
Everything here is a method on an array, and most of them take a function as an argument. Chapter 9’s arrow functions were building up to this.
Sorting, and its two traps
const nums = [10, 9, 100, 1];
console.log(nums.sort());
[ 1, 10, 100, 9 ]
That is not sorted. 9 is at the end, after 100.
Trap one: the default sort compares text, not numbers. With no
instructions, .sort() turns everything into a string and sorts
alphabetically, and alphabetically "100" comes before "9" for the same
reason “apple” comes before “banana”: it’s comparing the first character.
The fix is to say how to compare two items:
console.log([10, 9, 100, 1].sort((a, b) => a - b));
[ 1, 9, 10, 100 ]
That function is a comparator. It gets handed two items and returns a
negative number if a should come first, positive if b should, and zero
if it doesn’t matter. a - b does exactly that for numbers, and it’s worth
recognising on sight because you’ll see it constantly. Swap it to b - a
for descending:
console.log([10, 9, 100, 1].sort((a, b) => b - a));
[ 100, 10, 9, 1 ]
Sorting text needs no comparator, since alphabetical is what the default already does:
console.log(["banana", "Apple", "cherry"].sort());
[ 'Apple', 'banana', 'cherry' ]
Note Apple came first. Capitals sort before lowercase, so a mixed-case
list won’t come out the way a human would file it. .localeCompare() is
the fix when that matters, and the project uses it.
Sorting objects
Comparators are what make sorting your own types possible. Chapter 12’s books, by page count:
type Book = { title: string; pages: number };
const shelf: Book[] = [
{ title: "Ulysses", pages: 730 },
{ title: "Dune", pages: 412 },
{ title: "Emma", pages: 474 },
];
const byPages = [...shelf].sort((a, b) => a.pages - b.pages);
console.log(byPages.map((b) => b.title));
[ 'Dune', 'Emma', 'Ulysses' ]
Same a - b shape, reaching into a property. Sorting by title instead
means comparing text, which is what .localeCompare() is for:
const byTitle = [...shelf].sort((a, b) => a.title.localeCompare(b.title));
.localeCompare() returns exactly the negative, zero or positive a
comparator wants, and it handles capitals and accented letters the way a
human filing things would.
Finding one thing
const found = shelf.find((b) => b.title === "Emma");
console.log(found);
{ title: 'Emma', pages: 474 }
.find() walks the array, hands each item to your function, and gives back
the first one for which it returns true. The function you pass answers a
yes-or-no question about one item, which is why it reads like a sentence:
find the book whose title is Emma.
When nothing matches, you get undefined. And this is where something
genuinely good happens:
const missing = shelf.find((b) => b.title === "Nope");
console.log(missing.title);
error TS18048: 'missing' is possibly 'undefined'.
TypeScript stopped you. .find() is declared as returning
Book | undefined, that union from chapter 10, so the type says out loud
that it might not be there, and you cannot use the result until you’ve
dealt with that possibility.
An if is all it takes, and chapter 7’s truthiness does the work:
if (found) {
console.log(found.title);
} else {
console.log("Not on the shelf");
}
Inside that if, TypeScript knows found can’t be undefined any more,
so .title is allowed. That’s narrowing, and chapter 28 is about how
much of it TypeScript can do.
Finding all of them
const longBooks = shelf.filter((b) => b.pages > 450);
console.log(longBooks.length);
2
Same shape as .find(), different answer: .filter() gives back every
match rather than the first, as a new array. It doesn’t touch the
original, so unlike .sort() there’s nothing to defend against.
Nothing matching gives you an empty array, not undefined, which is
usually easier to handle. .length of zero is a perfectly good answer.
Boiling a list down to one value
Chapter 8’s running total:
let total = 0;
for (const b of shelf) {
total += b.pages;
}
The one-line version:
const total = shelf.reduce((sum, b) => sum + b.pages, 0);
console.log(total);
1616
.reduce() carries a value along as it walks the array. Your function gets
the value so far and the current item, and returns the new value so far.
The 0 at the end is where it starts, the same let total = 0 as before,
just moved.
That trailing 0 is easy to leave off and worth keeping:
const empty: number[] = [];
console.log(empty.reduce((a, b) => a + b));
TypeError: Reduce of empty array with no initial value
Without a starting value, .reduce() uses the first item as the start,
and an empty array hasn’t got one. Always pass the initial value; it costs
three characters and removes a crash.
Four more, briefly
| Method | Gives you |
|---|---|
.map() | a new array with every item transformed |
.some() | true if any item passes |
.every() | true if all items pass |
.findIndex() | the position of the first match, or -1 |
console.log(shelf.map((b) => b.title));
console.log(shelf.some((b) => b.pages > 700));
console.log(shelf.every((b) => b.pages > 400));
[ 'Ulysses', 'Dune', 'Emma' ]
true
true
.map() is the one you’ll use most after .filter(). Note .findIndex()
returns -1 rather than undefined, matching .indexOf() from chapter 4
rather than .find() from this one, which is a genuine inconsistency in
the language and not something you did wrong.
Exercise 1 · Rewrite chapter 8 in one line each
Go back to chapter 8’s accumulator section, the running total and the
longest-so-far. Rewrite both with .reduce().
The total is the easy one. The longest word is the interesting one,
because the value you carry along isn’t a number, it’s the best string so
far, and the starting value is "". Write the comparison inside the
function and let reduce do the walking.
Then decide honestly which version you’d rather read in six months. There isn’t one right answer, and knowing both is the point.
Check yourself
Project
A high-score table
Roughly 45 minutes
Build a high-score table for a game, using the tools from this chapter rather than loops.
Start with a Player type (a name and a score) and an array of at least
six of them, deliberately out of order and with mixed capitalisation in
the names.
Then produce, each in as few lines as it honestly takes:
- The top three, highest score first. Sort a copy, then
.slice(0, 3). - A named player’s score, looked up with
.find(). Handle the case where they aren’t in the table, and don’t reach for!or a cast to silence TypeScript, write theif. - The total of every score, with
.reduce()and an initial value. - The average, which is the total divided by the count. Chapter 6 applies: decide what you want to do about the decimals.
- Everyone above the average, with
.filter(). - The names alphabetically, using
.localeCompare()so the capitalised ones file where a human would put them.
The rule that makes this a real exercise: after all six, print the original array again and confirm it is exactly as you typed it, same order, same values. Every step above must leave it untouched.
That’s the chapter 13 lesson under test. Step 1 and step 6 both sort, and both will wreck your data if you forget the spread. The check at the end is the only thing that tells you.
Stretch: print the table as a ranked list, 1. Ada 340, using
.map() and the index. Look up how .map() supplies a position to the
function you give it, then remember it counts from zero and your ranking
doesn’t.