Contents

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

MethodGives 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

1. Why does [10, 9, 100, 1].sort() give [1, 10, 100, 9]?

Not quite. The order is completely predictable once you know what it is comparing.

Yes. "100" comes before "9" for the same reason "apple" comes before "banana". Pass (a, b) => a - b to compare them as numbers.

Not quite. Sorting again changes nothing. The comparison itself is what needs fixing.

2. const sorted = nums.sort(); What happened to nums?

Not quite. That is filter and map. sort rearranges in place and returns a reference to the same array.

Yes. sorted and nums are two names for one array, exactly the chapter 13 situation. Sort a copy with [...nums].sort() instead.

Not quite. Nothing is moved out. The same array is still there, just rearranged.

3. Why does TypeScript catch an unchecked .find() result when it did not catch scores[10]?

Not quite. Age has nothing to do with it. The difference is in what each one declares it returns.

Yes. find is honest about possibly finding nothing, so you are held to a check. Indexing claims it always succeeds, which is the lie chapter 10 showed you.

Not quite. The element type is not the issue. Indexing an array of books is exactly as unchecked as indexing an array of numbers.

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:

  1. The top three, highest score first. Sort a copy, then .slice(0, 3).
  2. 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 the if.
  3. The total of every score, with .reduce() and an initial value.
  4. The average, which is the total divided by the count. Chapter 6 applies: decide what you want to do about the decimals.
  5. Everyone above the average, with .filter().
  6. 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.