functional arbitrary sort
Here's another way to approach your problem. Let's say we have some fruits
and an arbitrary order
we wish to sort them in -
const fruits =
// 0 1 2 3 4
[ "apple", "banana", "cherry", "orange", "peach" ]
const order =
[ 1, 3, 2, 0, 4 ]
We want to be able to write something like this -
fruits.sort(sortByIndex(fruits, order))
console.log(fruits)
// [ "banana", "orange", "cherry", "apple", "peach" ]
// 1 3 2 0 4
We wish up a Comparison
module to handle our sorting code -
const { empty, map } =
Comparison
const sortByIndex = (values = [], indexes = []) =>
map(empty, x => indexes.indexOf(values.indexOf(x)))
Now we just have to implement Comparison
-
const Comparison =
{ empty: (a, b) =>
a < b ? -1
: a > b ? 1
: 0
, map: (m, f) =>
(a, b) => m(f(a), f(b))
}
const { empty, map } =
Comparison
const sortByIndex = (values = [], indexes = []) =>
map(empty, x => indexes.indexOf(values.indexOf(x)))
const fruits =
[ "apple", "banana", "cherry", "orange", "peach" ]
// 0 1 2 3 4
const order =
[ 1, 3, 2, 0, 4 ]
console.log(fruits)
// [ "apple", "banana", "cherry", "orange", "peach" ]
console.log(fruits.sort(sortByIndex(fruits, order)))
// [ "banana", "orange", "cherry", "apple", "peach" ]
why a module?
Implementing a Comparison
module means we have a tidy place to store all of our comparison logic. We could easily implement other useful functions like reverse
and concat
now -
const Comparison =
{ // ...
, concat: (m, n) =>
(a, b) => Ordered.concat(m(a, b), n(a, b))
, reverse: (m) =>
(a, b) => m(b, a)
}
const Ordered =
{ empty: 0
, concat: (a, b) =>
a === 0 ? b : a
}
Now we can model complex sorting logic with ease -
const sortByName =
map(empty, x => x.name)
const sortByAge =
map(empty, x => x.age)
const data =
[ { name: 'Alicia', age: 10 }
, { name: 'Alice', age: 15 }
, { name: 'Alice', age: 10 }
, { name: 'Alice', age: 16 }
]
Sort by name
then sort by age
-
data.sort(concat(sortByName, sortByAge))
// [ { name: 'Alice', age: 10 }
// , { name: 'Alice', age: 15 }
// , { name: 'Alice', age: 16 }
// , { name: 'Alicia', age: 10 }
// ]
Sort by age
then sort by name
-
data.sort(concat(sortByAge, sortByName))
// [ { name: 'Alice', age: 10 }
// , { name: 'Alicia', age: 10 }
// , { name: 'Alice', age: 15 }
// , { name: 'Alice', age: 16 }
// ]
And effortlessly reverse
any sorter. Here we sort by name
then reverse sort by age
-
data.sort(concat(sortByName, reverse(sortByAge)))
// [ { name: 'Alice', age: 16 }
// , { name: 'Alice', age: 15 }
// , { name: 'Alice', age: 10 }
// , { name: 'Alicia', age: 10 }
// ]
functional principles
Our Comparison
module is flexible yet reliable. This allows us to write our sorters in a formula-like way -
// this...
concat(reverse(sortByName), reverse(sortByAge))
// is the same as...
reverse(concat(sortByName, sortByAge))
And similarly with concat
expressions -
// this...
concat(sortByYear, concat(sortByMonth, sortByDay))
// is the same as...
concat(concat(sortByYear, sortByMonth), sortByDay)
// is the same as...
nsort(sortByYear, sortByMonth, sortByDay)
go nuts with nsort
Now let's say we want to sort by an arbitrary number of factors. For example, sorting date objects requires three comparisons: year
, month
, and day
-
const { empty, map, reverse, nsort } =
Comparison
const data =
[ { year: 2020, month: 4, day: 5 }
, { year: 2018, month: 1, day: 20 }
, { year: 2019, month: 3, day: 14 }
]
const sortByDate =
nsort
( map(empty, x => x.year) // primary: sort by year
, map(empty, x => x.month) // secondary: sort by month
, map(empty, x => x.day) // tertiary: sort by day
)
Now we can sort by year
, month
, day
-
data.sort(sortByDate)
// [ { year: 2019, month: 11, day: 14 }
// , { year: 2020, month: 4, day: 3 }
// , { year: 2020, month: 4, day: 5 }
// ]
And just as easily reverse sort by year
, month
, day
-
data.sort(reverse(sortByDate))
// [ { year: 2020, month: 4, day: 5 }
// , { year: 2020, month: 4, day: 3 }
// , { year: 2019, month: 11, day: 14 }
// ]
Implementing N-sort is a breeze thanks to functional principles. Our concat
and empty
do all the hard work -
const Comparison =
{ // ...
, nsort: (...m) =>
m.reduce(Comparison.concat, Comparison.empty)
}
Expand the snippet below to see this code in action -
const Comparison =
{ empty: (a, b) =>
a < b ? -1
: a > b ? 1
: 0
, map: (m, f) =>
(a, b) => m(f(a), f(b))
, concat: (m, n) =>
(a, b) => Ordered.concat(m(a, b), n(a, b))
, reverse: (m) =>
(a, b) => m(b, a)
, nsort: (...m) =>
m.reduce(Comparison.concat, Comparison.empty)
}
const Ordered =
{ empty: 0
, concat: (a, b) =>
a === 0 ? b : a
}
const { empty, map, concat, reverse, nsort } =
Comparison
const sortByDate =
nsort
( map(empty, x => x.year) // primary
, map(empty, x => x.month) // secondary
, map(empty, x => x.day) // tertiary
)
const data =
[ { year: 2020, month: 4, day: 5 }
, { year: 2019, month: 11, day: 14 }
, { year: 2020, month: 4, day: 3 }
]
console.log(data.sort(reverse(sortByDate)))
// [ { year: 2020, month: 4, day: 5 }
// , { year: 2020, month: 4, day: 3 }
// , { year: 2019, month: 11, day: 14 }
// ]
JavaScript modules
Above Comparison
and Ordered
are defined as simple objects. JavaScript is a very flexible language and import
/export
syntaxes were made explicitly available for modularising your programs. Writing modules in this way gives us a clear picture of where things should go and provides us with plenty of room to grow our code -
// Comparison.js
import { lt, gt, eq, concat:_concat } from "./Ordered"
const asc = (a, b) =>
(console.log(a, b), a < b) ? lt
: a > b ? gt
: eq
const empty =
asc
const map = (m, f) =>
(a, b) => m(f(a), f(b))
const concat = (m, n) =>
(a, b) => _concat(m(a, b), n(a, b))
const reverse = (m) =>
(a, b) => m(b, a)
const desc =
reverse(asc)
export { asc, concat, desc, empty, map, reverse }
// Ordered.js
const lt =
-1
const gt =
1
const eq =
0
const empty =
eq
const concat = (a, b) =>
a === eq ? b : a
export { concat, empty, eq, gt, lt }