juxta
A library for writing composable comparison functions.
juxta has the following features:
- Composable. You can express "sort by X then by Y" and "sort X before Y" using a uniform API.
- Type-safe. juxta has complete TypeScript definitions. If it compiles, it (probably) works.
- Readable. Code that uses juxta is much easier to understand and audit than the equivalent written out in full.
Example
;;; ; ; ; results.sortcompareSearchResults;
Why juxta?
JavaScript provides a built-in method to sort arrays, called Array.prototype.sort
. You can customize how it compares elements by passing a comparison function to the method.
Unfortunately, these comparison functions can be hard to write and understand. For example, here's a function that compares nullable strings, sorting null
values last:
I'd hate to be the person reviewing that code.
Fun fact! There's a bug in that example! Can you find it?
With juxta, this function can be written as follows:
;
That's much less typing -- and more importantly, it is guaranteed correct.
Creating comparison functions
juxta exposes its API through a default export. By convention we name it compare
:
;
There are three main ways to create a comparison function:
- Use
compare<T>()
to compare values of typeT
using the built-in<
and>
operators. For example,compare<number>()
compares values of typenumber
. - Use
compare(existingFunction)
to wrap an existing comparison function in a juxta object. This lets you use the helper methods detailed below. For example,compare((s: string, t: string) => s.localeCompare(t))
compares strings case-insensitively using the current locale. - Use
compare.on(...)
to transform the input before comparing it. For example,compare.on((x: any[]) => x.length)
compares arrays by length.
Using comparison functions
All juxta objects are functions, so you can pass them directly to .sort()
:
;console.log.sortcompareNumbers; // [1, 2, 3]
Ascending vs descending order
compare()
and compare.on()
use ascending order (smallest first) by default.
To sort by descending order (largest first) instead, use the .reverse()
method: compare<number>().reverse()
.
Calling .reverse()
twice gives the same result as calling it zero times.
Transforming the input
Each elf has a hat, and each hat has a bauble. We want to sort elves by the baubles on their hats. (The comparison of baubles is a solved problem and has been defined elsewhere.)
This can be written as follows:
;
Note that since we're transforming inputs, not outputs, the method calls may look "backwards" to what you would expect. This is apparent when using more than one .from()
call:
.fromh.bauble .frome.hat;
Sorting by multiple fields
On testing, it was found that there are elves with identical baubles. In this case, they can be distinguished by the colors of their socks.
To sort by more than one property, chain the comparison functions using .then()
:
.thencompare.one.sock.color;
Partitioning the input into groups
Oh no! Some elves have rebelled against the social order, and replaced the baubles on their hats with trinkets. Your assistant has provided you with two options: either punish the "trinketeers" by sorting them last, or cede to their demands and sort them first. Luckily, juxta allows for both:
// TODO: implement sock colors under the new regime .prepende.hasTrinket, compareElvesByTrinkets; .appende.hasTrinket, compareElvesByTrinkets;
In more peaceful times, .prepend()
and .append()
can be used for separating null
, undefined
, and NaN
values as well:
; ; .prepend_.isNull; .append_.isUndefined; .prepend_.isNumber, compareNumbers;
In the definition of compareStrings
, the .prepend()
and .append()
calls together extend the input type from string
to string | null | undefined
. These type changes can confuse the TypeScript compiler; writing out the generic parameters explicitly (<null>
and <undefined>
) helps it along.
Type annotations
juxta uses advanced TypeScript features to model its API. This means that you may need to write more type annotations than usual when using the library. Here are some general tips for using TypeScript with juxta:
-
Enable
noImplicitAny
. This ensures that if TypeScript fails to infer a type, it will raise an error instead of defaulting toany
. -
If a method takes a callback, give explicit types to each of the callback's arguments. If a method takes generic parameters, fill out each parameter. The examples in this documentation tend to follow these rules, so you're okay if you copy from them.
-
If you use an IDE such as Visual Studio Code, you can inspect the inferred type of any expression by hovering over it. This can help debug confusing type errors.
Case-insensitive string comparisons
Unlike some other comparison libraries, juxta does not provide a simple way to compare strings case-insensitively. This is because string comparison is a subtle topic that many people get wrong. I do not want to add foot-guns by presenting things as less complex than they really are.
juxta does provide compare.locale()
, which wraps the built-in Intl.Collator
object. For example, compare.locale('en', { sensitivity: 'base' })
will compare case-insensitively according to English sorting rules.