Article summary
While working on a project using ExtJS 4 as a JavaScript framework, I found I needed to sort the rows of a grid panel in a manner that was not sensitive to numbers lacking leading zeroes, a basic type of Natural Sort. I didn’t find much that was useful on the internet for accomplishing this particular task in the context of ExtJS, so I rolled my own solution.
The Problem
The strings which should sort as:
a1, a2, a3, a10, a31, a101, a300
Actually sort as:
a1, a10, a101, a2, a3, a300, a31
ExtJS has a concept of a ‘sortType’ on a model field that defines how any sorts on a field should be interpreted. Unfortunately, the sortType function is only designed to take a single input and morph it into something that’s orderable by a standard JavaScript sort function. This is great for quantities like dates that can be easily represented as a number. It can also be used easily for creating a case-insensitive sort by simply coercing all the characters into a single case. Problems arise, however, when attempting to use this for a natural sort.
Every implementation of a sort (and there are some good natural sorts for JavaScript) works by comparing two values. You can’t do any sort of comparison when you’ve only got a single value to work with. As such, I clearly had to come up with a way to transmogrify a string like a-2
so it would sort correctly alongside a-20
.
The Solution
I put on my regex robe and wizard hat, did some spelunking on stack overflow, and came up with a simple regex string replacement that works quite well for the limited cases needed here.
It’s a simple trick, greatly inspired by hours of work in my youth manually padding photo filenames with zeroes. To make a ‘dumb’ sort behave correctly on a string representation of a number, you just need to make sure it’s the same length as the rest of the numbers.
/**
* Do some tricksy things to left-pad numbers with zeros to make them
* sort in a natural fashion.
*/
Ext.apply(Ext.data.SortTypes, {
asNatural: function (str) {
// Pad all the numbers we can find with 10 zeros to the left, then trim
// down to the last 10 digits. A primitive natural sort occurs.
// WARN: May do odd things to any numbers longer than 10 digits. It will
// also not work as you might expect on decimals.
return str.replace(/(\d+)/g, "0000000000$1").replace(/0*(\d{10,})/g, "$1");
}
});
To accomplish this, the regex adds a whole bunch of zeroes (10 in this case) to the left of all chunks of digits it finds, then eats all the leading zeroes it can find down to 10 digits.
To illustrate, I’ll show an example with a smaller number of zeroes being padded.
Given the strings
abc123, abc1, abc2, abc1-1
We blindly prepend the string “0000” to anything made up of one or more digits.
abc0000123, abc00001, abc00002, abc00001-00001
And finally consume every zero we can up until we’re down to the correct length.
abc0123, abc0001, abc0002, abc0001-0001
The Application
To actually use this natural sort functionality in your app is extremely simple. Put the function defined above somewhere in your code that will be run at initialization, before any of your data is loaded but after Ext is loaded, so that the asNatural
sort function will already be available. Then when you define your Ext Models, add an extra config of sortType: 'asNatural'
. That’s it.
Ext.define('Flight', {
extend: 'Ext.data.Model',
fields: [{
name: 'flightNumber',
type: 'string',
sortType: 'asNatural'
}, {
name: 'inFlightMeal',
type: 'boolean'
}]
});
Any trigger of a sort on the flightNumber
field will now use the natural sort modification, including things like triggering a sort based on a grid’s column headers.
It should be noted that this is far from a general solution and will break in some suspicious cases. For example, try and figure out what will happen to a000000000000198734829279
. It’ll get truncated to a198734829279
for the sort, and will appear to come after a100000000000198734829279
. Sorting strings that contain decimal numbers can also be problematic. The strings a.01
and a.1
will evaluate exactly the same in the sort function. In some apps, this would be a deal-breaker.
If my usage was not tolerant to these failures, I would be able to subclass ExtJS’s store, override the sort function, and add a special sort trigger for any necessary fields, and utilize one of the nice natural sort algorithms mentioned above. In my case, the times when the known bugs will be triggered are few and far between, and the simplicity of this fix sure beats hacking into ExtJS’s internals.
It doesn’t sort correctly the negative values… this is what I’m getting.
5%
4%
0%
-100%
-50%
-5%
Would it be a small optimization to only add 9 0’s?
That would possibly reduce replacements in the 2nd regex.