comparator-factory-factory
v0.2.1
Published
Create comparison functions to be used for sorting arrays.
Downloads
13
Maintainers
Readme
comparator-factory-factory
Create comparison functions to be used for sorting arrays.
This is a dedicated tiny library, not aiming at a framework.
Features
- Available for both Node.js and browsers (including IE 11)
- Function-based comparison value selection
- Property-path-based comparison value selection can be implemented easily
- Handling
undefined
,null
,NaN
as first or last - String collation using native
Intl.Collator
- Locale-specific collation
- Case order: case-insensitive / upper-first / lower-first
- Numeric collation such that "1" < "2" < "10"
- See Intl.Collator at MDN for details
- Chaining comparison functions
- Designed to share the comparison rule in the product;
Creating not a comparison function directly but a comparison function factory that may be shared - Lightweight (~1.7kB minified, ~0.7kB gzipped)
Install
via npm
$ npm install comparator-factory-factory
import comparatorFactoryFactory from "comparator-factory-factory";
// const comparatorFactoryFactory = require("comparator-factory-factory");
const comparing = comparatorFactoryFactory();
[].sort(comparing());
via CDN
<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
<script>
const comparing = comparatorFactoryFactory();
[].sort(comparing());
</script>
or for modern browsers:
<script type="module">
import comparatorFactoryFactory from "https://cdn.jsdelivr.net/npm/[email protected]/index.min.mjs";
const comparing = comparatorFactoryFactory();
[].sort(comparing());
</script>
Usage & Examples
const comparing = comparatorFactoryFactory({
specials: [[null, "last"]],
collator: { caseFirst: "upper", numeric: true },
});
["A5", "A1", null, "A3", "A10", "a3", "A7"].sort(comparing());
// => ["A1", "A3", "a3", "A5", "A7", "A10", null]
["A5", "A1", null, "A3", "A10", "a3", "A7"].sort(comparing().reversed());
// => [null, "A10", "A7", "A5", "a3", "A3", "A1"]
const users = [
{ id: "01", name: "Alice", profile: { age: 17 } },
{ id: "02", name: "Bob" },
{ id: "03", profile: { age: 16 } },
{ id: "04", name: "alice", profile: { age: 15 } },
{ id: "05", name: "bob", profile: { age: 18 } },
{ id: "06", name: "Bob", profile: { age: 15 } },
];
{
const comparing = comparatorFactoryFactory();
users.sort(comparing(x => [x.profile.age, x.id]));
// => [
// { id: "02", name: "Bob" },
// { id: "04", name: "alice", profile: { age: 15 } },
// { id: "06", name: "Bob", profile: { age: 15 } },
// { id: "03", profile: { age: 16 } },
// { id: "01", name: "Alice", profile: { age: 17 } },
// { id: "05", name: "bob", profile: { age: 18 } },
// ]
}
{
const comparing1 = comparatorFactoryFactory({
specials: [[undefined, "last"]],
collator: { sensitivity: "base" },
});
const comparing2 = comparatorFactoryFactory({
specials: [[undefined, "last"]],
});
users.sort(
comparing1(x => x.name)
.reversed()
.or(comparing2(x => x.profile.age))
.or(comparing2(x => x.id))
);
// => [
// { id: "03", profile: { age: 16 } },
// { id: "06", name: "Bob", profile: { age: 15 } },
// { id: "05", name: "bob", profile: { age: 18 } },
// { id: "02", name: "Bob" },
// { id: "04", name: "alice", profile: { age: 15 } },
// { id: "01", name: "Alice", profile: { age: 17 } },
// ]
}
{
const comparingPropertyPath = comparatorFactoryFactory({
selector(fullpath) {
const paths = fullpath.replace(/\[(\d+)]/g, ".$1").split(".").filter(Boolean);
return obj => paths.every(path => (obj = obj[path]) != null) && obj;
},
});
users.sort(comparingPropertyPath("profile.age", "id"));
// => [
// { id: "02", name: "Bob" },
// { id: "04", name: "alice", profile: { age: 15 } },
// { id: "06", name: "Bob", profile: { age: 15 } },
// { id: "03", profile: { age: 16 } },
// { id: "01", name: "Alice", profile: { age: 17 } },
// { id: "05", name: "bob", profile: { age: 18 } },
// ]
}
API
Summary
// Create a comparison function factory based on the specified rule.
const comparatorFactory = comparatorFactoryFactory({
selector: key => obj => comparisonResult,
specials: [
[undefined, "first"], // array with 2 elements:
[null, "first"], // [0] value to treat specially
[NaN, "first"], // [1] "first" / "last"
],
collator: {
locales: undefined, // a BCP 47 language tag, or an array of such strings
sensitivity: "variant", // "base" / "accent" / "case" / "variant"
numeric: false,
caseFirst: "false", // "upper" / "lower" / "false" (use the locale's default)
},
});
// Create a comparison function.
const comparator = comparatorFactory(key1, key2, ...);
// Evaluate.
// 0 if obj1 and obj2 are equal,
// a negative number if obj1 is smaller,
// a positive number if obj1 is larger.
const comparisonResult = comparator(obj1, obj2);
// Create a comparison function with reverse order.
const reversedComparator = comparator.reversed();
// Comparator itself.
const comparatorItself = comparator.reversed(false);
// Create a combined comparison function.
// If comparator(obj1, obj2) is falsy, then evaluate specified comparison function.
const combinedComparator = comparator.or((obj1, obj2) => number);
comparatorFactoryFactory({ selector?, specials?, locales?, collator? }) => comparatorFactory
Create a comparison function factory based on the specified rule.
Parameters
selector: key => obj => comparisonResult
A function selecting comparison value from
key
andobj
.
The receiving parameterkey
is each argument ofcomparatorFactory(key1, key2, ...)
.
The receiving parameterobj
is each argument ofcomparator(obj1, obj2)
.
The default implementation is as follows.key => obj => { try { return key(obj); } catch { return undefined; } }
Following code is a property-path-based comparison example using lodash/get. (BTW, there are so many similar modules.)
const get = require("lodash/get"); const comparingPropertyPath = comparatorFactoryFactory({ selector: key => obj => get(obj, key), }); // for TypeScript: // const comparingPropertyPath = comparatorFactoryFactory<string>({ // selector: key => obj => get(obj, key), // }); const users = [ { id: 1, profile: { age: 18 } }, { id: 2, profile: { age: 15 } }, ]; users.sort(comparingPropertyPath("profile.age", "id"));
specials: [[value1, "first" (or "last")], [value2, "first" (or "last")], ...]
Special values to place first or last.
The default value is as follows.[ [undefined, "first"], [null, "first"], [NaN, "first"], ]
collator: { locales?, sensitivity?, numeric?, caseFirst? } | { compare: (string1, string2) => number }
String comparison method.
Possible values are as follows.- An options object for
Intl.Collator
constructor with optional propertylocales
- An object that has
compare(string1, string2) => number
method (aIntl.Collator
instance does)
See Intl.Collator at MDN for details.
The default value is the defaultIntl.Collator()
.- An options object for
comparatorFactory(key1, key2, ...) => comparator
Create a comparison function.
Parameters
key1
,key2
, ...Comparison key passed to the
selector
option of the rule.
If the length of arguments is 0,obj
itself becomes the comparison value.
comparator(obj1, obj2) => number
Evaluate.
0 if obj1
and obj2
are considered to be equal,
a negative number if obj1
is considered to be smaller than obj2
,
a positive number if obj1
is considered to be larger than obj2
.
comparator.reversed(really? = true) => comparator
Create a comparison function with reverse order.
comparator.or((obj1, obj2) => number) => comparator
Create a combined comparison function.
The combined comparison function evaluates the original comparator(obj1, obj2)
first.
If that result equals to 0 (or falsy), it evaluates specified comparison function next.
Limitation
Array.prototype.sort()
always put the undefined
at the end of the array.
This behavior is specified in the ECMAScript specification.
const comparing = comparatorFactoryFactory({
specials: [[undefined, "first"], [null, "first"], [NaN, "first"]],
});
[{ id: 3 }, { id: 1 }, { id: undefined }, { id: 7 }].sort(comparing(x => x.id));
// => [{ id: undefined }, { id: 1 }, { id: 3 }, { id: 7 }]
// As expected.
[3, 1, null, 7].sort(comparing());
// => [null, 1, 3, 7]
// As expected.
[3, 1, NaN, 7].sort(comparing());
// => [NaN, 1, 3, 7]
// As expected.
[3, 1, undefined, 7].sort(comparing());
// => [1, 3, 7, undefined]
// NOT as expected.
// The expected result is [undefined, 1, 3, 7]
// but `undefined` is always placed at the end...
License
WTFPL