forto-sorter
Version:
Fast and powerful array sorting. Sort by any property in any direction with easy to read syntax.
490 lines (473 loc) • 17.1 kB
JavaScript
var defaultComparer = function (a, b, order) {
if (a == null)
return order;
if (b == null)
return -order;
if (a < b)
return -1;
if (a === b)
return 0;
return 1;
};
var castComparer = function (comparer) { return function (a, b, order) { return comparer(a, b, order) * order; }; };
var throwInvalidConfigErrorIfTrue = function (condition, context) {
if (condition)
throw Error("Invalid sort config: " + context);
};
var unpackObjectSorter = function (sortByObj) {
var _a = sortByObj || {}, asc = _a.asc, desc = _a.desc;
var order = asc ? 1 : -1;
var sortBy = (asc || desc);
// Validate object config
throwInvalidConfigErrorIfTrue(!sortBy, 'Expected `asc` or `desc` property');
throwInvalidConfigErrorIfTrue(asc && desc, 'Ambiguous object with `asc` and `desc` config properties');
var comparer = sortByObj.comparer && castComparer(sortByObj.comparer);
return { order: order, sortBy: sortBy, comparer: comparer };
};
var multiPropertySorterProvider = function (defaultComparer) {
return function multiPropertySorter(sortBy, sortByArr, depth, order, comparer, a, b) {
var valA;
var valB;
if (typeof sortBy === 'string') {
valA = a[sortBy];
valB = b[sortBy];
}
else if (typeof sortBy === 'function') {
valA = sortBy(a);
valB = sortBy(b);
}
else {
var objectSorterConfig = unpackObjectSorter(sortBy);
return multiPropertySorter(objectSorterConfig.sortBy, sortByArr, depth, objectSorterConfig.order, objectSorterConfig.comparer || defaultComparer, a, b);
}
var equality = comparer(valA, valB, order);
if ((equality === 0 || (valA == null && valB == null)) &&
sortByArr.length > depth) {
return multiPropertySorter(sortByArr[depth], sortByArr, depth + 1, order, comparer, a, b);
}
return equality;
};
};
function sortStrategy(sortBy, comparer, order) {
// Flat array sorter
if (sortBy === undefined || sortBy === true) {
return function (a, b) { return comparer(a, b, order); };
}
// Sort list of objects by single object key
if (typeof sortBy === 'string') {
throwInvalidConfigErrorIfTrue(sortBy.includes('.'), 'String syntax not allowed for nested properties.');
return function (a, b) { return comparer(a[sortBy], b[sortBy], order); };
}
// Sort list of objects by single function sorter
if (typeof sortBy === 'function') {
return function (a, b) { return comparer(sortBy(a), sortBy(b), order); };
}
// Sort by multiple properties
if (Array.isArray(sortBy)) {
var multiPropSorter_1 = multiPropertySorterProvider(comparer);
return function (a, b) { return multiPropSorter_1(sortBy[0], sortBy, 1, order, comparer, a, b); };
}
// Unpack object config to get actual sorter strategy
var objectSorterConfig = unpackObjectSorter(sortBy);
return sortStrategy(objectSorterConfig.sortBy, objectSorterConfig.comparer || comparer, objectSorterConfig.order);
}
var sortArray = function (order, ctx, sortBy, comparer) {
var _a;
if (!Array.isArray(ctx)) {
return ctx;
}
// Unwrap sortBy if array with only 1 value to get faster sort strategy
if (Array.isArray(sortBy) && sortBy.length < 2) {
_a = sortBy, sortBy = _a[0];
}
return ctx.sort(sortStrategy(sortBy, comparer, order));
};
var sortByAlgo = function (algorithm, input, compare) {
if (compare === void 0) { compare = function (a, b) { return (a > b ? 1 : -1); }; }
var list = input.list;
if (!Array.isArray(list)) {
throw new Error('Sort: invalid array');
}
if (typeof compare !== 'function') {
throw new Error('Sort: "compare" must be a function');
}
/**
* check if two elements should be swapped
* @return {boolean}
*/
var shouldSwap = function (i, j) { return (compare(list[i], list[j]) > 0); };
/**
* swap positions of two elements in the list
*/
var swap = function (i, j) {
var temp = list[j];
list[j] = list[i];
list[i] = temp;
};
return algorithm(input, { shouldSwap: shouldSwap, swap: swap, compare: compare });
};
var bubbleSort = function (_a, _b) {
var list = _a.list;
var shouldSwap = _b.shouldSwap, swap = _b.swap;
var upperIndex = list.length - 1;
while (upperIndex > 0) {
var swapIndex = 0;
for (var i = 0; i < upperIndex; i += 1) {
if (shouldSwap(i, i + 1)) {
swap(i, i + 1);
swapIndex = i;
}
}
upperIndex = swapIndex;
}
return list;
};
var insertionSort = function (_a, _b) {
var list = _a.list;
var shouldSwap = _b.shouldSwap, swap = _b.swap;
for (var i = 1; i < list.length; i += 1) {
var insertionIndex = i;
for (var j = i - 1; j >= 0; j -= 1) {
if (shouldSwap(j, insertionIndex)) {
swap(j, insertionIndex);
insertionIndex = j;
}
}
}
return list;
};
var selectionSort = function (_a, _b) {
var list = _a.list;
var shouldSwap = _b.shouldSwap, swap = _b.swap;
for (var i = 0; i < list.length - 1; i += 1) {
var selectedIndex = i;
for (var j = i + 1; j < list.length; j += 1) {
if (shouldSwap(selectedIndex, j)) {
selectedIndex = j;
}
}
if (selectedIndex !== i) {
swap(selectedIndex, i);
}
}
return list;
};
var radixSort = function (_a) {
var list = _a.list, _b = _a.order, order = _b === void 0 ? 'asc' : _b, _c = _a.getNumber, getNumber = _c === void 0 ? (function (n) { return n; }) : _c;
if (order && !['asc', 'desc'].includes(order)) {
throw new Error('radix sort: order is either "asc" or "desc"');
}
if (getNumber && typeof getNumber !== 'function') {
throw new Error('radix sort: invalid getNumber callback');
}
var sortPartition = function (numbers, max) {
// 0 - 9 buckets
var buckets = [[], [], [], [], [], [], [], [], [], []];
var sorted = numbers;
var _loop_1 = function (i) {
for (var j = 0; j < sorted.length; j += 1) {
var n = Math.abs(getNumber(sorted[j])).toString();
var digit = n[n.length - 1 - i] || 0;
buckets[digit].push(sorted[j]);
}
var temp = [];
buckets.forEach(function (bucket, digit) {
temp = temp.concat(bucket);
buckets[digit] = [];
});
sorted = temp;
};
// sort numbers into buckets starting from least significant digit
for (var i = 0; i < ("" + max).length; i += 1) {
_loop_1(i);
}
return sorted;
};
var positive = [];
var negative = [];
var maxPositive = -Infinity;
var minNegative = Infinity;
for (var i = 0; i < list.length; i += 1) {
var n = getNumber(list[i]);
if (Number.isNaN(+n)) {
throw new Error("radix sort: invalid numeric value: " + n);
}
if (n >= 0) {
positive.push(list[i]);
if (n > maxPositive) {
maxPositive = n;
}
}
else {
negative.push(list[i]);
if (n < minNegative) {
minNegative = n;
}
}
}
var sortedPositive = sortPartition(positive, maxPositive);
var sortedNegative = sortPartition(negative, -minNegative);
// merge sorted numbers
var sorted = [];
var first = order === 'asc' ? sortedNegative : sortedPositive;
var second = order === 'asc' ? sortedPositive : sortedNegative;
for (var i = first.length - 1; i >= 0; i -= 1) {
sorted.push(first[i]);
}
for (var i = 0; i < second.length; i += 1) {
sorted.push(second[i]);
}
sorted.forEach(function (n, i) {
list[i] = n; // eslint-disable-line no-param-reassign
});
return list;
};
var heapSort = function (_a, _b) {
var list = _a.list;
var compare = _b.compare, swap = _b.swap;
/**
* calculates a parent's index from a child's index
* @param {number} childIndex
* @returns {number}
*/
var getParentIndex = function (childIndex) { return (Math.floor((childIndex - 1) / 2)); };
/**
* calculates the left child's index of a parent's index
* @param {number} parentIndex
* @returns {number}
*/
var getLeftChildIndex = function (parentIndex) { return ((parentIndex * 2) + 1); };
/**
* calculates the right child's index of a parent's index
* @param {number} parentIndex
* @returns {number}
*/
var getRightChildIndex = function (parentIndex) { return ((parentIndex * 2) + 2); };
/**
* @return {boolean}
*/
var shouldSwap = function (i, j) { return (i >= 0 && j >= 0
&& i < list.length
&& j < list.length
&& compare(list[j], list[i]) > 0); };
/**
* bubbles an element at a position up in the heap
*/
var heapifyUp = function (i) {
var childIndex = i;
var parentIndex = getParentIndex(childIndex);
while (shouldSwap(parentIndex, childIndex)) {
swap(parentIndex, childIndex);
childIndex = parentIndex;
parentIndex = getParentIndex(childIndex);
}
};
/**
* converts the array into a heap
*/
var heapify = function () {
for (var i = 0; i < list.length; i += 1) {
heapifyUp(i);
}
return list;
};
/**
* @param {number}
*/
var compareChildrenBefore = function (i, leftIndex, rightIndex) {
if (shouldSwap(leftIndex, rightIndex) && rightIndex < i) {
return rightIndex;
}
return leftIndex;
};
/**
* pushes the swapped element with root down to its correct location
* @param {number} i - swapped node's index
*/
var heapifyDownUntil = function (i) {
var parentIndex = 0;
var leftIndex = 1;
var rightIndex = 2;
var childIndex;
while (leftIndex < i) {
childIndex = compareChildrenBefore(i, leftIndex, rightIndex);
if (shouldSwap(parentIndex, childIndex)) {
swap(parentIndex, childIndex);
}
parentIndex = childIndex;
leftIndex = getLeftChildIndex(parentIndex);
rightIndex = getRightChildIndex(parentIndex);
}
};
heapify();
for (var i = list.length - 1; i > 0; i -= 1) {
swap(0, i);
heapifyDownUntil(i);
}
return list;
};
var quickSort = function (_a, _b) {
var list = _a.list;
var shouldSwap = _b.shouldSwap, swap = _b.swap;
var sortRecursively = function (startIndex, endIndex) {
if (startIndex === void 0) { startIndex = 0; }
if (endIndex === void 0) { endIndex = list.length - 1; }
var pivotIndex = startIndex;
var lowerIndex = startIndex;
var higherIndex = endIndex;
while (lowerIndex <= higherIndex) {
while (shouldSwap(pivotIndex, lowerIndex) && lowerIndex < endIndex) {
lowerIndex += 1;
}
while (!shouldSwap(pivotIndex, higherIndex) && higherIndex > startIndex) {
higherIndex -= 1;
}
if (lowerIndex <= higherIndex) {
swap(lowerIndex, higherIndex);
lowerIndex += 1;
higherIndex -= 1;
}
}
if (startIndex < higherIndex) {
sortRecursively(startIndex, higherIndex);
}
if (lowerIndex < endIndex) {
sortRecursively(lowerIndex, endIndex);
}
return list;
};
return sortRecursively();
};
var mergeSort = function (_a, _b) {
var list = _a.list;
var shouldSwap = _b.shouldSwap;
/**
* @return {number}
*/
var getMiddleIndex = function (startIndex, endIndex) { return (startIndex + Math.floor((endIndex - startIndex) / 2)); };
/**
* merges two sorted partitions using a temporary array
* @return {array} the start and end of a partition
*/
var partitionAndSort = function (startIndex, endIndex) {
if (startIndex === void 0) { startIndex = 0; }
if (endIndex === void 0) { endIndex = list.length - 1; }
if (endIndex - startIndex <= 0)
return [startIndex, startIndex + 1];
var middleIndex = getMiddleIndex(startIndex, endIndex);
var leftPartition = partitionAndSort(startIndex, middleIndex);
var rightPartition = partitionAndSort(middleIndex + 1, endIndex);
var leftStart = leftPartition[0], leftEnd = leftPartition[1];
var rightStart = rightPartition[0], rightEnd = rightPartition[1];
var merged = [];
var leftIndex = leftStart;
var rightIndex = rightStart;
while (leftIndex < leftEnd || rightIndex < rightEnd) {
if (rightIndex === rightEnd) {
merged.push(list[leftIndex]);
leftIndex += 1;
}
else if (leftIndex === leftEnd) {
merged.push(list[rightIndex]);
rightIndex += 1;
}
else if (shouldSwap(leftIndex, rightIndex)) {
merged.push(list[rightIndex]);
rightIndex += 1;
}
else {
merged.push(list[leftIndex]);
leftIndex += 1;
}
}
// update list segment with merged partition
var j = 0;
for (var i = leftStart; i < rightEnd; i += 1) {
list[i] = merged[j]; // eslint-disable-line no-param-reassign
j += 1;
}
return [leftStart, rightEnd];
};
partitionAndSort();
return list;
};
// Comparers
var createNewSortInstance = function (opts) {
var comparer = castComparer(opts.comparer);
return function (_ctx) {
var ctx = Array.isArray(_ctx) && !opts.inPlaceSorting ? _ctx.slice() : _ctx;
return {
/**
* Sort array in ascending order.
* @example
* sort([3, 1, 4]).asc();
* sort(users).asc(u => u.firstName);
* sort(users).asc([
* U => u.firstName
* u => u.lastName,
* ]);
*/
asc: function (sortBy) {
return sortArray(1, ctx, sortBy, comparer);
},
/**
* Sort array in descending order.
* @example
* sort([3, 1, 4]).desc();
* sort(users).desc(u => u.firstName);
* sort(users).desc([
* U => u.firstName
* u => u.lastName,
* ]);
*/
desc: function (sortBy) {
return sortArray(-1, ctx, sortBy, comparer);
},
/**
* Sort array in ascending or descending order. It allows sorting on multiple props
* in different order for each of them.
* @example
* sort(users).by([
* { asc: u => u.score }
* { desc: u => u.age }
* ]);
*/
by: function (sortBy) {
return sortArray(1, ctx, sortBy, comparer);
},
};
};
};
var sort = createNewSortInstance({
comparer: defaultComparer,
});
var inPlaceSort = createNewSortInstance({
comparer: defaultComparer,
inPlaceSorting: true,
});
var sortByAlgorithm = function (algorithmName, list, compare) {
switch (algorithmName) {
case 'bubble':
sortByAlgo(bubbleSort, { list: list }, compare);
break;
case 'insertion':
sortByAlgo(insertionSort, { list: list }, compare);
break;
case 'selection':
sortByAlgo(selectionSort, { list: list }, compare);
break;
case 'radix':
sortByAlgo(radixSort, { list: list }, compare);
break;
case 'heap':
sortByAlgo(heapSort, { list: list }, compare);
break;
case 'quick':
sortByAlgo(quickSort, { list: list }, compare);
break;
case 'merge':
sortByAlgo(mergeSort, { list: list }, compare);
break;
}
};
export { createNewSortInstance, inPlaceSort, sort, sortByAlgorithm };