Skip to content

Instantly share code, notes, and snippets.

@jherax
Last active July 19, 2024 00:12
Show Gist options
  • Save jherax/ce5d7ba5f7bba519a575e1dfe9cd92c8 to your computer and use it in GitHub Desktop.
Save jherax/ce5d7ba5f7bba519a575e1dfe9cd92c8 to your computer and use it in GitHub Desktop.
Sorts an array with multiple ordering criteria (Schwartzian transform)
/**
* Sorts an array of objects In-Place,
* sorting by multiple fields sequentially.
*
* @description This function is meant to be used
* with arrays of objects, AND when you need to set
* multiple sorting criteria. For other cases it is
* recommended to use the native method `array.sort(callback)`
* since for simple cases this function is more expensive
* in time and memory.
*
* @param array List of objects to sort
* @param parser Function that returns an array with the fields to sort
* @returns Original array sorted
*/
function arraySortBy<T extends PlainObject>(
array: T[],
parser: (item: T) => Decorated[],
): T[] {
let item: T;
// Schwartzian transform (decorate-sort-undecorate)
for (let i = array.length; i; ) {
item = array[--i];
// decorate items in array, by appending the sorting criteria
(array[i] as Decorated) = [item, ...parser(item)];
}
// sort the decorated array
(array as Decorated[]).sort(sortItems);
// undecorate the array
for (let i = array.length; i; ) {
item = array[--i];
array[i] = (item as Decorated)[0];
}
return array;
}
function sortItems(a: Decorated[], b: Decorated[]): number {
// first position is the original element in the array
// and the following values are the sorting criteria
for (let i = 1, sorted = 0; i < a.length; i += 1) {
// sort by multiple fields sequentially
sorted = comparer(a[i], b[i]);
if (sorted) return sorted;
}
return 0;
}
function comparer<T>(a: T, b: T): number {
if (typeof a !== 'string') {
if (a === b) return 0;
return a > b ? 1 : -1;
}
// used this convention for descending sort,
// to not care about case sensitivity.
if (a.startsWith('<<:')) [a, b] = [b, a];
return (a as string).localeCompare(b as string);
}
// item T is converted to unknown[]
// when decorating the array, and T
// is incompatible with unknown[]
type Decorated = unknown;
type PlainObject = {
[key: string]: unknown;
};
describe('Testing arraySortBy()', () => {
it('should sort the array by name (asc) and by age (desc)', () => {
const array = [
{id: 6, age: 21, name: 'Pedro'},
{id: 9, age: 26, name: 'pedro'},
{id: 2, age: 26, name: 'maría'},
{id: 7, age: 32, name: 'Maria'},
];
// to indicate descending order for numbers,
// just convert it to a negative value.
arraySortBy(array, item => [item.name, -item.age]);
expect(array).toStrictEqual([
{id: 7, age: 32, name: 'Maria'},
{id: 2, age: 26, name: 'maría'},
{id: 9, age: 26, name: 'pedro'},
{id: 6, age: 21, name: 'Pedro'},
]);
});
it('should sort the array by status (desc) and by birthday (desc)', () => {
const array = [
{birthday: '1983/03/06', name: 'David', task: 'Raw material', status: 'Completed'},
{birthday: '1980/12/24', name: 'Marlin', task: 'Job material', status: 'Not started'},
{birthday: '1985/08/31', name: 'Töm', task: 'Scrap material', status: 'Completed'},
{birthday: '1983/03/05', name: 'Mark', task: 'Raw material', status: 'In progress'},
];
// for descending order in strings,
// just prepend the key "<<:"
arraySortBy(array, item => [`<<:${item.status}`, -new Date(item.birthday)]);
expect(array).toStrictEqual([
{birthday: '1980/12/24', name: 'Marlin', task: 'Job material', status: 'Not started'},
{birthday: '1983/03/05', name: 'Mark', task: 'Raw material', status: 'In progress'},
{birthday: '1985/08/31', name: 'Töm', task: 'Scrap material', status: 'Completed'},
{birthday: '1983/03/06', name: 'David', task: 'Raw material', status: 'Completed'},
]);
});
});
@jherax
Copy link
Author

jherax commented Oct 21, 2016

You can find a similar approach in the following github repo:
https://github.com/jherax/array-sort-by

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment