better sort in JavaScript

Better Sort Ordering in JavaScript

Lou Mauget Development Technologies, JavaScript, Programming, Tutorial Leave a Comment

The ordering of sorted mixed alpha/numeric strings can appear weird, and we all know there has to be a better sort ordering in JavaScript out there. I mean, why does unadorned JavaScript array.sort()produce this…

  1. Item 1
  2. Item 100
  3. Item 2

when I want this?

  1. Item 1
  2. Item 2
  3. Item 100

If I’m asked to resort and populate an ordered dropdown list from unordered server-originated alphanumeric strings, I believe Q/A would prefer the second result.

A colleague of mine, Michelle Farmer, recently showed me a couple of links that present a solution. In this post, I’ll share that solution. I’ll describe reusing that information to drop in a declarative collation approach. The result will be intuitive ordering of strings of mixed numbers and alphabetic characters.

The Issue

A sort using a simple item-to-item string comparison can produce rather unintuitive alphanumeric string ordering. I’d rather see any embedded-digit-character sequence behave like a single number instead of individual-digit characters.

It’s embarrassing and virtually impossible to argue to a Q/A person that the strange unhelpful ordering is correct. Yeah, and gravity attracts sideways.

Here is a bare-bone alphanumeric sort example executed in a CLI node script.

const input = [
  'Item 1',
  'Item 2',
  'Item 100'
];
const output = input.sort();
output;

Here’s the example run captured in a screenshot. The results are in green.

The ordering of the input array was correct in Q/A’s eyes. The ordering of the sort output? Not cool.

Why did the sort ruin the desired order?

The presumed default sort comparison function implementation is a string comparison resembling strA < strB (or we could explicitly supply it). It’s a string comparison. The comparison internally scans the two strings left-to-right, comparing each character of strA to its sibling in strB using the ordering of the character set in-play (e.g. UTF8, …).

Work it out manually for ['1', '2', '10'] to see for yourself.

First, do it by comparing character-to-character, swapping items based on arg1.charAt(0) < arg2.charAt(0), producing ['1', '10', '2'].

Now, do it by comparing number-to-number, swapping elements based on parseInt(arg1) < parseInt(arg2). That result is ['1', '2', '10']. Better.

Can we mix and match the two variants with alphabetic runs of characters? I’d want arbitrary encountered sub-sequences of numbers to contribute to the overall comparison numerically instead of digit-by-digit.

Use a Collator

Envision a smarter comparison that parses each string of a compare input, collating it into typed sequences. In my example, “Item” could be identified as a group of alphabetic characters while “1000” could be typed as a single number, not digit-by-digit. Fold in some rules about what compares to what and when, and it gets even more complicated… Maybe we could have a smarter comparison function that considers each digit sequence as a number.

Relax. I’m not going to code a collator. The JavaScript Intl.Collator has a comparison function, compare, that has options. An Intl.Collator instance has a numeric option that collates any embedded sequence of digits to a number for part of a locale comparison. That’s what I want!

Just pass the compare method of an Intl.Collator instance created with option object { numeric: true, sensitivty: 'base' }. like this…

const input = [
  'Item 1',
  'Item 2',
  'Item 100'
];
const collator = new Intl.Collator('en', { numeric: true, sensitivity: 'base' });
const output = input.sort(collator.compare);
output;

It works! The execution output value in the Node CLI has intuitive ordering, shown in green.

The sensitivity option stipulates, base, a normal comparison. If I passed undefined as the language, it still worked on the languages I tried.

Sorting Objects

You’re sorting objects, not flat strings? Then you could supply a compare function to the sort that drills into each parameter object to check the desired field. Delegate its comparison to collator.compare to override the default argument pair.


const input = [
  { name: 'Item 1'},
  { name: 'Item 2'},
  { name: 'Item 100'}
];
const collator = new Intl.Collator('en', { numeric: true, sensitivity: 'base' });
const output = input.sort((a, b) => collator.compare(a.name, b.name));
output;

Here, the object order is correct as well.

Better sort ordering in JavaScript

Digression

If you consult the MDN link for the collator in the references, you will find interesting uses aside from comparison functions, such as a melding of national language and date manipulation.

Summary

In this post, I showed how to cajole Array.sort() into producing the following order. This…

  1. Item 1
  2. Item 2
  3. Item 100

instead of this…

  1. Item 1
  2. Item 100
  3. Item 2

The answer was to pass the sort function a comparator argument from the International Collator built into every major browser and Node.js. This approach is simple and declarative for lists of flat strings. The comparison function arguments default to each string being compared. For sorting objects such as a list of dropdown choices, just pass a pair of the sort field drill-downs to the comparison function.

You may be interested in other cool things that the collator may bring, such as giving you the date of “yesterday”. I encourage you to play around with it on your own!

If you found this helpful, I’ve written a handful of others, all available on the Keyhole Dev Blog. Give them a read, and drop me a comment below if you’d like!

References

 

0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments