Rewriting Fluent Sort
I wrote an npm module for sorting arrays awhile back, fluent-sort, because hand writing nested sorting rules is kind of annoying (i.e., sort by rank and then sort alphabetically). The original fluent-sort was kind of junky, because it wasn't really a true fluent interface, and it wasn't really based on pure functions fully either.
All of fluent-sort was inspired by the simple syntax of C#:
JavaScript would look something like this:
class Pet {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
const pets = [
new Pet("Barley", 8),
new Pet("Boots", 8),
new Pet("Whiskers", 1)
];
const orderedPets = pets.sort((left, right) => {
if (left.age === right.age) {
if (left.name === right.age) return 0;
return left.name > right.name ? -1 : 1;
}
return left.age > right.age ? 1 : -1;
});
console.log(orderedPets);
/*
[ Pet { name: 'Whiskers', age: 1 },
Pet { name: 'Boots', age: 8 },
Pet { name: 'Barley', age: 8 } ]
*/
This is, frankly, terrible and dumb. Why am I returning -1 or 1? When do I return either? Why do I have to have that cognitive overload each time I need to sort?
Original release of fluent-sort
The original release of fluent-sort was suffering from an identity crises. It had a somewhat fluent syntax, and tried to occasionally be pure, and returned too many different classes just to make something have to call SortBy before ThenBy. It tried too hard to maintain compatibility with JavaScript's sort method signature.
There was nothing inherently wrong with any of that, but the original fluent-sort was trying too hard to keep the original sort method logic at the forefront of the library, and focused entirely too much on fancy method names. With fluent-sort's v2 release, I shifted the focus to the far more common concept of sorting by fields on an object, and making fluent-sort, well, actually fluent.
API Changes
The original version of fluent-sort returned specific classes called Orderables and OrderableInitiators, which allowed those objects to only be useful for sorting. They did not expose the underlying array, but rather internally stored the array as a class property. That means that you were no longer dealing with arrays, and were adding a great deal of overhead just for the sake of performing a sort. It wasn't wrong, but it wasn't quite right, either.
It was just unecessary.
Version 2 of fluent-sort takes an entirely different approach: FluentSortArrays extend native JavaScript arrays, allowing you to modify the array, check its array, and perform array methods on it like so:
Which logs:
4
5
8, 12
This is much more in line with the concept of a fluent API.
Using fluent-sort
Importing
es6 with imports
You can import FluentSortArray from the root of the module, like so:
import FluentSortArray from "fluent-sort";
es5 / nodejs without imports
If you're still working in an ES5 environment and can't rely on the import/export syntax, then you can require FluentSortArray like so:
const { FluentSortArray } = require("fluent-sort/es5/");
In Browser
A preminified version of fluent-sort is packaged in node_modules/fluent-sort/dist/fluentSort.min.js
Add that script to your page, and then you can use FluentSortArray
on your page.
Constructing a FluentSortArray
The FluentSortArray
extends the native Array
class, so it has the same constructor. There are a number of ways to initialize a FluentSortArray
:
const fromClassConstructor = new FluentSortArray(1,2,3); //equivalent to [1,2,3]
const fromSpreadingAnotherArray = new FluentSortArray(...[1,2,3]); // equivalent to above, technically, but may be preferable to write for you
const regularArr = [1,2,3];
const fromRegularArrWithoutMutatingRegularArr = FluentSortArray.fromArray(regularArr); // copies regularArr and constructs a new array
const getsMutated = [1,2,3];
const byMutatingRegularArray = FluentSortArray.makeFluent(getsMutated); // mutates getsMutated into a FluentSortArray; (getsMutated === byMutatingRegularArray) === true
All of these FluentSortArrays, by nature of extending array, provide all the normal array methods like slice
, push
, pop
, filter
, etc. and can be mutated at any time, and elements can be accessed by index like normal.
Chaining Sorting Rules
The assumption is that you will mostly be sorting by fields:
const testCases = [
{
id: 0,
name: "Strong Monster",
strength: 10,
agility: 5,
intelligence: 8,
monsterdexOrder: 5
},
{
id: 1,
name: "Fast Monster",
strength: 5,
agility: 10,
intelligence: 5,
monsterdexOrder: 1
},
{
id: 2,
name: "Mediocre Monster",
strength: 7.5,
agility: 7.5,
intelligence: 8,
monsterdexOrder: 6
},
{
id: 3,
name: "Unimpressive Monster",
strength: 2,
agility: 2,
intelligence: 2,
monsterdexOrder: 4
},
{
id: 4,
name: "Slow Monster",
strength: 7.5,
agility: 3,
intelligence: 8,
monsterdexOrder: 17
},
{
id: 5,
name: "Smart Monster",
strength: 3,
agility: 7.5,
intelligence: 15,
monsterdexOrder: 75
}
];
const sortedTests = new FluentSortArray(...testCases) // Spreads the array to construct a new FluentSortArray using native Array constructor syntax
.sortBy(x => x.intelligence) // Returns the extended array
.thenBy(y => y.agility) // Returns the same extended array
.executeCompositeSort(); // Performs the sort and returns the extended array
You can add any number of thenBy
statements. Each time you add a sortBy
statement, you will start a brand new set of rules and disregard all rules seen previously.
The following methods are supported on each instance of a FluentSortArray:
Method | Description |
---|---|
sortBy(selector) |
Sets a new sort order with selector as the initial field to sort on; sorts in ascending order |
sortByAscending(selector) |
Identitcal to sortBy(selector) |
sortByDescending(selector) |
Identical to sortBy(selector) but in descending order |
sortComparing(comparator) |
Sets a new sort order with a native JS sort comparator function (return 0, -1, or 1) as the initial value to rank an element as |
thenBy(selector) |
Adds another rule to sort on, if the previous sorting rule resulted in an identical value across two elements. |
thenByAscending(selector) |
Identical to thenBy(selector) |
thenByDescending(selector) |
Identical to thenBy(selector) but in descending order |
thenComparing(comparator) |
Adds a new sort order with a native JS sort comparator function (return 0, -1, or 1) that will be evaluated if the previous sort rule returned an identical value across two elements. |
executeCompositeSort() |
Performs the sort with all rules configured and returns the sorted array. This sort occurs in-place and modifies the instance you are working on. |
Each method returns the instance you are configuring, allowing you to chain multiple statements easily:
const sortedTests = new FluentSortArray(...testCases)
.sortBy(x => x.intelligence)
.thenBy(y => y.agility)
.executeCompositeSort();
Lessons Learned
Building
Many environments still do not support import / export syntax, so I configured babel to build out a special version that should work all the way down to ES5. I wanted to release with fluent-sort v2 the following:
- The original import / export / ES6 syntax version with all its features, copied into the root of the published package
- A babel-configured ES5 version that is stored in an
/es5
directory - A webpack bundled library that creates a
fluentSort.min.js
version stored in the/dist
directory for browsers to use
This was quite annoying, and I did not want those artifacts in the repository. I wanted people to be able to easily install the package and use it
Publishing
I configured the package.json folder with some tasks to accomplish all this.
This results in the following build
folder to be generated and published to npm:
The Path Forward
I'm not done with this library just yet, despite it being basically feature complete for now. The next goals are:
- Improve test coverage, and come up with better tests.
- Create a demo website allowing users to modify objects and construct sorts, then see the code generated and the resulting sort order.
- There is actually a (maybe not ready) pure version of this class provided as well that needs to be better tested,
PureFluentSortArray
, which constructs and returns a new object each time you add a sort rule. This requires testing and documentation.