Sorting out Javascript Sort
Jenny Shaw
Posted on July 3, 2019
You Don't Know Sort
Until recently, I'd really underestimated how much Javascript's sort method can accomplish.
It's a tool that can easily be taken for granted, especially in those circumstances when you can just call it on an array and, without any extra effort at all, watch it magically rearrange its elements in order as you expect it to.
// Orders names alphabetically
let myPets = ["Oreo", "Shortbread", "Peanut", "Souffie", "Tella"];
pets.sort();
// => [ 'Oreo', 'Peanut', 'Shortbread', 'Souffie', 'Tella' ]
// Order numbers numerically
let numbers = [3, 2, 1, 6, 5, 4];
numbers.sort();
// => [ 1, 2, 3, 4, 5, 6 ]
// Don't stop reading here! You know nothing yet!
However, sort
on its own doesn't exactly behave as one would expect it to. For example, when called on amazingInventions
, an array of capitalized and lowercased words, sort
will order all the capitalized words before the lowercased ones. It's a bit odd and a little inconvenient, but I can see that there's some logic involved, so I'm not ready to riot over it yet.
let amazingInventions = ['computer', 'Internet', 'telegraph', 'vaccines'];
amazingInventions.sort();
// returns . . . . . . . [ 'Internet', 'computer', 'telegraph', 'vaccines' ]
// when you expected . . [ 'computer', 'Internet', 'telegraph', 'vaccines' ]
The function also appears to order numbers until you introduce multi-digit and negative numbers into your arrays, and that's when you really start to notice that something's not quite right.
let complexNumbers = [1, 3, -2, -1, 5, 11];
complexNumbers.sort();
// returns . . . . . . . [ -1, -2, 1, 11, 3, 5 ]
// when you expected . . [ -2, -1, 1, 3, 5, 11 ]
In the example above, sort
places -1
before -2
, and inserts 11
between 1
and 3
. That order clearly makes no sense, so how does this happen?
How Sort Works
It turns out that Javascript's sort
sorts numbers just like a dictionary sorts words. Remember when you were a kid and learned how to alphabetize words letter by letter from left to right? sort
is doing the same thing here. And regardless of whether your input is an array of strings or numbers or both, it'll interpret each element like a string and will systematically order elements one character unit at a time according to its Unicode code point.
Let's take a look into this for ourselves. Below, we have an array containing a variety of characters. It includes one lowercase and one uppercase letter, single and double-digit numbers, and we'll also toss in a dollar sign for good measure.
let randomStuff = ["a", "A", "1", "2" "12", "$"];
To make the point clearer, I'll use charCodeAt()
and create a handy reference that points each element to its first character's character code. Don't worry about the process, but just pay attention to the return.
charCodes = {}
for(let el of randomStuff) {
charCodes[el] = el.charCodeAt(0)
}
// => {
// '1': 49,
// '2': 50,
// '12': 49,
// '$': 36,
// A: 65,
// a: 97 }
You'll notice that 1
and 12
share the same first character, 1, therefore each also shares the same first character code, 49. So by this logic of comparing first characters only, 12
would be ordered before 2
because sort
is using 12
's first digit's character code to compare it with 2
's.
Let's sort the array using only .sort()
, and we'll get this return.
arr.sort();
// => [ '$', '1', '12', '2', 'A', 'a' ]
So, understanding that sort
looks at elements character by character and compares by character code, it makes sense that capital A
would come before lowercase a
and that $
would be first in line before everything else. sort
is still in a way rearranging elements in numerical order, but strictly by each character's character code. Yes, the result still looks wonky, but at least we understand now it's not completely random and that it does follow a predictable set of rules.
Let's Sort Stuff!
Now that we've made more sense of .sort()
, we can really use it to its fullest potential by taking advantage of the fact that it is a higher-order function. I'll try not to sound super repetitive while explaining this but, a higher-order function is a type of function that can take another function as an argument or has a return value that is itself a function. Some examples of other common higher-order functions we use are forEach
, map
, filter
, and reduce
.
In the context of sort
, we want to pass in a "compare function", and the best part about being able to do that is that we can really make sort
do precisely what we want, whether that be sorting array elements purely alphabetically, numerically, or by properties. We can do quite a lot!
Sort Purely Alphabetically
I was an English teacher in a past life, so it really bugs me to see words "alphabetized" by uppercase then lowercase letters. It's not how you'd see it in a dictionary, so it's no reason to let sort
get away with this kind of behavior.
To fix the faulty alphabetical ordering of sort
, our compare function will do the following:
- Compare words, two at a time
- Lowercase words before comparisons to prevent division between lower and uppercased words! (Note: this won't affect the element in the end, but it sets pairs up nicely for a fair comparison)
- Apply the following logic: If word
a
's character code is lower than wordb
's, return-1
, else return1
The logic we apply is important here because the return value determines how we'll sort each element. A negative return means that a
should be sorted before b
and a positive return means that b
should be sorted before a
.
let pWords = ["Paris", "panic", "potato", "Portugal"]
pWords.sort() // applying only .sort()
// => [ 'Paris', 'Portugal', 'panic', 'potato' ] -- BAD.
// create compare function
function compareWords(a,b) {
if (a.toLowerCase() < b.toLowerCase()) {
return -1;
} else {
return 1;
}
}
// pass compareWords function into .sort()
pWords.sort(compareWords)
// => [ 'panic', 'Paris', 'Portugal', 'potato' ] -- MUCH BETTER.
That's exactly how I want it sorted and I feel so much better. And just because I prefer my code to look succinct, I might slim it down using an arrow function and a ternary operator.
pWords.sort((a,b) => a.toLowerCase() < b.toLowerCase() ? -1 : 1)
Nice!
To sort in reverse-alphabetical order, just reverse the comparison operator.
pWords.sort((a,b) => a.toLowerCase() > b.toLowerCase() ? -1 : 1)
Numeric Order
A compare function that orders numeric arrays uses the same concept as compareWords
. Again, we'll compare two elements a
and b
, but this time using the subtraction operator.
Similarly, if the difference returns a negative value, a
is sorted before b
, if the difference returns a positive value, b
is sorted before a
.
let numbers = [1, 101, 23, 18]
// You could do it this way
function compareNumbers(a,b) {
return a - b;
}
numbers.sort(compareNumbers);
// But this is much simpler
numbers.sort((a,b) => a - b);
// => [ 1, 18, 23, 101 ]
Order by Word Length
We can get a little more creative here and instead of ordering alphabetically, we can order by word length. Remember how we sorted numbers? It's a lot like that. We're not comparing letters anymore, but we're comparing the number of characters in a word, which is why the order of "Paris" and "panic" don't matter.
pWords.sort((a,b) => a.length - b.length)
// => [ 'Paris', 'panic', 'potato', 'Portugal' ]
Ordering objects by property
This is where sort
gets really fun. Imagine we've got an array of objects. I created an array containing a small sampling of McDonald's burgers. Included in each object is the name of the burger, the calorie count, and a general list of ingredients that make the burger.
I can sort this array of burgers in a number of different ways, each by a different property. First, I'll sort alphabetically by burger name.
To do this, we'll follow the structure of our alphabetical or numerical compare functions, but this time, we'll chain a property name of our objects to our variables a
and b
.
let McDBurgers = [
{name: "hamburger",
calories: 250,
ingredients: ["bun", "beef patty", "ketchup", "pickle slices", "onions", "mustard"]},
{name: "Cheeseburger",
calories: 300,
ingredients: ["bun", "beef patty", "american cheese", "ketchup", "pickle slices", "onions", "mustard"]},
{name: "McChicken",
calories: 410,
ingredients: ["bun", "chicken patty", "lettuce", "mayonnaise"]},
{name: "Filet-O-Fish",
calories: 390,
ingredients: ["bun", "fish filet patty", "american cheese", "tartar sauce"]}
];
// Sort by burger name
McDBurgers.sort((a,b) => a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1)
//=> [
// {
// name: 'Cheeseburger',
// calories: 300,
// ...
// },
// {
// name: 'Filet-O-Fish',
// calories: 390,
// ...
// },
// {
// name: 'hamburger',
// calories: 250,
// ...
// },
// {
// name: 'McChicken',
// calories: 410,
// ...
// }
//]
Look! Our burger objects are neatly ordered alphabetically!
I can go even further and order them by calorie count or by count of unique ingredients.
// sort by calorie count
McDBurgers.sort((a,b) => a.calories - b.calories)
// sort by number of unique ingredients
McDBurgers.sort((a,b) => a.ingredients.length - b.ingredients.length)
If we were to run these lines, we should be able to see each of McDBurger's burger objects rearrange accordingly. Plug each line of code into your console and see for yourself what returns come back! Then try to explore what other ways you can manipulate sort
to order your arrays!
Posted on July 3, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
October 21, 2024