forto-sorter
Version:
Fast and powerful array sorting. Sort by any property in any direction with easy to read syntax.
631 lines (526 loc) • 20.8 kB
text/typescript
import { assert } from 'chai';
import {
sort,
inPlaceSort,
createNewSortInstance,
} from '../src/sort';
describe('sort', () => {
let flatArray:number[];
let flatNaturalArray:string[];
let students:{
name:string,
dob:Date,
address:{ streetNumber?:number },
}[];
let multiPropArray:{
name:string,
lastName:string,
age:number,
unit:string,
}[];
beforeEach(() => {
flatArray = [1, 5, 3, 2, 4, 5];
flatNaturalArray = ['A10', 'A2', 'B10', 'B2'];
students = [{
name: 'Mate',
dob: new Date(1987, 14, 11),
address: { streetNumber: 3 },
}, {
name: 'Ante',
dob: new Date(1987, 14, 9),
address: {},
}, {
name: 'Dino',
dob: new Date(1987, 14, 10),
address: { streetNumber: 1 },
}];
multiPropArray = [{
name: 'aa',
lastName: 'aa',
age: 10,
unit: 'A10',
}, {
name: 'aa',
lastName: undefined,
age: 8,
unit: 'A2',
}, {
name: 'aa',
lastName: null,
age: 9,
unit: 'A01',
}, {
name: 'aa',
lastName: 'bb',
age: 11,
unit: 'C2',
}, {
name: 'bb',
lastName: 'aa',
age: 6,
unit: 'B3',
}];
});
it('Should sort flat array in ascending order', () => {
const sorted = sort(flatArray).asc();
assert.deepStrictEqual(sorted, [1, 2, 3, 4, 5, 5]);
// flatArray should not be modified
assert.deepStrictEqual(flatArray, [1, 5, 3, 2, 4, 5]);
assert.notEqual(sorted,flatArray);
});
it('Should in place sort flat array in ascending order', () => {
const sorted = inPlaceSort(flatArray).asc();
assert.deepStrictEqual(sorted, [1, 2, 3, 4, 5, 5]);
assert.deepStrictEqual(flatArray, [1, 2, 3, 4, 5, 5]);
assert.equal(sorted, flatArray);
});
it('Should sort flat array in descending order', () => {
const sorted = sort(flatArray).desc();
assert.deepStrictEqual(sorted, [5, 5, 4, 3, 2, 1]);
// Passed array is not mutated
assert.deepStrictEqual(flatArray, [1, 5, 3, 2, 4, 5]);
// Can do in place sorting
const sorted2 = inPlaceSort(flatArray).desc();
assert.equal(sorted2, flatArray);
assert.deepStrictEqual(flatArray, [5, 5, 4, 3, 2, 1]);
});
it('Should sort flat array with by sorter', () => {
const sorted = sort(flatArray).by({ asc: true });
assert.deepStrictEqual(sorted, [1, 2, 3, 4, 5, 5]);
const sorted2 = sort(flatArray).by({ desc: true });
assert.deepStrictEqual(sorted2, [5, 5, 4, 3, 2, 1]);
// Passed array is not mutated
assert.deepStrictEqual(flatArray, [1, 5, 3, 2, 4, 5]);
// Can do in place sorting
const sorted3 = inPlaceSort(flatArray).by({ desc: true });
assert.equal(sorted3, flatArray);
assert.deepStrictEqual(flatArray, [5, 5, 4, 3, 2, 1]);
});
it('Should sort by student name in ascending order', () => {
const sorted = sort(students).asc(p => p.name.toLowerCase());
assert.deepStrictEqual(['Ante', 'Dino', 'Mate'], sorted.map(p => p.name));
});
it('Should sort by student name in descending order', () => {
const sorted = sort(students).desc((p) => p.name.toLowerCase());
assert.deepStrictEqual(['Mate', 'Dino', 'Ante'], sorted.map(p => p.name));
});
it('Should sort nil values to the bottom', () => {
const sorted1 = sort(students).asc((p) => p.address.streetNumber);
assert.deepStrictEqual([1, 3, undefined], sorted1.map(p => p.address.streetNumber));
const sorted2 = sort(students).desc((p) => p.address.streetNumber);
assert.deepStrictEqual([3, 1, undefined], sorted2.map(p => p.address.streetNumber));
assert.deepStrictEqual(
sort([1, undefined, 3, null, 2]).asc(),
[1, 2, 3, null, undefined],
);
assert.deepStrictEqual(
sort([1, undefined, 3, null, 2]).desc(),
[3, 2, 1, null, undefined],
);
});
it('Should ignore values that are not sortable', () => {
assert.equal(sort('string' as any).asc(), 'string' as any);
assert.equal(sort(undefined).desc(), undefined);
assert.equal(sort(null).desc(), null);
assert.equal(sort(33 as any).asc(), 33 as any);
assert.deepStrictEqual(sort({ name: 'test' } as any).desc(), { name: 'test' } as any);
assert.equal((sort(33 as any) as any).by({ asc: true }), 33 as any);
});
it('Should sort dates correctly', () => {
const sorted = sort(students).asc('dob');
assert.deepStrictEqual(sorted.map(p => p.dob), [
new Date(1987, 14, 9),
new Date(1987, 14, 10),
new Date(1987, 14, 11),
]);
});
it('Should sort on single property when passed as array', () => {
const sorted = sort(students).asc(['name']);
assert.deepStrictEqual(['Ante', 'Dino', 'Mate'], sorted.map(p => p.name));
});
it('Should sort on multiple properties', () => {
const sorted = sort(multiPropArray).asc([
p => p.name,
p => p.lastName,
p => p.age,
]);
const sortedArray = sorted.map(arr => ({
name: arr.name,
lastName: arr.lastName,
age: arr.age,
}));
assert.deepStrictEqual(sortedArray, [
{ name: 'aa', lastName: 'aa', age: 10 },
{ name: 'aa', lastName: 'bb', age: 11 },
{ name: 'aa', lastName: undefined, age: 8 },
{ name: 'aa', lastName: null, age: 9 },
{ name: 'bb', lastName: 'aa', age: 6 },
]);
});
it('Should sort on multiple properties by string sorter', () => {
const sorted = sort(multiPropArray).asc(['name', 'age', 'lastName']);
const sortedArray = sorted.map(arr => ({
name: arr.name,
lastName: arr.lastName,
age: arr.age,
}));
assert.deepStrictEqual(sortedArray, [
{ name: 'aa', lastName: undefined, age: 8 },
{ name: 'aa', lastName: null, age: 9 },
{ name: 'aa', lastName: 'aa', age: 10 },
{ name: 'aa', lastName: 'bb', age: 11 },
{ name: 'bb', lastName: 'aa', age: 6 },
]);
});
it('Should sort on multiple mixed properties', () => {
const sorted = sort(multiPropArray).asc(['name', p => p.lastName, 'age']);
const sortedArray = sorted.map(arr => ({
name: arr.name,
lastName: arr.lastName,
age: arr.age,
}));
assert.deepStrictEqual(sortedArray, [
{ name: 'aa', lastName: 'aa', age: 10 },
{ name: 'aa', lastName: 'bb', age: 11 },
{ name: 'aa', lastName: undefined, age: 8 },
{ name: 'aa', lastName: null, age: 9 },
{ name: 'bb', lastName: 'aa', age: 6 },
]);
});
it('Should sort with all equal values', () => {
const same = [
{ name: 'a', age: 1 },
{ name: 'a', age: 1 },
];
const sorted = sort(same).asc(['name', 'age']);
assert.deepStrictEqual(sorted, [
{ name: 'a', age: 1 },
{ name: 'a', age: 1 },
]);
});
it('Should sort descending by name and ascending by lastName', () => {
const sorted = sort(multiPropArray).by([
{ desc: 'name' },
{ asc: 'lastName' },
]);
const sortedArray = sorted.map(arr => ({
name: arr.name,
lastName: arr.lastName,
}));
assert.deepStrictEqual(sortedArray, [
{ name: 'bb', lastName: 'aa' },
{ name: 'aa', lastName: 'aa' },
{ name: 'aa', lastName: 'bb' },
{ name: 'aa', lastName: undefined },
{ name: 'aa', lastName: null },
]);
});
it('Should sort ascending by name and descending by age', () => {
const sorted = sort(multiPropArray).by([
{ asc: 'name' },
{ desc: 'age' },
]);
const sortedArray = sorted.map(arr => ({ name: arr.name, age: arr.age }));
assert.deepStrictEqual(sortedArray, [
{ name: 'aa', age: 11 },
{ name: 'aa', age: 10 },
{ name: 'aa', age: 9 },
{ name: 'aa', age: 8 },
{ name: 'bb', age: 6 },
]);
});
it('Should sort ascending by lastName, descending by name and ascending by age', () => {
const sorted = sort(multiPropArray).by([
{ asc: p => p.lastName },
{ desc: p => p.name },
{ asc: p => p.age },
]);
const sortedArray = sorted.map(arr => ({
name: arr.name,
lastName: arr.lastName,
age: arr.age,
}));
assert.deepStrictEqual(sortedArray, [
{ name: 'bb', lastName: 'aa', age: 6 },
{ name: 'aa', lastName: 'aa', age: 10 },
{ name: 'aa', lastName: 'bb', age: 11 },
{ name: 'aa', lastName: undefined, age: 8 },
{ name: 'aa', lastName: null, age: 9 },
]);
});
it('Should throw error if asc or desc props not provided with object config', () => {
const errorMessage = 'Invalid sort config: Expected `asc` or `desc` property';
assert.throws(
() => sort(multiPropArray).by([{ asci: 'name' }] as any),
Error,
errorMessage,
);
assert.throws(
() => sort(multiPropArray).by([{ asc: 'lastName' }, { ass: 'name' }] as any),
Error,
errorMessage,
);
assert.throws(() => sort([1, 2]).asc(null), Error, errorMessage);
assert.throws(() => sort([1, 2]).desc([1, 2, 3] as any), Error, errorMessage);
});
it('Should throw error if both asc and dsc props provided with object config', () => {
const errorMessage = 'Invalid sort config: Ambiguous object with `asc` and `desc` config properties';
assert.throws(
() => sort(multiPropArray).by([{ asc: 'name', desc: 'lastName' }] as any),
Error,
errorMessage,
);
});
it('Should throw error if using nested property with string syntax', () => {
assert.throw(
() => sort(students).desc('address.streetNumber' as any),
Error,
'Invalid sort config: String syntax not allowed for nested properties.',
);
});
it('Should sort ascending on single property with by sorter', () => {
const sorted = sort(multiPropArray).by([{ asc: p => p.age }]);
assert.deepStrictEqual([6, 8, 9, 10, 11], sorted.map(m => m.age));
});
it('Should sort descending on single property with by sorter', () => {
const sorted = sort(multiPropArray).by([{ desc: 'age' }]);
assert.deepStrictEqual([11, 10, 9, 8, 6], sorted.map(m => m.age));
});
it('Should sort flat array in asc order using natural sort comparer', () => {
const sorted = sort(flatNaturalArray).by([{
asc: true,
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare,
}]);
assert.deepStrictEqual(sorted, ['A2', 'A10', 'B2', 'B10']);
});
it('Should sort flat array in desc order using natural sort comparer', () => {
const sorted = sort(flatNaturalArray).by([{
desc: true,
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare,
}]);
assert.deepStrictEqual(sorted, ['B10', 'B2', 'A10', 'A2']);
});
it('Should sort object in asc order using natural sort comparer', () => {
const sorted = sort(multiPropArray).by([{
asc: p => p.unit,
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare,
}]);
assert.deepStrictEqual(['A01', 'A2', 'A10', 'B3', 'C2'], sorted.map(m => m.unit));
});
it('Should sort object in desc order using natural sort comparer', () => {
const sorted = sort(multiPropArray).by([{
desc: p => p.unit,
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare,
}]);
assert.deepStrictEqual(['C2', 'B3', 'A10', 'A2', 'A01'], sorted.map(m => m.unit));
});
it('Should sort object on multiple props using both default and custom comparer', () => {
const testArr = [
{ a: 'A2', b: 'A2' },
{ a: 'A2', b: 'A10' },
{ a: 'A10', b: 'A2' },
];
const sorted = sort(testArr).by([{
desc: p => p.a,
}, {
asc: 'b',
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare,
}]);
assert.deepStrictEqual(sorted, [
{ a: 'A2', b: 'A2' },
{ a: 'A2', b: 'A10' }, // <= B is sorted using natural sort comparer
{ a: 'A10', b: 'A2' }, // <= A is sorted using default sort comparer
]);
});
// BUG repo case: https://github.com/snovakovic/fast-sort/issues/18
it('Sort by comparer should not override default sort of other array property', () => {
const rows = [
{ status: 0, title: 'A' },
{ status: 0, title: 'D' },
{ status: 0, title: 'B' },
{ status: 1, title: 'C' },
];
const sorted = sort(rows).by([{
asc: row => row.status,
comparer: (a, b) => a - b,
}, {
asc: row => row.title,
}]);
assert.deepStrictEqual(sorted, [
{ status: 0, title: 'A' },
{ status: 0, title: 'B' },
{ status: 0, title: 'D' },
{ status: 1, title: 'C' },
]);
});
it('Should create natural sort instance and handle sorting correctly', () => {
const naturalSort = createNewSortInstance({
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare,
});
const sorted1 = naturalSort(multiPropArray).desc('unit');
assert.deepStrictEqual(['C2', 'B3', 'A10', 'A2', 'A01'], sorted1.map(m => m.unit));
const sorted2 = naturalSort(multiPropArray).by({ asc: 'unit' });
assert.deepStrictEqual(['A01', 'A2', 'A10', 'B3', 'C2'], sorted2.map(m => m.unit));
const sorted3 = naturalSort(multiPropArray).asc('lastName');
assert.deepStrictEqual(['aa', 'aa', 'bb', null, undefined], sorted3.map(m => m.lastName));
const sorted4 = naturalSort(multiPropArray).desc(p => p.lastName);
assert.deepStrictEqual([undefined, null, 'bb', 'aa', 'aa'], sorted4.map(m => m.lastName));
const sorted5 = naturalSort(flatArray).desc();
assert.deepStrictEqual(sorted5, [5, 5, 4, 3, 2, 1]);
const sorted6 = naturalSort(flatNaturalArray).asc();
assert.deepStrictEqual(sorted6, ['A2', 'A10', 'B2', 'B10']);
});
it('Should handle sorting on multiples props with custom sorter instance', () => {
const naturalSort = createNewSortInstance({
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare,
});
const arr = [
{ a: 'a', b: 'A2' },
{ a: 'a', b: 'A20' },
{ a: 'a', b: null },
{ a: 'a', b: 'A3' },
{ a: 'a', b: undefined },
];
const sort1 = naturalSort(arr).asc('b');
assert.deepStrictEqual(sort1.map(a => a.b), ['A2', 'A3', 'A20', null, undefined]);
const sorted2 = naturalSort(arr).asc(['a', 'b']);
assert.deepStrictEqual(sorted2.map(a => a.b), ['A2', 'A3', 'A20', null, undefined]);
const sorted3 = naturalSort(arr).desc('b');
assert.deepStrictEqual(sorted3.map(a => a.b), [undefined, null, 'A20', 'A3', 'A2']);
const sorted4 = naturalSort(arr).desc(['a', 'b']);
assert.deepStrictEqual(sorted4.map(a => a.b), [undefined, null, 'A20', 'A3', 'A2']);
});
it('Should create custom tag sorter instance', () => {
const tagImportance = { vip: 3, influencer: 2, captain: 1 };
const customTagComparer = (a, b) => (tagImportance[a] || 0) - (tagImportance[b] || 0);
const tags = ['influencer', 'unknown', 'vip', 'captain'];
const tagSorter = createNewSortInstance({ comparer: customTagComparer });
assert.deepStrictEqual(tagSorter(tags).asc(), ['unknown', 'captain', 'influencer', 'vip']);
assert.deepStrictEqual(tagSorter(tags).desc(), ['vip', 'influencer', 'captain', 'unknown']);
assert.deepStrictEqual(sort(tags).asc(tag => tagImportance[tag] || 0), ['unknown', 'captain', 'influencer', 'vip']);
});
it('Should be able to override natural sort comparer', () => {
const naturalSort = createNewSortInstance({
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare,
});
const sorted1 = naturalSort(multiPropArray).by([{
asc: 'name',
}, {
desc: 'unit',
comparer(a, b) { // NOTE: override natural sort
if (a === b) return 0;
return a < b ? -1 : 1;
},
}]);
let sortedArray = sorted1.map(arr => ({ name: arr.name, unit: arr.unit }));
assert.deepStrictEqual(sortedArray, [
{ name: 'aa', unit: 'C2' },
{ name: 'aa', unit: 'A2' },
{ name: 'aa', unit: 'A10' },
{ name: 'aa', unit: 'A01' },
{ name: 'bb', unit: 'B3' },
]);
const sorted2 = naturalSort(multiPropArray).by([{ asc: 'name' }, { desc: 'unit' }]);
sortedArray = sorted2.map(arr => ({ name: arr.name, unit: arr.unit }));
assert.deepStrictEqual(sortedArray, [
{ name: 'aa', unit: 'C2' },
{ name: 'aa', unit: 'A10' },
{ name: 'aa', unit: 'A2' },
{ name: 'aa', unit: 'A01' },
{ name: 'bb', unit: 'B3' },
]);
});
it('Should sort in asc order with by sorter if object config not provided', () => {
const sorted = sort(multiPropArray).by(['name', 'unit'] as any);
const sortedArray = sorted.map(arr => ({ name: arr.name, unit: arr.unit }));
assert.deepStrictEqual(sortedArray, [
{ name: 'aa', unit: 'A01' },
{ name: 'aa', unit: 'A10' },
{ name: 'aa', unit: 'A2' },
{ name: 'aa', unit: 'C2' },
{ name: 'bb', unit: 'B3' },
]);
});
it('Should ignore empty array as a sorting prop', () => {
assert.deepStrictEqual(sort([2, 1, 4]).asc([]), [1, 2, 4]);
});
it('Should sort by computed property', () => {
const repos = [
{ openIssues: 0, closedIssues: 5 },
{ openIssues: 4, closedIssues: 4 },
{ openIssues: 3, closedIssues: 3 },
];
const sorted1 = sort(repos).asc(r => r.openIssues + r.closedIssues);
assert.deepStrictEqual(sorted1, [
{ openIssues: 0, closedIssues: 5 },
{ openIssues: 3, closedIssues: 3 },
{ openIssues: 4, closedIssues: 4 },
]);
const sorted2 = sort(repos).desc(r => r.openIssues + r.closedIssues);
assert.deepStrictEqual(sorted2, [
{ openIssues: 4, closedIssues: 4 },
{ openIssues: 3, closedIssues: 3 },
{ openIssues: 0, closedIssues: 5 },
]);
});
it('Should not mutate sort by array', () => {
const sortBy = [{ asc: 'name' }, { asc: 'unit' }];
const sorted = sort(multiPropArray).by(sortBy as any);
assert.deepStrictEqual(sortBy, [{ asc: 'name' }, { asc: 'unit' }]);
const sortedArray = sorted.map(arr => ({ name: arr.name, unit: arr.unit }));
assert.deepStrictEqual(sortedArray, [
{ name: 'aa', unit: 'A01' },
{ name: 'aa', unit: 'A10' },
{ name: 'aa', unit: 'A2' },
{ name: 'aa', unit: 'C2' },
{ name: 'bb', unit: 'B3' },
]);
});
it('Should sort readme example for natural sort correctly', () => {
const testArr = ['image-2.jpg', 'image-11.jpg', 'image-3.jpg'];
// By default fast-sort is not doing natural sort
const sorted1 = sort(testArr).desc(); // =>
assert.deepStrictEqual(sorted1, ['image-3.jpg', 'image-2.jpg', 'image-11.jpg']);
const sorted2 = sort(testArr).by({
desc: true,
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare,
});
assert.deepStrictEqual(sorted2, ['image-11.jpg', 'image-3.jpg', 'image-2.jpg']);
// If we want to reuse natural sort in multiple places we can create new sort instance
const naturalSort = createNewSortInstance({
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare,
});
const sorted3 = naturalSort(testArr).asc();
assert.deepStrictEqual(sorted3, ['image-2.jpg', 'image-3.jpg', 'image-11.jpg']);
const sorted4 = naturalSort(testArr).desc();
assert.deepStrictEqual(sorted4, ['image-11.jpg', 'image-3.jpg', 'image-2.jpg']);
assert.notEqual(sorted3, testArr);
});
it('Should create sort instance that sorts nil value to the top in desc order', () => {
const nilSort = createNewSortInstance({
comparer(a, b):number {
if (a == null) return 1;
if (b == null) return -1;
if (a < b) return -1;
if (a === b) return 0;
return 1;
},
});
const sorter1 = nilSort(multiPropArray).asc(p => p.lastName);
assert.deepStrictEqual(['aa', 'aa', 'bb', undefined, null], sorter1.map(p => p.lastName));
const sorter2 = nilSort(multiPropArray).desc(p => p.lastName);
assert.deepStrictEqual([null, undefined, 'bb', 'aa', 'aa'], sorter2.map(p => p.lastName));
// By default custom sorter should not mutate provided array
assert.notEqual(sorter1, multiPropArray);
assert.notEqual(sorter2, multiPropArray);
});
it('Should mutate array with custom sorter if inPlaceSorting provided', () => {
const customInPlaceSorting = createNewSortInstance({
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare,
inPlaceSorting: true, // <= NOTE
});
const sorted = customInPlaceSorting(flatArray).asc();
assert.equal(sorted, flatArray);
assert.deepStrictEqual(flatArray, [1, 2, 3, 4, 5, 5]);
});
});