UNPKG

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
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 };