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.

. 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.

. 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

import compare , { Comparator } from ' juxta ' ; import _ from ' lodash ' ; import moment from ' moment ' ; interface SearchResult { ; ; ; } ; ; ; results . sort ( compareSearchResults ) ;

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:

function compareStringsNullLast : number { if ( a === null && b === null ) { return 0 ; } else if ( a === null && b !== null ) { return - 1 ; } else if ( a !== null && b === null ) { return 1 ; } else { return a < b ? - 1 : a > b ? 1 : 0 ; } }

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 :

import compare from ' juxta ' ;

There are three main ways to create a comparison function:

Use compare<T>() to compare values of type T using the built-in < and > operators. For example, compare<number>() compares values of type number .

to compare values of type using the built-in and operators. For example, compares values of type . 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.

to wrap an existing comparison function in a juxta object. This lets you use the helper methods detailed below. For example, 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 ( [ 3 , 2 , 1 ] . sort ( compareNumbers ) ) ;

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:

. from ( h . bauble ) . from ( e . 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() :

. then ( compare . on ( e . 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:

. prepend ( e . hasTrinket ( ) , compareElvesByTrinkets ) ; . append ( e . hasTrinket ( ) , compareElvesByTrinkets ) ;

In more peaceful times, .prepend() and .append() can be used for separating null , undefined , and NaN values as well:

import _ from ' lodash ' ; ; . prepend < null > ( _ . isNull ) ; . append < undefined > ( _ . isUndefined ) ; . prepend < number > ( _ . 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 to any .

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.