├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── benchmark.jpg ├── benchmark ├── getRandomInt.js ├── implementations │ ├── base.js │ ├── deepObject.js │ ├── flatArray.js │ ├── flatObject.js │ └── multiProperty.js ├── index.js ├── package-lock.json ├── package.json └── runner.js ├── dist ├── sort.cjs.js ├── sort.d.ts ├── sort.js ├── sort.min.d.ts ├── sort.min.js └── sort.mjs ├── package-lock.json ├── package.json ├── rollup.config.js ├── src └── sort.ts ├── test ├── integration │ ├── dist.test.js │ ├── npm.test.js │ ├── package-lock.json │ └── package.json └── sort.spec.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs. 2 | # More information at http://EditorConfig.org 3 | 4 | # No .editorconfig files above the root directory 5 | root = true 6 | 7 | [*] 8 | indent_style = space 9 | indent_size = 2 10 | end_of_line = lf 11 | charset = utf-8 12 | trim_trailing_whitespace = true 13 | insert_final_newline = true 14 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | ecmaVersion: 2018, 5 | sourceType: 'module', 6 | }, 7 | extends: [ 8 | 'airbnb-base', 9 | 'plugin:@typescript-eslint/recommended', 10 | ], 11 | env: { 12 | node: true, 13 | es6: true, 14 | mocha: true, 15 | }, 16 | globals: { 17 | expect: true, 18 | }, 19 | rules: { 20 | 'func-names': ['error', 'never'], 21 | 'space-before-function-paren': ['error', 'never'], 22 | 'no-param-reassign': 0, 23 | 'arrow-parens': 0, 24 | 'dot-notation': 0, 25 | 'operator-linebreak': 0, 26 | 'import/no-unresolved': 0, 27 | 'import/extensions': 0, 28 | "@typescript-eslint/explicit-module-boundary-types": "off", 29 | '@typescript-eslint/type-annotation-spacing': ['error', { before: false, after: false }], 30 | '@typescript-eslint/naming-convention': [ 31 | 'error', 32 | { 33 | selector: 'interface', 34 | format: ['PascalCase'], 35 | custom: { 36 | regex: '^I[A-Z]', 37 | match: true, 38 | }, 39 | }, 40 | ], 41 | '@typescript-eslint/member-delimiter-style': ['error', { 42 | multiline: { delimiter: 'comma' }, 43 | }], 44 | '@typescript-eslint/no-explicit-any': 0, 45 | '@typescript-eslint/explicit-function-return-type': 0, 46 | }, 47 | }; 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | node_modules/ 3 | .vscode 4 | TODO 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | ** 2 | !dist/** 3 | !package.json 4 | !README.md 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [3.4.0] - 2023-04-15 9 | 10 | ### Fixed 11 | 12 | * Issue with sorting multiple types in array (https://github.com/snovakovic/fast-sort/issues/62) 13 | 14 | ## [3.3.3] - 2023-04-15 15 | 16 | Fix usage in TS environment with `"moduleResolution": "node16"` 17 | 18 | ## [3.3.0] - 2023-04-14 19 | 20 | Added proper support for ESM modules 21 | 22 | ## [3.2.1] - 2023-01-06 23 | 24 | ### Added 25 | 26 | * `defaultComparer` is now exported so it can be used for overriding of custom sort instances 27 | 28 | ## [3.1.2] - 2021-11-10 29 | 30 | ### Fixed 31 | 32 | * Issue with sorting dates (https://github.com/snovakovic/fast-sort/issues/51) 33 | 34 | ## [3.1.0] - 2021-11-10 35 | 36 | ### Fixed 37 | 38 | * TypeScript interface to allow sorting readonly arrays if inPlaceSorting is not used 39 | 40 | ## [3.0.0] - 2021-04-08 41 | 42 | ### Changed 43 | 44 | * Default export has been replaced with named exports 45 | 46 | ```javascript 47 | import sort from 'fast-sort'; // older versions 48 | 49 | import { sort } from 'fast-sort'; // v3 and up 50 | ``` 51 | 52 | * By default `sort` no longer mutates array as was case in previous versions it now creates new array instance. 53 | 54 | * `sort.createNewInstance` is now provided as named export 55 | 56 | ```javascript 57 | import { createNewSortInstance } from 'fast-sort'; 58 | ``` 59 | 60 | ### Added 61 | 62 | * `inPlaceSort` mutates provided array instead of creating new array instance. This was default behaviour of previous sort versions 63 | * `inPlaceSorting` option that can be passed to `createNewSortInstance`. 64 | 65 | ## [2.2.0] - 2019-12-14 66 | 67 | ## [3.0.0] - 2021-04-08 68 | ### Changed 69 | 70 | * Old `IComparer` interface has been renamed to `ISortInstanceOptions`. New `IComparer` interface is created that now describes actual comparer function 71 | 72 | ## [2.0.0] - 2019-12-14 73 | 74 | ### Added 75 | 76 | * Option to create new custom sort instance 77 | ```javascript 78 | const naturalSort = sort.createNewInstance({ 79 | comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare, 80 | }); 81 | ``` 82 | * TypeScript support 83 | * more info on this release on https://github.com/snovakovic/fast-sort/releases/tag/v2.0.0 84 | 85 | ## [1.6.0] 86 | 87 | ### Added 88 | 89 | * Option to override default comparer in by sorter 90 | ```javascript 91 | sort(testArr).by({ 92 | desc: true, 93 | comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare, 94 | }); 95 | ``` 96 | 97 | ## [1.5.0] 98 | 99 | ### Added 100 | 101 | * Option to sort in multiple directions 102 | ```javascript 103 | sort(users).by([{ asc: 'age' }, { desc: 'firstName' }]); 104 | ``` 105 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at stefan.novakovich@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Stefan Novaković 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fast-sort 2 | 3 | [![Start](https://img.shields.io/github/stars/snovakovic/fast-sort?style=flat-square)](https://github.com/snovakovic/fast-sort/stargazers) 4 | [![Total Downloads](https://img.shields.io/npm/dt/fast-sort.svg)](https://www.npmjs.com/package/fast-sort) 5 | [![Known Vulnerabilities](https://snyk.io/test/github/snovakovic/fast-sort/badge.svg)](https://snyk.io/test/github/snovakovic/fast-sort) 6 | [![Open Source Love](https://badges.frapsoft.com/os/v1/open-source.svg?v=103)](https://opensource.org/) 7 | [![MIT Licence](https://badges.frapsoft.com/os/mit/mit.svg?v=103)](https://opensource.org/licenses/mit-license.php) 8 | 9 | [![NPM Package](https://nodei.co/npm/fast-sort.png)](https://www.npmjs.com/package/fast-sort) 10 | 11 | Fast-sort is a lightweight (850 bytes gzip), zero-dependency sorting library with TypeScript support. 12 | Its easy-to-use and flexible syntax, combined with [incredible speed](#benchmark) , make it a top choice for developers seeking efficient, reliable, and customizable sorting solutions. 13 | 14 | ## Quick examples 15 | 16 | ```javascript 17 | import { sort } from 'fast-sort'; 18 | 19 | // Sort flat arrays 20 | const ascSorted = sort([1,4,2]).asc(); // => [1, 2, 4] 21 | const descSorted = sort([1, 4, 2]).desc(); // => [4, 2, 1] 22 | 23 | // Sort users (array of objects) by firstName in descending order 24 | const sorted = sort(users).desc(u => u.firstName); 25 | 26 | // Sort users in ascending order by firstName and lastName 27 | const sorted = sort(users).asc([ 28 | u => u.firstName, 29 | u => u.lastName 30 | ]); 31 | 32 | // Sort users ascending by firstName and descending by city 33 | const sorted = sort(users).by([ 34 | { asc: u => u.firstName }, 35 | { desc: u => u.address.city } 36 | ]); 37 | 38 | // Sort based on computed property 39 | const sorted = sort(repositories).desc(r => r.openIssues + r.closedIssues); 40 | 41 | // Sort using string for object key 42 | // Only available for root object properties 43 | const sorted = sort(users).asc('firstName'); 44 | ``` 45 | 46 | Fore more examples check [unit tests](https://github.com/snovakovic/fast-sort/blob/master/test/sort.spec.ts). 47 | 48 | ## In place sorting 49 | 50 | Fast-sort provides an inPlace sorting option that mutates the original array instead of creating a new instance, resulting in marginally faster and more memory-efficient sorting. However, both the inPlaceSort and default sort methods offer exactly the same functionality. 51 | 52 | ```javascript 53 | const { sort, inPlaceSort } = require('fast-sort'); 54 | 55 | const array = [3, 1, 5]; 56 | const sorted = sort(array).asc(); 57 | 58 | // sorted => [1, 3, 5] 59 | // array => [3, 1, 5] 60 | 61 | inPlaceSort(array).asc(); 62 | 63 | // array => [1, 3, 5] 64 | ``` 65 | 66 | ## Natural sorting / Language sensitive sorting 67 | 68 | By default `fast-sort` is not doing language sensitive sorting of strings. 69 | e.g `'image-11.jpg'` will be sorted before `'image-2.jpg'` (in ascending sorting). 70 | We can provide custom [Intl.Collator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Collator) comparer to fast-sort for language sensitive sorting of strings. 71 | Keep in mind that natural sort is slower then default sorting so recommendation is to use it 72 | only when needed. 73 | 74 | ```javascript 75 | import { sort, createNewSortInstance } from 'fast-sort'; 76 | 77 | const testArr = ['image-2.jpg', 'image-11.jpg', 'image-3.jpg']; 78 | 79 | // By default fast-sort is not doing natural sort 80 | sort(testArr).desc(); // => ['image-3.jpg', 'image-2.jpg', 'image-11.jpg'] 81 | 82 | // We can use `by` sort to override default comparer 83 | // with the one that is doing language sensitive comparison 84 | sort(testArr).by({ 85 | desc: true, 86 | comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare, 87 | }); // => ['image-11.jpg', 'image-3.jpg', 'image-2.jpg'] 88 | 89 | // Or we can create new sort instance with language sensitive comparer. 90 | // Recommended if used in multiple places 91 | const naturalSort = createNewSortInstance({ 92 | comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare, 93 | }); 94 | 95 | naturalSort(testArr).asc(); // => ['image-2.jpg', 'image-3.jpg', 'image-11.jpg'] 96 | naturalSort(testArr).desc(); // => ['image-11.jpg', 'image-3.jpg', 'image-2.jpg'] 97 | ``` 98 | 99 | NOTE: It's known that `Intl.Collator` might not sort `null` values correctly so make sure to cast them to `undefine` 100 | as described in the following issue 101 | https://github.com/snovakovic/fast-sort/issues/54#issuecomment-1072289388 102 | 103 | ## Custom sorting 104 | 105 | Fast sort can be tailored to fit any sorting need or use case by: 106 | * creating custom sorting instances 107 | * overriding default comparer in `by` sorter 108 | * custom handling in provided callback function 109 | * combination of any from above 110 | 111 | For example we will sort `tags` by "custom" tag importance (e.g `vip` tag is of greater importance then `captain` tag). 112 | 113 | ```javascript 114 | import { sort, createNewSortInstance } from 'fast-sort'; 115 | 116 | const tags = ['influencer', 'unknown', 'vip', 'captain']; 117 | const tagsImportance = { // Domain specific tag importance 118 | vip: 3, 119 | influencer: 2, 120 | captain: 1, 121 | }; 122 | 123 | // We can use power of computed prop to sort tags by domain specific importance 124 | const descTags = sort(tags).desc(tag => tagImportance[tag] || 0); 125 | // => ['vip', 'influencer', 'captain', 'unknown']; 126 | 127 | // Or we can create specialized tagSorter so we can reuse it in multiple places 128 | const tagSorter = createNewSortInstance({ 129 | comparer: (a, b) => (tagImportance[a] || 0) - (tagImportance[b] || 0), 130 | inPlaceSorting: true, // default[false] => Check "In Place Sort" section for more info. 131 | }); 132 | 133 | tagSorter(tags).asc(); // => ['unknown', 'captain', 'influencer', 'vip']; 134 | tagSorter(tags).desc(); // => ['vip', 'influencer', 'captain', 'unknown']; 135 | 136 | // Default sorter will sort tags by comparing string values not by their domain specific value 137 | const defaultSort = sort(tags).asc(); // => ['captain', 'influencer', 'unknown' 'vip'] 138 | ``` 139 | ## More examples 140 | 141 | ```javascript 142 | // Sorting values that are not sortable will return same value back 143 | sort(null).asc(); // => null 144 | sort(33).desc(); // => 33 145 | 146 | // By default fast-sort sorts null and undefined values to the 147 | // bottom no matter if sorting is in asc or decs order. 148 | // If this is not intended behaviour you can check "Should create sort instance that sorts nil value to the top in desc order" test on how to override 149 | const addresses = [{ city: 'Split' }, { city: undefined }, { city: 'Zagreb'}]; 150 | sort(addresses).asc(a => a.city); // => Split, Zagreb, undefined 151 | sort(addresses).desc(a => a.city); // => Zagreb, Split, undefined 152 | ``` 153 | 154 | ## Migrating from older versions 155 | 156 | Documentation for v2 and older versions is available [here](https://github.com/snovakovic/fast-sort/blob/v2/README.md). 157 | 158 | 159 | For migrating to v3 you can reference [CHANGELOG](https://github.com/snovakovic/fast-sort/blob/master/CHANGELOG.md) for what has been changed. 160 | 161 | ## Benchmark 162 | 163 | Five different benchmarks have been created to get better insight of how fast-sort perform under different scenarios. 164 | Each benchmark is run with different array sizes raging from small 100 items to large 100 000 items. 165 | 166 | Every run of benchmark outputs different results but the results are constantly showing better scores compared to similar popular sorting libraries. 167 | 168 | #### Benchmark scores 169 | 170 | Benchmark has been run on: 171 | 172 | * 16 GB Ram 173 | * Intel® Core™ i5-4570 CPU @ 3.20GHz × 4 174 | * Ubuntu 16.04 175 | * Node 8.9.1 176 | 177 | Independent benchmark results from MacBook Air can be found in following PR: 178 | https://github.com/snovakovic/fast-sort/pull/48 179 | 180 | ![benchmark results](https://github.com/snovakovic/fast-sort/raw/master/benchmark.jpg) 181 | 182 | #### Running benchmark 183 | 184 | To run benchmark on your PC follow steps from below 185 | 186 | 1) git clone https://github.com/snovakovic/fast-sort.git 187 | 2) cd fast-sort/benchmark 188 | 3) npm install 189 | 4) npm start 190 | 191 | In case you notice any irregularities in benchmark or you want to add sort library to benchmark score 192 | please open issue [here](https://github.com/snovakovic/fast-sort) 193 | 194 | -------------------------------------------------------------------------------- /benchmark.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snovakovic/fast-sort/c9dc062fbaacc2743734d07bd055d1451d8ddf70/benchmark.jpg -------------------------------------------------------------------------------- /benchmark/getRandomInt.js: -------------------------------------------------------------------------------- 1 | export default function getRandomInt(min, max) { 2 | min = Math.ceil(min); 3 | max = Math.floor(max); 4 | return Math.floor(Math.random() * (max - min)) + min; 5 | }; 6 | -------------------------------------------------------------------------------- /benchmark/implementations/base.js: -------------------------------------------------------------------------------- 1 | import runner from '../runner.js'; 2 | 3 | export function run({ 4 | sortImplementation, 5 | testArr, 6 | numberOfRuns, 7 | librariesToRun, 8 | }) { 9 | // Control array to make sure that all implementation have sorted arrays correctly 10 | const controlArr = sortImplementation.fastSort([...testArr]); 11 | const run = runner.bind(undefined, testArr, controlArr, numberOfRuns); 12 | 13 | const results = {}; 14 | Object 15 | .keys(sortImplementation) 16 | .forEach((key) => { 17 | if (librariesToRun.includes(key)) { 18 | results[key] = run(sortImplementation[key]); 19 | } 20 | }); 21 | 22 | return results; 23 | }; 24 | -------------------------------------------------------------------------------- /benchmark/implementations/deepObject.js: -------------------------------------------------------------------------------- 1 | import fastSort from 'fast-sort'; 2 | import sortArray from 'sort-array'; 3 | import sortOn from 'sort-on'; 4 | import arraySort from 'array-sort'; 5 | import lodash from 'lodash'; 6 | import latestFastSortSort from '../../dist/sort.js'; 7 | 8 | import * as base from './base.js'; 9 | 10 | const sortImplementation = { 11 | fastSort: (arr) => fastSort.sort(arr).asc((p) => p.level1.level2.amount), 12 | latestFastSort: (arr) => latestFastSortSort.sort(arr).asc((p) => p.level1.level2.amount), 13 | lodash: (arr) => lodash.sortBy(arr, [(p) => p.level1.level2.amount]), 14 | sortArray: (arr) => sortArray(arr, { 15 | by: 'amount', 16 | order: 'asc', 17 | computed: { 18 | amount: p => p.level1.level2.amount, 19 | }, 20 | }), 21 | sortOn: (arr) => sortOn(arr, 'level1.level2.amount'), 22 | arraySort: (arr) => arraySort(arr, 'level1.level2.amount'), 23 | native: (arr) => arr.sort((a, b) => { 24 | if (a.level1.level2.amount == null) return 1; 25 | if (b.level1.level2.amount == null) return -1; 26 | 27 | if (a.level1.level2.amount === b.level1.level2.amount) return 0; 28 | if (a.level1.level2.amount < b.level1.level2.amount) return -1; 29 | return 1; 30 | }), 31 | }; 32 | 33 | export function run({ 34 | size, 35 | numberOfRuns, 36 | librariesToRun, 37 | randomizer = Math.random, 38 | }) { 39 | const testArr = []; 40 | for (let i = 0; i < size; i++) { 41 | testArr.push({ 42 | name: 'test', 43 | level1: { 44 | level2: { amount: randomizer() }, 45 | }, 46 | }); 47 | } 48 | 49 | return base.run({ 50 | sortImplementation, 51 | testArr, 52 | numberOfRuns, 53 | librariesToRun, 54 | }); 55 | }; 56 | -------------------------------------------------------------------------------- /benchmark/implementations/flatArray.js: -------------------------------------------------------------------------------- 1 | import fastSort from 'fast-sort'; 2 | import arraySort from 'array-sort'; 3 | import sortArray from 'sort-array'; 4 | import sortOn from 'sort-on'; 5 | import lodash from 'lodash'; 6 | import latestFastSortSort from '../../dist/sort.js'; 7 | 8 | import * as base from './base.js'; 9 | 10 | const sortImplementation = { 11 | fastSort: (arr) => fastSort.sort(arr).asc(), 12 | latestFastSort: (arr) => latestFastSortSort.sort(arr).asc(), 13 | lodash: (arr) => lodash.sortBy(arr), 14 | arraySort: (arr) => arraySort(arr), 15 | sortArray: (arr) => sortArray(arr), 16 | sortOn: (arr) => sortOn(arr, x => x), 17 | native: (arr) => arr.sort((a, b) => { 18 | if (a == null) return 1; 19 | if (b == null) return -1; 20 | 21 | if (a === b) return 0; 22 | if (a < b) return -1; 23 | return 1; 24 | }), 25 | }; 26 | 27 | export function run({ 28 | size, 29 | numberOfRuns, 30 | librariesToRun, 31 | randomizer = Math.random, 32 | }) { 33 | const testArr = []; 34 | for (let i = 0; i < size; i++) { 35 | testArr.push(randomizer()); 36 | } 37 | 38 | return base.run({ 39 | sortImplementation, 40 | testArr, 41 | numberOfRuns, 42 | librariesToRun, 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /benchmark/implementations/flatObject.js: -------------------------------------------------------------------------------- 1 | import fastSort from 'fast-sort'; 2 | import sortArray from 'sort-array'; 3 | import sortOn from 'sort-on'; 4 | import arraySort from 'array-sort'; 5 | import lodash from 'lodash'; 6 | import latestFastSortSort from '../../dist/sort.js'; 7 | 8 | import * as base from './base.js'; 9 | 10 | const sortImplementation = { 11 | fastSort: (arr) => fastSort.sort(arr).asc('amount'), 12 | latestFastSort: (arr) => latestFastSortSort.sort(arr).asc('amount'), 13 | lodash: (arr) => lodash.sortBy(arr, [(p) => p.amount]), 14 | sortArray: (arr) => sortArray(arr, { by: 'amount', order: 'asc' }), 15 | sortOn: (arr) => sortOn(arr, 'amount'), 16 | arraySort: (arr) => arraySort(arr, 'amount'), 17 | native: (arr) => arr.sort((a, b) => { 18 | if (a.amount == null) return 1; 19 | if (b.amount == null) return -1; 20 | 21 | if (a.amount === b.amount) return 0; 22 | if (a.amount < b.amount) return -1; 23 | return 1; 24 | }), 25 | }; 26 | 27 | export function run({ 28 | size, 29 | numberOfRuns, 30 | librariesToRun, 31 | randomizer = Math.random, 32 | }) { 33 | const testArr = []; 34 | for (let i = 0; i < size; i++) { 35 | testArr.push({ 36 | name: 'test', 37 | amount: randomizer(), 38 | }); 39 | } 40 | 41 | return base.run({ 42 | sortImplementation, 43 | testArr, 44 | numberOfRuns, 45 | librariesToRun, 46 | }); 47 | }; 48 | -------------------------------------------------------------------------------- /benchmark/implementations/multiProperty.js: -------------------------------------------------------------------------------- 1 | import fastSort from 'fast-sort'; 2 | import arraySort from 'array-sort'; 3 | import lodash from 'lodash'; 4 | import sortArray from 'sort-array'; 5 | import sortOn from 'sort-on'; 6 | import getRandomInt from '../getRandomInt.js'; 7 | import latestFastSortSort from '../../dist/sort.js'; 8 | 9 | import * as base from './base.js'; 10 | 11 | const sortImplementation = { 12 | fastSort: (arr) => fastSort.sort(arr).asc([ 13 | (p) => p.am1, 14 | (p) => p.am2, 15 | ]), 16 | latestFastSort: (arr) => latestFastSortSort.sort(arr).asc([ 17 | (p) => p.am1, 18 | (p) => p.am2, 19 | ]), 20 | lodash: (arr) => lodash.sortBy(arr, [ 21 | (p) => p.am1, 22 | (p) => p.am2, 23 | ]), 24 | arraySort: (arr) => arraySort(arr, 'am1', 'am2'), 25 | sortArray: (arr) => sortArray(arr, { 26 | by: ['am1', 'am2'], 27 | }), 28 | sortOn: (arr) => sortOn(arr, ['am1', 'am2']), 29 | }; 30 | 31 | export function run({ size, numberOfRuns, librariesToRun }) { 32 | const testArr = []; 33 | for (let i = 0; i < size; i++) { 34 | testArr.push({ 35 | name: 'test', 36 | am1: getRandomInt(1, 20), 37 | am2: Math.random(), 38 | }); 39 | } 40 | 41 | return base.run({ 42 | sortImplementation, 43 | testArr, 44 | numberOfRuns, 45 | librariesToRun, 46 | }); 47 | }; 48 | -------------------------------------------------------------------------------- /benchmark/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | import chalk from 'chalk'; 4 | import Table from 'cli-table2'; 5 | import { stdout as log } from 'single-line-log'; 6 | 7 | import getRandomInt from './getRandomInt.js'; 8 | import * as flatObject from './implementations/flatObject.js'; 9 | import * as deepObject from './implementations/deepObject.js'; 10 | import * as multiProperty from './implementations/multiProperty.js'; 11 | import * as flatArray from './implementations/flatArray.js'; 12 | 13 | const librariesToRun = [ 14 | 'fastSort', 15 | // 'latestFastSort', 16 | 'lodash', 17 | 'arraySort', 18 | 'sortArray', 19 | 'sortOn', 20 | 'native', 21 | ]; 22 | 23 | const runConfiguration = [ 24 | { size: 1000, numberOfRuns: 10, librariesToRun }, 25 | { size: 5000, numberOfRuns: 50, librariesToRun }, 26 | { size: 20000, numberOfRuns: 25, librariesToRun }, 27 | { size: 100000, numberOfRuns: 5, librariesToRun }, 28 | ]; 29 | 30 | const headerItems = [chalk.hex('f49b42')('Library')]; 31 | headerItems.push(...runConfiguration.map((c) => chalk.hex('f49b42')(`${c.size} items`))); 32 | 33 | function getRowValue(name, run) { 34 | if (!run[name]) { 35 | return chalk.red('NOT SUPPORTED'); 36 | } 37 | 38 | const fastSort = run.fastSort.average; 39 | const lib = run[name].average; 40 | let comparison = ''; 41 | if (fastSort !== lib) { 42 | const color = fastSort < lib ? 'red' : 'green'; 43 | const comparedTofastSort = (Math.max(fastSort, lib) / Math.min(fastSort, lib)).toFixed(2); 44 | comparison = chalk[color](`${fastSort < lib ? '↓' : '↑'} ${comparedTofastSort}x `); 45 | comparison = `(${comparison})`; 46 | } 47 | 48 | const result = `${run[name].average.toFixed(4)}ms ${comparison}`; 49 | return name === 'fastSort' 50 | ? chalk.blue(result) 51 | : result; 52 | } 53 | 54 | function addRow(libName, result, table) { 55 | const value = getRowValue.bind(null, libName); 56 | 57 | if (libName === 'fastSort') libName = chalk.blue(libName); 58 | table.push([libName, ...result.map((r) => value(r))]); 59 | } 60 | 61 | const run = function(implementation, randomizer) { 62 | const res = []; 63 | 64 | runConfiguration.forEach((conf, idx) => { 65 | res.push(implementation.run(Object.assign(conf, { randomizer }))); 66 | log(`${idx + 1}/${runConfiguration.length}`); 67 | log.clear(); 68 | }); 69 | 70 | log(''); 71 | 72 | const table = new Table({ head: headerItems }); 73 | librariesToRun.forEach((lib) => addRow(lib, res, table)); 74 | 75 | console.log(table.toString()); 76 | }; 77 | 78 | console.info('\n --------------- SORT BENCHMARK ---------------'); 79 | 80 | console.info('\n Benchmark 1: Flat object high randomization \n'); 81 | run(flatObject); 82 | 83 | console.info('\n Benchmark 2: Flat object low randomization \n'); 84 | run(flatObject, () => getRandomInt(1, 5)); 85 | 86 | console.log('\n Benchmark 3: Flat array high randomization \n'); 87 | run(flatArray); 88 | 89 | console.log('\n Benchmark 4: Deep nested properties high randomization \n'); 90 | run(deepObject); 91 | 92 | console.log('\n Benchmark 5: Multi property sort low randomization \n'); 93 | run(multiProperty); 94 | -------------------------------------------------------------------------------- /benchmark/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sort-benchmark", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "ansi-regex": { 8 | "version": "2.1.1", 9 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", 10 | "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" 11 | }, 12 | "ansi-styles": { 13 | "version": "3.2.1", 14 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 15 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 16 | "requires": { 17 | "color-convert": "^1.9.0" 18 | } 19 | }, 20 | "array-back": { 21 | "version": "4.0.1", 22 | "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.1.tgz", 23 | "integrity": "sha512-Z/JnaVEXv+A9xabHzN43FiiiWEE7gPCRXMrVmRm00tWbjZRul1iHm7ECzlyNq1p4a4ATXz+G9FJ3GqGOkOV3fg==" 24 | }, 25 | "array-sort": { 26 | "version": "1.0.0", 27 | "resolved": "https://registry.npmjs.org/array-sort/-/array-sort-1.0.0.tgz", 28 | "integrity": "sha512-ihLeJkonmdiAsD7vpgN3CRcx2J2S0TiYW+IS/5zHBI7mKUq3ySvBdzzBfD236ubDBQFiiyG3SWCPc+msQ9KoYg==", 29 | "requires": { 30 | "default-compare": "^1.0.0", 31 | "get-value": "^2.0.6", 32 | "kind-of": "^5.0.2" 33 | } 34 | }, 35 | "chalk": { 36 | "version": "2.4.2", 37 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 38 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 39 | "requires": { 40 | "ansi-styles": "^3.2.1", 41 | "escape-string-regexp": "^1.0.5", 42 | "supports-color": "^5.3.0" 43 | } 44 | }, 45 | "cli-table2": { 46 | "version": "0.2.0", 47 | "resolved": "https://registry.npmjs.org/cli-table2/-/cli-table2-0.2.0.tgz", 48 | "integrity": "sha1-LR738hig54biFFQFYtS9F3/jLZc=", 49 | "requires": { 50 | "colors": "^1.1.2", 51 | "lodash": "^3.10.1", 52 | "string-width": "^1.0.1" 53 | }, 54 | "dependencies": { 55 | "lodash": { 56 | "version": "3.10.1", 57 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", 58 | "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=" 59 | } 60 | } 61 | }, 62 | "code-point-at": { 63 | "version": "1.1.0", 64 | "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", 65 | "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" 66 | }, 67 | "color-convert": { 68 | "version": "1.9.3", 69 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 70 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 71 | "requires": { 72 | "color-name": "1.1.3" 73 | } 74 | }, 75 | "color-name": { 76 | "version": "1.1.3", 77 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 78 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" 79 | }, 80 | "colors": { 81 | "version": "1.2.5", 82 | "resolved": "https://registry.npmjs.org/colors/-/colors-1.2.5.tgz", 83 | "integrity": "sha512-erNRLao/Y3Fv54qUa0LBB+//Uf3YwMUmdJinN20yMXm9zdKKqH9wt7R9IIVZ+K7ShzfpLV/Zg8+VyrBJYB4lpg==", 84 | "optional": true 85 | }, 86 | "default-compare": { 87 | "version": "1.0.0", 88 | "resolved": "https://registry.npmjs.org/default-compare/-/default-compare-1.0.0.tgz", 89 | "integrity": "sha512-QWfXlM0EkAbqOCbD/6HjdwT19j7WCkMyiRhWilc4H9/5h/RzTF9gv5LYh1+CmDV5d1rki6KAWLtQale0xt20eQ==", 90 | "requires": { 91 | "kind-of": "^5.0.2" 92 | } 93 | }, 94 | "dot-prop": { 95 | "version": "7.2.0", 96 | "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-7.2.0.tgz", 97 | "integrity": "sha512-Ol/IPXUARn9CSbkrdV4VJo7uCy1I3VuSiWCaFSg+8BdUOzF9n3jefIpcgAydvUZbTdEBZs2vEiTiS9m61ssiDA==", 98 | "requires": { 99 | "type-fest": "^2.11.2" 100 | } 101 | }, 102 | "escape-string-regexp": { 103 | "version": "1.0.5", 104 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 105 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" 106 | }, 107 | "fast-sort": { 108 | "version": "3.2.0", 109 | "resolved": "https://registry.npmjs.org/fast-sort/-/fast-sort-3.2.0.tgz", 110 | "integrity": "sha512-EgQtkmWo2Icq6uei57fTrZAKayL9b4OISU1613737AuLcIbAZ57tcOtGaK2A7zO54kk97wOnSw6INDA++rjMAQ==" 111 | }, 112 | "get-value": { 113 | "version": "2.0.6", 114 | "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", 115 | "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" 116 | }, 117 | "has-flag": { 118 | "version": "3.0.0", 119 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 120 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" 121 | }, 122 | "is-fullwidth-code-point": { 123 | "version": "1.0.0", 124 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", 125 | "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", 126 | "requires": { 127 | "number-is-nan": "^1.0.0" 128 | } 129 | }, 130 | "kind-of": { 131 | "version": "5.1.0", 132 | "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", 133 | "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" 134 | }, 135 | "lodash": { 136 | "version": "4.17.21", 137 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", 138 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" 139 | }, 140 | "number-is-nan": { 141 | "version": "1.0.1", 142 | "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", 143 | "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" 144 | }, 145 | "single-line-log": { 146 | "version": "1.1.2", 147 | "resolved": "https://registry.npmjs.org/single-line-log/-/single-line-log-1.1.2.tgz", 148 | "integrity": "sha1-wvg/Jzo+GhbtsJlWYdoO1e8DM2Q=", 149 | "requires": { 150 | "string-width": "^1.0.1" 151 | } 152 | }, 153 | "sort-array": { 154 | "version": "4.0.1", 155 | "resolved": "https://registry.npmjs.org/sort-array/-/sort-array-4.0.1.tgz", 156 | "integrity": "sha512-4fo5xlLYHx3kbJ3UVUtxie788kdjHk5XserNE9iHCGSeaHWcuPhxTGjKmVpABhoWpgXN6EiZN9gC/2Y3wanbWg==", 157 | "requires": { 158 | "array-back": "^4.0.1", 159 | "typical": "^6.0.0" 160 | } 161 | }, 162 | "sort-on": { 163 | "version": "5.2.0", 164 | "resolved": "https://registry.npmjs.org/sort-on/-/sort-on-5.2.0.tgz", 165 | "integrity": "sha512-VMjzSQ6S4iMIt8VYpPJGUhViP1kwap7xzPdn/U00HkzkAwazrGsPVmjrSlrXXHpd6VzaUIxEWGvcUBkUAD0jrA==", 166 | "requires": { 167 | "dot-prop": "^7.2.0" 168 | } 169 | }, 170 | "string-width": { 171 | "version": "1.0.2", 172 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", 173 | "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", 174 | "requires": { 175 | "code-point-at": "^1.0.0", 176 | "is-fullwidth-code-point": "^1.0.0", 177 | "strip-ansi": "^3.0.0" 178 | } 179 | }, 180 | "strip-ansi": { 181 | "version": "3.0.1", 182 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", 183 | "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", 184 | "requires": { 185 | "ansi-regex": "^2.0.0" 186 | } 187 | }, 188 | "supports-color": { 189 | "version": "5.5.0", 190 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 191 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 192 | "requires": { 193 | "has-flag": "^3.0.0" 194 | } 195 | }, 196 | "type-fest": { 197 | "version": "2.19.0", 198 | "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", 199 | "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==" 200 | }, 201 | "typical": { 202 | "version": "6.0.0", 203 | "resolved": "https://registry.npmjs.org/typical/-/typical-6.0.0.tgz", 204 | "integrity": "sha512-bTTHXOq5E2HgNQiWCyE/Qlw3hPZN+mYB0nUfIAKIeH+pYu+j1BLVyARiD2Ezfh62vug4/qDEk89ELbsvqv5L2Q==" 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /benchmark/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sort-benchmark", 3 | "version": "1.0.0", 4 | "description": "benchmark for fast-sort library", 5 | "type": "module", 6 | "exports": "./index.js", 7 | "dependencies": { 8 | "array-sort": "^1.0.0", 9 | "chalk": "^2.4.2", 10 | "cli-table2": "^0.2.0", 11 | "fast-sort": "^3.2.0", 12 | "lodash": "^4.17.21", 13 | "single-line-log": "^1.1.2", 14 | "sort-array": "^4.0.1", 15 | "sort-on": "^5.2.0" 16 | }, 17 | "scripts": { 18 | "start": "node index.js" 19 | }, 20 | "author": "stefan.novakovich@gmail.com", 21 | "license": "MIT" 22 | } 23 | -------------------------------------------------------------------------------- /benchmark/runner.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | 3 | export default function(arr, controlArr, numberOfRuns, sortImplementation) { 4 | const times = []; 5 | const { length } = arr; 6 | assert.equal(arr.length, controlArr.length, 'control array does not match test array'); 7 | 8 | for (let i = 0; i < numberOfRuns; i++) { 9 | const arrToSort = [...arr]; 10 | const start = process.hrtime(); 11 | const sorted = sortImplementation(arrToSort); 12 | const end = process.hrtime(start); 13 | 14 | const seconds = end[0]; 15 | const ms = end[1] / 1000000; 16 | times.push((seconds * 1000) + ms); 17 | 18 | try { 19 | assert.deepEqual(sorted[0], controlArr[0], 'First value does not match'); 20 | assert.deepEqual(sorted[length / 2], controlArr[length / 2], 'Middle value does not match'); 21 | assert.deepEqual(sorted[length - 1], controlArr[length - 1], 'Last value does not match'); 22 | } catch (err) { 23 | // eslint-disable-next-line no-console 24 | console.log('rr', err); 25 | } 26 | } 27 | 28 | return { 29 | max: Math.max(...times), 30 | min: Math.min(...times), 31 | average: times.reduce((sum, val) => sum + val, 0) / times.length, 32 | noOfRuns: times.length, 33 | arraySize: length, 34 | }; 35 | }; 36 | -------------------------------------------------------------------------------- /dist/sort.cjs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, '__esModule', { value: true }); 4 | 5 | // >>> INTERFACES <<< 6 | // >>> HELPERS <<< 7 | var castComparer = function (comparer) { return function (a, b, order) { return comparer(a, b, order) * order; }; }; 8 | var throwInvalidConfigErrorIfTrue = function (condition, context) { 9 | if (condition) 10 | throw Error("Invalid sort config: " + context); 11 | }; 12 | var unpackObjectSorter = function (sortByObj) { 13 | var _a = sortByObj || {}, asc = _a.asc, desc = _a.desc; 14 | var order = asc ? 1 : -1; 15 | var sortBy = (asc || desc); 16 | // Validate object config 17 | throwInvalidConfigErrorIfTrue(!sortBy, 'Expected `asc` or `desc` property'); 18 | throwInvalidConfigErrorIfTrue(asc && desc, 'Ambiguous object with `asc` and `desc` config properties'); 19 | var comparer = sortByObj.comparer && castComparer(sortByObj.comparer); 20 | return { order: order, sortBy: sortBy, comparer: comparer }; 21 | }; 22 | // >>> SORTERS <<< 23 | var multiPropertySorterProvider = function (defaultComparer) { 24 | return function multiPropertySorter(sortBy, sortByArr, depth, order, comparer, a, b) { 25 | var valA; 26 | var valB; 27 | if (typeof sortBy === 'string') { 28 | valA = a[sortBy]; 29 | valB = b[sortBy]; 30 | } 31 | else if (typeof sortBy === 'function') { 32 | valA = sortBy(a); 33 | valB = sortBy(b); 34 | } 35 | else { 36 | var objectSorterConfig = unpackObjectSorter(sortBy); 37 | return multiPropertySorter(objectSorterConfig.sortBy, sortByArr, depth, objectSorterConfig.order, objectSorterConfig.comparer || defaultComparer, a, b); 38 | } 39 | var equality = comparer(valA, valB, order); 40 | if ((equality === 0 || (valA == null && valB == null)) && 41 | sortByArr.length > depth) { 42 | return multiPropertySorter(sortByArr[depth], sortByArr, depth + 1, order, comparer, a, b); 43 | } 44 | return equality; 45 | }; 46 | }; 47 | function getSortStrategy(sortBy, comparer, order) { 48 | // Flat array sorter 49 | if (sortBy === undefined || sortBy === true) { 50 | return function (a, b) { return comparer(a, b, order); }; 51 | } 52 | // Sort list of objects by single object key 53 | if (typeof sortBy === 'string') { 54 | throwInvalidConfigErrorIfTrue(sortBy.includes('.'), 'String syntax not allowed for nested properties.'); 55 | return function (a, b) { return comparer(a[sortBy], b[sortBy], order); }; 56 | } 57 | // Sort list of objects by single function sorter 58 | if (typeof sortBy === 'function') { 59 | return function (a, b) { return comparer(sortBy(a), sortBy(b), order); }; 60 | } 61 | // Sort by multiple properties 62 | if (Array.isArray(sortBy)) { 63 | var multiPropSorter_1 = multiPropertySorterProvider(comparer); 64 | return function (a, b) { return multiPropSorter_1(sortBy[0], sortBy, 1, order, comparer, a, b); }; 65 | } 66 | // Unpack object config to get actual sorter strategy 67 | var objectSorterConfig = unpackObjectSorter(sortBy); 68 | return getSortStrategy(objectSorterConfig.sortBy, objectSorterConfig.comparer || comparer, objectSorterConfig.order); 69 | } 70 | var sortArray = function (order, ctx, sortBy, comparer) { 71 | var _a; 72 | if (!Array.isArray(ctx)) { 73 | return ctx; 74 | } 75 | // Unwrap sortBy if array with only 1 value to get faster sort strategy 76 | if (Array.isArray(sortBy) && sortBy.length < 2) { 77 | _a = sortBy, sortBy = _a[0]; 78 | } 79 | return ctx.sort(getSortStrategy(sortBy, comparer, order)); 80 | }; 81 | function createNewSortInstance(opts) { 82 | var comparer = castComparer(opts.comparer); 83 | return function (arrayToSort) { 84 | var ctx = Array.isArray(arrayToSort) && !opts.inPlaceSorting 85 | ? arrayToSort.slice() 86 | : arrayToSort; 87 | return { 88 | asc: function (sortBy) { 89 | return sortArray(1, ctx, sortBy, comparer); 90 | }, 91 | desc: function (sortBy) { 92 | return sortArray(-1, ctx, sortBy, comparer); 93 | }, 94 | by: function (sortBy) { 95 | return sortArray(1, ctx, sortBy, comparer); 96 | }, 97 | }; 98 | }; 99 | } 100 | var defaultComparer = function (a, b, order) { 101 | if (a == null) 102 | return order; 103 | if (b == null) 104 | return -order; 105 | if (typeof a !== typeof b) { 106 | return typeof a < typeof b ? -1 : 1; 107 | } 108 | if (a < b) 109 | return -1; 110 | if (a > b) 111 | return 1; 112 | return 0; 113 | }; 114 | var sort = createNewSortInstance({ 115 | comparer: defaultComparer, 116 | }); 117 | var inPlaceSort = createNewSortInstance({ 118 | comparer: defaultComparer, 119 | inPlaceSorting: true, 120 | }); 121 | 122 | exports.createNewSortInstance = createNewSortInstance; 123 | exports.defaultComparer = defaultComparer; 124 | exports.inPlaceSort = inPlaceSort; 125 | exports.sort = sort; 126 | -------------------------------------------------------------------------------- /dist/sort.d.ts: -------------------------------------------------------------------------------- 1 | declare type IOrder = 1 | -1; 2 | export interface IComparer { 3 | (a: any, b: any, order: IOrder): number; 4 | } 5 | export interface ISortInstanceOptions { 6 | comparer?: IComparer; 7 | inPlaceSorting?: boolean; 8 | } 9 | export interface ISortByFunction { 10 | (prop: T): any; 11 | } 12 | export declare type ISortBy = keyof T | ISortByFunction | (keyof T | ISortByFunction)[]; 13 | export interface ISortByAscSorter extends ISortInstanceOptions { 14 | asc: boolean | ISortBy; 15 | } 16 | export interface ISortByDescSorter extends ISortInstanceOptions { 17 | desc: boolean | ISortBy; 18 | } 19 | export declare type ISortByObjectSorter = ISortByAscSorter | ISortByDescSorter; 20 | export interface IFastSort { 21 | /** 22 | * Sort array in ascending order. 23 | * @example 24 | * sort([3, 1, 4]).asc(); 25 | * sort(users).asc(u => u.firstName); 26 | * sort(users).asc([ 27 | * u => u.firstName, 28 | * u => u.lastName, 29 | * ]); 30 | */ 31 | asc(sortBy?: ISortBy | ISortBy[]): T[]; 32 | /** 33 | * Sort array in descending order. 34 | * @example 35 | * sort([3, 1, 4]).desc(); 36 | * sort(users).desc(u => u.firstName); 37 | * sort(users).desc([ 38 | * u => u.firstName, 39 | * u => u.lastName, 40 | * ]); 41 | */ 42 | desc(sortBy?: ISortBy | ISortBy[]): T[]; 43 | /** 44 | * Sort array in ascending or descending order. It allows sorting on multiple props 45 | * in different order for each of them. 46 | * @example 47 | * sort(users).by([ 48 | * { asc: u => u.score }, 49 | * { desc: u => u.age }, 50 | * ]); 51 | */ 52 | by(sortBy: ISortByObjectSorter | ISortByObjectSorter[]): T[]; 53 | } 54 | export declare function createNewSortInstance(opts: ISortInstanceOptions & { 55 | inPlaceSorting?: false; 56 | }): (arrayToSort: readonly T[]) => IFastSort; 57 | export declare function createNewSortInstance(opts: ISortInstanceOptions): (arrayToSort: T[]) => IFastSort; 58 | export declare const defaultComparer: (a: any, b: any, order: IOrder) => number; 59 | export declare const sort: (arrayToSort: readonly T[]) => IFastSort; 60 | export declare const inPlaceSort: (arrayToSort: T[]) => IFastSort; 61 | export {}; 62 | -------------------------------------------------------------------------------- /dist/sort.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : 3 | typeof define === 'function' && define.amd ? define(['exports'], factory) : 4 | (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.fastSort = {})); 5 | }(this, (function (exports) { 'use strict'; 6 | 7 | // >>> INTERFACES <<< 8 | // >>> HELPERS <<< 9 | var castComparer = function (comparer) { return function (a, b, order) { return comparer(a, b, order) * order; }; }; 10 | var throwInvalidConfigErrorIfTrue = function (condition, context) { 11 | if (condition) 12 | throw Error("Invalid sort config: " + context); 13 | }; 14 | var unpackObjectSorter = function (sortByObj) { 15 | var _a = sortByObj || {}, asc = _a.asc, desc = _a.desc; 16 | var order = asc ? 1 : -1; 17 | var sortBy = (asc || desc); 18 | // Validate object config 19 | throwInvalidConfigErrorIfTrue(!sortBy, 'Expected `asc` or `desc` property'); 20 | throwInvalidConfigErrorIfTrue(asc && desc, 'Ambiguous object with `asc` and `desc` config properties'); 21 | var comparer = sortByObj.comparer && castComparer(sortByObj.comparer); 22 | return { order: order, sortBy: sortBy, comparer: comparer }; 23 | }; 24 | // >>> SORTERS <<< 25 | var multiPropertySorterProvider = function (defaultComparer) { 26 | return function multiPropertySorter(sortBy, sortByArr, depth, order, comparer, a, b) { 27 | var valA; 28 | var valB; 29 | if (typeof sortBy === 'string') { 30 | valA = a[sortBy]; 31 | valB = b[sortBy]; 32 | } 33 | else if (typeof sortBy === 'function') { 34 | valA = sortBy(a); 35 | valB = sortBy(b); 36 | } 37 | else { 38 | var objectSorterConfig = unpackObjectSorter(sortBy); 39 | return multiPropertySorter(objectSorterConfig.sortBy, sortByArr, depth, objectSorterConfig.order, objectSorterConfig.comparer || defaultComparer, a, b); 40 | } 41 | var equality = comparer(valA, valB, order); 42 | if ((equality === 0 || (valA == null && valB == null)) && 43 | sortByArr.length > depth) { 44 | return multiPropertySorter(sortByArr[depth], sortByArr, depth + 1, order, comparer, a, b); 45 | } 46 | return equality; 47 | }; 48 | }; 49 | function getSortStrategy(sortBy, comparer, order) { 50 | // Flat array sorter 51 | if (sortBy === undefined || sortBy === true) { 52 | return function (a, b) { return comparer(a, b, order); }; 53 | } 54 | // Sort list of objects by single object key 55 | if (typeof sortBy === 'string') { 56 | throwInvalidConfigErrorIfTrue(sortBy.includes('.'), 'String syntax not allowed for nested properties.'); 57 | return function (a, b) { return comparer(a[sortBy], b[sortBy], order); }; 58 | } 59 | // Sort list of objects by single function sorter 60 | if (typeof sortBy === 'function') { 61 | return function (a, b) { return comparer(sortBy(a), sortBy(b), order); }; 62 | } 63 | // Sort by multiple properties 64 | if (Array.isArray(sortBy)) { 65 | var multiPropSorter_1 = multiPropertySorterProvider(comparer); 66 | return function (a, b) { return multiPropSorter_1(sortBy[0], sortBy, 1, order, comparer, a, b); }; 67 | } 68 | // Unpack object config to get actual sorter strategy 69 | var objectSorterConfig = unpackObjectSorter(sortBy); 70 | return getSortStrategy(objectSorterConfig.sortBy, objectSorterConfig.comparer || comparer, objectSorterConfig.order); 71 | } 72 | var sortArray = function (order, ctx, sortBy, comparer) { 73 | var _a; 74 | if (!Array.isArray(ctx)) { 75 | return ctx; 76 | } 77 | // Unwrap sortBy if array with only 1 value to get faster sort strategy 78 | if (Array.isArray(sortBy) && sortBy.length < 2) { 79 | _a = sortBy, sortBy = _a[0]; 80 | } 81 | return ctx.sort(getSortStrategy(sortBy, comparer, order)); 82 | }; 83 | function createNewSortInstance(opts) { 84 | var comparer = castComparer(opts.comparer); 85 | return function (arrayToSort) { 86 | var ctx = Array.isArray(arrayToSort) && !opts.inPlaceSorting 87 | ? arrayToSort.slice() 88 | : arrayToSort; 89 | return { 90 | asc: function (sortBy) { 91 | return sortArray(1, ctx, sortBy, comparer); 92 | }, 93 | desc: function (sortBy) { 94 | return sortArray(-1, ctx, sortBy, comparer); 95 | }, 96 | by: function (sortBy) { 97 | return sortArray(1, ctx, sortBy, comparer); 98 | }, 99 | }; 100 | }; 101 | } 102 | var defaultComparer = function (a, b, order) { 103 | if (a == null) 104 | return order; 105 | if (b == null) 106 | return -order; 107 | if (typeof a !== typeof b) { 108 | return typeof a < typeof b ? -1 : 1; 109 | } 110 | if (a < b) 111 | return -1; 112 | if (a > b) 113 | return 1; 114 | return 0; 115 | }; 116 | var sort = createNewSortInstance({ 117 | comparer: defaultComparer, 118 | }); 119 | var inPlaceSort = createNewSortInstance({ 120 | comparer: defaultComparer, 121 | inPlaceSorting: true, 122 | }); 123 | 124 | exports.createNewSortInstance = createNewSortInstance; 125 | exports.defaultComparer = defaultComparer; 126 | exports.inPlaceSort = inPlaceSort; 127 | exports.sort = sort; 128 | 129 | Object.defineProperty(exports, '__esModule', { value: true }); 130 | 131 | }))); 132 | -------------------------------------------------------------------------------- /dist/sort.min.d.ts: -------------------------------------------------------------------------------- 1 | declare type IOrder = 1 | -1; 2 | export interface IComparer { 3 | (a: any, b: any, order: IOrder): number; 4 | } 5 | export interface ISortInstanceOptions { 6 | comparer?: IComparer; 7 | inPlaceSorting?: boolean; 8 | } 9 | export interface ISortByFunction { 10 | (prop: T): any; 11 | } 12 | export declare type ISortBy = keyof T | ISortByFunction | (keyof T | ISortByFunction)[]; 13 | export interface ISortByAscSorter extends ISortInstanceOptions { 14 | asc: boolean | ISortBy; 15 | } 16 | export interface ISortByDescSorter extends ISortInstanceOptions { 17 | desc: boolean | ISortBy; 18 | } 19 | export declare type ISortByObjectSorter = ISortByAscSorter | ISortByDescSorter; 20 | export interface IFastSort { 21 | /** 22 | * Sort array in ascending order. 23 | * @example 24 | * sort([3, 1, 4]).asc(); 25 | * sort(users).asc(u => u.firstName); 26 | * sort(users).asc([ 27 | * u => u.firstName, 28 | * u => u.lastName, 29 | * ]); 30 | */ 31 | asc(sortBy?: ISortBy | ISortBy[]): T[]; 32 | /** 33 | * Sort array in descending order. 34 | * @example 35 | * sort([3, 1, 4]).desc(); 36 | * sort(users).desc(u => u.firstName); 37 | * sort(users).desc([ 38 | * u => u.firstName, 39 | * u => u.lastName, 40 | * ]); 41 | */ 42 | desc(sortBy?: ISortBy | ISortBy[]): T[]; 43 | /** 44 | * Sort array in ascending or descending order. It allows sorting on multiple props 45 | * in different order for each of them. 46 | * @example 47 | * sort(users).by([ 48 | * { asc: u => u.score }, 49 | * { desc: u => u.age }, 50 | * ]); 51 | */ 52 | by(sortBy: ISortByObjectSorter | ISortByObjectSorter[]): T[]; 53 | } 54 | export declare function createNewSortInstance(opts: ISortInstanceOptions & { 55 | inPlaceSorting?: false; 56 | }): (arrayToSort: readonly T[]) => IFastSort; 57 | export declare function createNewSortInstance(opts: ISortInstanceOptions): (arrayToSort: T[]) => IFastSort; 58 | export declare const defaultComparer: (a: any, b: any, order: IOrder) => number; 59 | export declare const sort: (arrayToSort: readonly T[]) => IFastSort; 60 | export declare const inPlaceSort: (arrayToSort: T[]) => IFastSort; 61 | export {}; 62 | -------------------------------------------------------------------------------- /dist/sort.min.js: -------------------------------------------------------------------------------- 1 | !function(r,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n((r="undefined"!=typeof globalThis?globalThis:r||self).fastSort={})}(this,function(r){"use strict";function u(t){return function(r,n,e){return t(r,n,e)*e}}var f=function(r,n){if(r)throw Error("Invalid sort config: "+n)},y=function(r){var n=r||{},e=n.asc,t=n.desc,o=e?1:-1,i=e||t;return f(!i,"Expected `asc` or `desc` property"),f(e&&t,"Ambiguous object with `asc` and `desc` config properties"),{order:o,sortBy:i,comparer:r.comparer&&u(r.comparer)}},c=function(l){return function r(n,e,t,o,i,u,f){var c,a;if("string"==typeof n)c=u[n],a=f[n];else{if("function"!=typeof n){var s=y(n);return r(s.sortBy,e,t,s.order,s.comparer||l,u,f)}c=n(u),a=n(f)}var p=i(c,a,o);return(0===p||null==c&&null==a)&&e.length>t?r(e[t],e,t+1,o,i,u,f):p}};function o(r,n,e,t){return Array.isArray(n)?(Array.isArray(e)&&e.length<2&&(e=e[0]),n.sort(function r(e,t,o){if(void 0===e||!0===e)return function(r,n){return t(r,n,o)};if("string"==typeof e)return f(e.includes("."),"String syntax not allowed for nested properties."),function(r,n){return t(r[e],n[e],o)};if("function"==typeof e)return function(r,n){return t(e(r),e(n),o)};if(Array.isArray(e)){var i=c(t);return function(r,n){return i(e[0],e,1,o,t,r,n)}}var n=y(e);return r(n.sortBy,n.comparer||t,n.order)}(e,t,r))):n}function n(e){var t=u(e.comparer);return function(r){var n=Array.isArray(r)&&!e.inPlaceSorting?r.slice():r;return{asc:function(r){return o(1,n,r,t)},desc:function(r){return o(-1,n,r,t)},by:function(r){return o(1,n,r,t)}}}}function e(r,n,e){return null==r?e:null==n?-e:typeof r!=typeof n?typeof r>> INTERFACES <<< 2 | // >>> HELPERS <<< 3 | var castComparer = function (comparer) { return function (a, b, order) { return comparer(a, b, order) * order; }; }; 4 | var throwInvalidConfigErrorIfTrue = function (condition, context) { 5 | if (condition) 6 | throw Error("Invalid sort config: " + context); 7 | }; 8 | var unpackObjectSorter = function (sortByObj) { 9 | var _a = sortByObj || {}, asc = _a.asc, desc = _a.desc; 10 | var order = asc ? 1 : -1; 11 | var sortBy = (asc || desc); 12 | // Validate object config 13 | throwInvalidConfigErrorIfTrue(!sortBy, 'Expected `asc` or `desc` property'); 14 | throwInvalidConfigErrorIfTrue(asc && desc, 'Ambiguous object with `asc` and `desc` config properties'); 15 | var comparer = sortByObj.comparer && castComparer(sortByObj.comparer); 16 | return { order: order, sortBy: sortBy, comparer: comparer }; 17 | }; 18 | // >>> SORTERS <<< 19 | var multiPropertySorterProvider = function (defaultComparer) { 20 | return function multiPropertySorter(sortBy, sortByArr, depth, order, comparer, a, b) { 21 | var valA; 22 | var valB; 23 | if (typeof sortBy === 'string') { 24 | valA = a[sortBy]; 25 | valB = b[sortBy]; 26 | } 27 | else if (typeof sortBy === 'function') { 28 | valA = sortBy(a); 29 | valB = sortBy(b); 30 | } 31 | else { 32 | var objectSorterConfig = unpackObjectSorter(sortBy); 33 | return multiPropertySorter(objectSorterConfig.sortBy, sortByArr, depth, objectSorterConfig.order, objectSorterConfig.comparer || defaultComparer, a, b); 34 | } 35 | var equality = comparer(valA, valB, order); 36 | if ((equality === 0 || (valA == null && valB == null)) && 37 | sortByArr.length > depth) { 38 | return multiPropertySorter(sortByArr[depth], sortByArr, depth + 1, order, comparer, a, b); 39 | } 40 | return equality; 41 | }; 42 | }; 43 | function getSortStrategy(sortBy, comparer, order) { 44 | // Flat array sorter 45 | if (sortBy === undefined || sortBy === true) { 46 | return function (a, b) { return comparer(a, b, order); }; 47 | } 48 | // Sort list of objects by single object key 49 | if (typeof sortBy === 'string') { 50 | throwInvalidConfigErrorIfTrue(sortBy.includes('.'), 'String syntax not allowed for nested properties.'); 51 | return function (a, b) { return comparer(a[sortBy], b[sortBy], order); }; 52 | } 53 | // Sort list of objects by single function sorter 54 | if (typeof sortBy === 'function') { 55 | return function (a, b) { return comparer(sortBy(a), sortBy(b), order); }; 56 | } 57 | // Sort by multiple properties 58 | if (Array.isArray(sortBy)) { 59 | var multiPropSorter_1 = multiPropertySorterProvider(comparer); 60 | return function (a, b) { return multiPropSorter_1(sortBy[0], sortBy, 1, order, comparer, a, b); }; 61 | } 62 | // Unpack object config to get actual sorter strategy 63 | var objectSorterConfig = unpackObjectSorter(sortBy); 64 | return getSortStrategy(objectSorterConfig.sortBy, objectSorterConfig.comparer || comparer, objectSorterConfig.order); 65 | } 66 | var sortArray = function (order, ctx, sortBy, comparer) { 67 | var _a; 68 | if (!Array.isArray(ctx)) { 69 | return ctx; 70 | } 71 | // Unwrap sortBy if array with only 1 value to get faster sort strategy 72 | if (Array.isArray(sortBy) && sortBy.length < 2) { 73 | _a = sortBy, sortBy = _a[0]; 74 | } 75 | return ctx.sort(getSortStrategy(sortBy, comparer, order)); 76 | }; 77 | function createNewSortInstance(opts) { 78 | var comparer = castComparer(opts.comparer); 79 | return function (arrayToSort) { 80 | var ctx = Array.isArray(arrayToSort) && !opts.inPlaceSorting 81 | ? arrayToSort.slice() 82 | : arrayToSort; 83 | return { 84 | asc: function (sortBy) { 85 | return sortArray(1, ctx, sortBy, comparer); 86 | }, 87 | desc: function (sortBy) { 88 | return sortArray(-1, ctx, sortBy, comparer); 89 | }, 90 | by: function (sortBy) { 91 | return sortArray(1, ctx, sortBy, comparer); 92 | }, 93 | }; 94 | }; 95 | } 96 | var defaultComparer = function (a, b, order) { 97 | if (a == null) 98 | return order; 99 | if (b == null) 100 | return -order; 101 | if (typeof a !== typeof b) { 102 | return typeof a < typeof b ? -1 : 1; 103 | } 104 | if (a < b) 105 | return -1; 106 | if (a > b) 107 | return 1; 108 | return 0; 109 | }; 110 | var sort = createNewSortInstance({ 111 | comparer: defaultComparer, 112 | }); 113 | var inPlaceSort = createNewSortInstance({ 114 | comparer: defaultComparer, 115 | inPlaceSorting: true, 116 | }); 117 | 118 | export { createNewSortInstance, defaultComparer, inPlaceSort, sort }; 119 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fast-sort", 3 | "version": "3.4.1", 4 | "description": "Fast easy to use and flexible sorting with TypeScript support", 5 | "author": "Stefan Novakovic ", 6 | "contributors": [ 7 | "Linus Unnebäck: https://github.com/LinusU", 8 | "Luca Ban: https://github.com/mesqueeb", 9 | "Tony Gutierrez: https://github.com/tony-gutierrez" 10 | ], 11 | "license": "MIT", 12 | "homepage": "https://github.com/snovakovic/fast-sort", 13 | "main": "dist/sort.cjs.js", 14 | "module": "dist/sort.mjs", 15 | "types": "dist/sort.d.ts", 16 | "exports": { 17 | ".": { 18 | "require": "./dist/sort.cjs.js", 19 | "import": "./dist/sort.mjs", 20 | "types": "./dist/sort.d.ts" 21 | }, 22 | "./dist/sort.min": "./dist/sort.min.js", 23 | "./dist/sort.js": "./dist/sort.js" 24 | }, 25 | "scripts": { 26 | "test": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' mocha -r ts-node/register test/*.spec.ts", 27 | "test:watch": "watch 'npm run test' ./src", 28 | "test:integration:dist": "node test/integration/dist.test.js", 29 | "test:integration:npm": "node test/integration/npm.test.js", 30 | "build": "npm run test && rm -rf dist && rollup -c", 31 | "prepublishOnly": "npm run build && npm run test:integration:dist", 32 | "postpublish": "npm run test:integration:npm" 33 | }, 34 | "repository": { 35 | "type": "git", 36 | "url": "git+https://github.com/snovakovic/fast-sort.git" 37 | }, 38 | "bugs": { 39 | "url": "https://github.com/snovakovic/fast-sort/issues" 40 | }, 41 | "keywords": [ 42 | "sort", 43 | "sortBy", 44 | "order", 45 | "orderBy", 46 | "array sort", 47 | "object sort", 48 | "natural sort" 49 | ], 50 | "dependencies": {}, 51 | "devDependencies": { 52 | "@types/chai": "^4.2.15", 53 | "@types/mocha": "^8.2.1", 54 | "@typescript-eslint/eslint-plugin": "^4.17.0", 55 | "@typescript-eslint/parser": "^4.17.0", 56 | "chai": "^4.3.3", 57 | "eslint": "^7.21.0", 58 | "eslint-config-airbnb-base": "^14.2.1", 59 | "eslint-plugin-import": "^2.22.1", 60 | "mocha": "^10.2.0", 61 | "rollup": "^2.41.1", 62 | "rollup-plugin-copy": "^3.4.0", 63 | "rollup-plugin-typescript2": "^0.30.0", 64 | "rollup-plugin-uglify": "^6.0.4", 65 | "ts-node": "^9.1.1", 66 | "tslib": "^2.1.0", 67 | "typescript": "^4.2.3", 68 | "watch": "^1.0.2" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2'; 2 | import { uglify } from 'rollup-plugin-uglify'; 3 | import copy from 'rollup-plugin-copy'; 4 | import pkg from './package.json'; 5 | 6 | export default { 7 | input: 'src/sort.ts', 8 | output: [ 9 | { 10 | file: 'dist/sort.js', 11 | format: 'umd', 12 | name: 'fastSort', 13 | }, 14 | { 15 | file: 'dist/sort.min.js', 16 | format: 'umd', 17 | name: 'fastSort', 18 | plugins: [uglify({})], 19 | }, 20 | { 21 | file: pkg.module, // "dist/sort.esm.js" 22 | format: 'esm', 23 | }, 24 | { 25 | file: pkg.main, // "dist/sort.cjs.js" 26 | format: 'cjs', 27 | }, 28 | 29 | ], 30 | plugins: [ 31 | typescript({ 32 | // eslint-disable-next-line global-require 33 | typescript: require('typescript'), 34 | }), 35 | copy({ 36 | hook: 'writeBundle', 37 | targets: [{ 38 | src: 'dist/sort.d.ts', 39 | dest: 'dist', 40 | rename: 'sort.min.d.ts', 41 | }], 42 | }), 43 | ], 44 | }; 45 | -------------------------------------------------------------------------------- /src/sort.ts: -------------------------------------------------------------------------------- 1 | // >>> INTERFACES <<< 2 | 3 | type IOrder = 1 | -1; 4 | 5 | export interface IComparer { 6 | (a:any, b:any, order:IOrder):number, 7 | } 8 | 9 | export interface ISortInstanceOptions { 10 | comparer?:IComparer, 11 | inPlaceSorting?:boolean, 12 | } 13 | 14 | export interface ISortByFunction { 15 | (prop:T):any, 16 | } 17 | 18 | export type ISortBy = keyof T | ISortByFunction | (keyof T | ISortByFunction)[]; 19 | 20 | export interface ISortByAscSorter extends ISortInstanceOptions { 21 | asc:boolean | ISortBy, 22 | } 23 | 24 | export interface ISortByDescSorter extends ISortInstanceOptions { 25 | desc:boolean | ISortBy, 26 | } 27 | 28 | export type ISortByObjectSorter = ISortByAscSorter | ISortByDescSorter; 29 | 30 | type IAnySortBy = ISortBy | ISortBy[] 31 | | ISortByObjectSorter | ISortByObjectSorter[] 32 | | boolean; 33 | 34 | // >>> HELPERS <<< 35 | 36 | const castComparer = (comparer:IComparer) => (a, b, order:IOrder) => comparer(a, b, order) * order; 37 | 38 | const throwInvalidConfigErrorIfTrue = function(condition:boolean, context:string) { 39 | if (condition) throw Error(`Invalid sort config: ${context}`); 40 | }; 41 | 42 | const unpackObjectSorter = function(sortByObj:ISortByObjectSorter) { 43 | const { asc, desc } = sortByObj as any || {}; 44 | const order = asc ? 1 : -1 as IOrder; 45 | const sortBy = (asc || desc) as boolean | ISortBy; 46 | 47 | // Validate object config 48 | throwInvalidConfigErrorIfTrue(!sortBy, 'Expected `asc` or `desc` property'); 49 | throwInvalidConfigErrorIfTrue(asc && desc, 'Ambiguous object with `asc` and `desc` config properties'); 50 | 51 | const comparer = sortByObj.comparer && castComparer(sortByObj.comparer); 52 | 53 | return { order, sortBy, comparer }; 54 | }; 55 | 56 | // >>> SORTERS <<< 57 | 58 | const multiPropertySorterProvider = function(defaultComparer:IComparer) { 59 | return function multiPropertySorter( 60 | sortBy:IAnySortBy, 61 | sortByArr:ISortBy[] | ISortByObjectSorter[], 62 | depth:number, 63 | order:IOrder, 64 | comparer:IComparer, 65 | a, 66 | b, 67 | ):number { 68 | let valA; 69 | let valB; 70 | 71 | if (typeof sortBy === 'string') { 72 | valA = a[sortBy]; 73 | valB = b[sortBy]; 74 | } else if (typeof sortBy === 'function') { 75 | valA = sortBy(a); 76 | valB = sortBy(b); 77 | } else { 78 | const objectSorterConfig = unpackObjectSorter(sortBy as ISortByObjectSorter); 79 | return multiPropertySorter( 80 | objectSorterConfig.sortBy, 81 | sortByArr, 82 | depth, 83 | objectSorterConfig.order, 84 | objectSorterConfig.comparer || defaultComparer, 85 | a, 86 | b, 87 | ); 88 | } 89 | 90 | const equality = comparer(valA, valB, order); 91 | 92 | if ( 93 | (equality === 0 || (valA == null && valB == null)) && 94 | sortByArr.length > depth 95 | ) { 96 | return multiPropertySorter(sortByArr[depth], sortByArr, depth + 1, order, comparer, a, b); 97 | } 98 | 99 | return equality; 100 | }; 101 | }; 102 | 103 | function getSortStrategy( 104 | sortBy:IAnySortBy, 105 | comparer:IComparer, 106 | order:IOrder, 107 | ):(a, b)=>number { 108 | // Flat array sorter 109 | if (sortBy === undefined || sortBy === true) { 110 | return (a, b) => comparer(a, b, order); 111 | } 112 | 113 | // Sort list of objects by single object key 114 | if (typeof sortBy === 'string') { 115 | throwInvalidConfigErrorIfTrue(sortBy.includes('.'), 'String syntax not allowed for nested properties.'); 116 | return (a, b) => comparer(a[sortBy], b[sortBy], order); 117 | } 118 | 119 | // Sort list of objects by single function sorter 120 | if (typeof sortBy === 'function') { 121 | return (a, b) => comparer(sortBy(a), sortBy(b), order); 122 | } 123 | 124 | // Sort by multiple properties 125 | if (Array.isArray(sortBy)) { 126 | const multiPropSorter = multiPropertySorterProvider(comparer); 127 | return (a, b) => multiPropSorter(sortBy[0], sortBy, 1, order, comparer, a, b); 128 | } 129 | 130 | // Unpack object config to get actual sorter strategy 131 | const objectSorterConfig = unpackObjectSorter(sortBy as ISortByObjectSorter); 132 | return getSortStrategy( 133 | objectSorterConfig.sortBy, 134 | objectSorterConfig.comparer || comparer, 135 | objectSorterConfig.order, 136 | ); 137 | } 138 | 139 | const sortArray = function(order:IOrder, ctx:any[], sortBy:IAnySortBy, comparer:IComparer) { 140 | if (!Array.isArray(ctx)) { 141 | return ctx; 142 | } 143 | 144 | // Unwrap sortBy if array with only 1 value to get faster sort strategy 145 | if (Array.isArray(sortBy) && sortBy.length < 2) { 146 | [sortBy] = sortBy; 147 | } 148 | 149 | return ctx.sort(getSortStrategy(sortBy, comparer, order)); 150 | }; 151 | 152 | // >>> Public <<< 153 | 154 | export interface IFastSort { 155 | /** 156 | * Sort array in ascending order. 157 | * @example 158 | * sort([3, 1, 4]).asc(); 159 | * sort(users).asc(u => u.firstName); 160 | * sort(users).asc([ 161 | * u => u.firstName, 162 | * u => u.lastName, 163 | * ]); 164 | */ 165 | asc(sortBy?:ISortBy | ISortBy[]):T[], 166 | /** 167 | * Sort array in descending order. 168 | * @example 169 | * sort([3, 1, 4]).desc(); 170 | * sort(users).desc(u => u.firstName); 171 | * sort(users).desc([ 172 | * u => u.firstName, 173 | * u => u.lastName, 174 | * ]); 175 | */ 176 | desc(sortBy?:ISortBy | ISortBy[]):T[], 177 | /** 178 | * Sort array in ascending or descending order. It allows sorting on multiple props 179 | * in different order for each of them. 180 | * @example 181 | * sort(users).by([ 182 | * { asc: u => u.score }, 183 | * { desc: u => u.age }, 184 | * ]); 185 | */ 186 | by(sortBy:ISortByObjectSorter | ISortByObjectSorter[]):T[], 187 | } 188 | 189 | export function createNewSortInstance( 190 | opts:ISortInstanceOptions & { inPlaceSorting?:false } 191 | ):(arrayToSort:readonly T[])=>IFastSort 192 | export function createNewSortInstance(opts:ISortInstanceOptions):(arrayToSort:T[])=>IFastSort 193 | export function createNewSortInstance(opts:ISortInstanceOptions):(arrayToSort:T[])=>IFastSort { // eslint-disable-line max-len 194 | const comparer = castComparer(opts.comparer); 195 | 196 | return function(arrayToSort:T[]):IFastSort { 197 | const ctx = Array.isArray(arrayToSort) && !opts.inPlaceSorting 198 | ? arrayToSort.slice() 199 | : arrayToSort; 200 | 201 | return { 202 | asc(sortBy?:ISortBy | ISortBy[]):T[] { 203 | return sortArray(1, ctx, sortBy, comparer); 204 | }, 205 | desc(sortBy?:ISortBy | ISortBy[]):T[] { 206 | return sortArray(-1, ctx, sortBy, comparer); 207 | }, 208 | by(sortBy:ISortByObjectSorter | ISortByObjectSorter[]):T[] { 209 | return sortArray(1, ctx, sortBy, comparer); 210 | }, 211 | }; 212 | }; 213 | } 214 | 215 | export const defaultComparer = (a:any, b:any, order:IOrder):number => { 216 | if (a == null) return order; 217 | if (b == null) return -order; 218 | 219 | if (typeof a !== typeof b) { 220 | return typeof a < typeof b ? -1 : 1; 221 | } 222 | 223 | if (a < b) return -1; 224 | if (a > b) return 1; 225 | 226 | return 0; 227 | }; 228 | 229 | export const sort = createNewSortInstance({ 230 | comparer: defaultComparer, 231 | }); 232 | 233 | export const inPlaceSort = createNewSortInstance({ 234 | comparer: defaultComparer, 235 | inPlaceSorting: true, 236 | }); 237 | -------------------------------------------------------------------------------- /test/integration/dist.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | const { sort: sortFull } = require('../../dist/sort'); 4 | const { sort: sortMin } = require('../../dist/sort.min'); 5 | 6 | // Just sanity checks to ensure dist is not broken 7 | // For more detail unit tests check sort.spec.js 8 | function runTests(sort) { 9 | assert.deepStrictEqual(sort([1, 4, 2]).asc(), [1, 2, 4]); 10 | assert.deepStrictEqual(sort([1, 4, 2]).by({ asc: true }), [1, 2, 4]); 11 | assert.deepStrictEqual(sort([1, 4, 2]).desc(), [4, 2, 1]); 12 | assert.deepStrictEqual(sort([1, 4, 2]).by({ desc: true }), [4, 2, 1]); 13 | 14 | console.log('dist integration test success'); 15 | } 16 | 17 | runTests(sortFull); 18 | runTests(sortMin); 19 | -------------------------------------------------------------------------------- /test/integration/npm.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console, global-require, import/no-extraneous-dependencies, import/no-dynamic-require */ 2 | 3 | process.chdir(__dirname); // Enable running from package script 4 | 5 | const assert = require('assert'); 6 | const { exec } = require('child_process'); 7 | 8 | // Just sanity checks to ensure dist is not broken 9 | // For more detail unit tests check sort.spec.js 10 | function runTests({ sort }) { 11 | assert.deepStrictEqual(sort([1, 4, 2]).asc(), [1, 2, 4]); 12 | assert.deepStrictEqual(sort([1, 4, 2]).by({ asc: true }), [1, 2, 4]); 13 | assert.deepStrictEqual(sort([1, 4, 2]).desc(), [4, 2, 1]); 14 | assert.deepStrictEqual(sort([1, 4, 2]).by({ desc: true }), [4, 2, 1]); 15 | } 16 | 17 | function run(err) { 18 | if (err) { 19 | console.error('Problem with installing fast-sort aborting execution', err); 20 | return; 21 | } 22 | 23 | runTests(require('fast-sort')); 24 | runTests(require('fast-sort/dist/sort.min')); 25 | 26 | console.log('npm integration test success'); 27 | } 28 | 29 | exec('npm uninstall fast-sort && npm install --no-save fast-sort', run); 30 | -------------------------------------------------------------------------------- /test/integration/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "integration_test", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1 5 | } 6 | -------------------------------------------------------------------------------- /test/integration/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "integration_test", 3 | "version": "1.0.0", 4 | "description": "Integration test for fast-sort", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": {} 12 | } 13 | -------------------------------------------------------------------------------- /test/sort.spec.ts: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai'; 2 | import { 3 | sort, 4 | inPlaceSort, 5 | createNewSortInstance, 6 | defaultComparer, 7 | } from '../src/sort'; 8 | 9 | describe('sort', () => { 10 | let flatArray:number[]; 11 | let flatNaturalArray:string[]; 12 | let students:{ 13 | name:string, 14 | dob:Date, 15 | address:{ streetNumber?:number }, 16 | }[]; 17 | let multiPropArray:{ 18 | name:string, 19 | lastName:string, 20 | age:number, 21 | unit:string, 22 | }[]; 23 | 24 | beforeEach(() => { 25 | flatArray = [1, 5, 3, 2, 4, 5]; 26 | flatNaturalArray = ['A10', 'A2', 'B10', 'B2']; 27 | 28 | students = [{ 29 | name: 'Mate', 30 | dob: new Date(1987, 14, 11), 31 | address: { streetNumber: 3 }, 32 | }, { 33 | name: 'Ante', 34 | dob: new Date(1987, 14, 9), 35 | address: {}, 36 | }, { 37 | name: 'Dino', 38 | dob: new Date(1987, 14, 10), 39 | address: { streetNumber: 1 }, 40 | }]; 41 | 42 | multiPropArray = [{ 43 | name: 'aa', 44 | lastName: 'aa', 45 | age: 10, 46 | unit: 'A10', 47 | }, { 48 | name: 'aa', 49 | lastName: null as any, 50 | age: 9, 51 | unit: 'A01', 52 | }, { 53 | name: 'aa', 54 | lastName: 'bb', 55 | age: 11, 56 | unit: 'C2', 57 | }, { 58 | name: 'bb', 59 | lastName: 'aa', 60 | age: 6, 61 | unit: 'B3', 62 | }]; 63 | }); 64 | 65 | it('Should sort flat array in ascending order', () => { 66 | const sorted = sort(flatArray).asc(); 67 | assert.deepStrictEqual(sorted, [1, 2, 3, 4, 5, 5]); 68 | 69 | // flatArray should not be modified 70 | assert.deepStrictEqual(flatArray, [1, 5, 3, 2, 4, 5]); 71 | assert.notEqual(sorted, flatArray); 72 | }); 73 | 74 | it('Should in place sort flat array in ascending order', () => { 75 | const sorted = inPlaceSort(flatArray).asc(); 76 | assert.deepStrictEqual(sorted, [1, 2, 3, 4, 5, 5]); 77 | 78 | assert.deepStrictEqual(flatArray, [1, 2, 3, 4, 5, 5]); 79 | assert.equal(sorted, flatArray); 80 | }); 81 | 82 | it('Should sort flat array in descending order', () => { 83 | const sorted = sort(flatArray).desc(); 84 | assert.deepStrictEqual(sorted, [5, 5, 4, 3, 2, 1]); 85 | 86 | // Passed array is not mutated 87 | assert.deepStrictEqual(flatArray, [1, 5, 3, 2, 4, 5]); 88 | 89 | // Can do in place sorting 90 | const sorted2 = inPlaceSort(flatArray).desc(); 91 | assert.equal(sorted2, flatArray); 92 | assert.deepStrictEqual(flatArray, [5, 5, 4, 3, 2, 1]); 93 | }); 94 | 95 | it('Should sort flat array with by sorter', () => { 96 | const sorted = sort(flatArray).by({ asc: true }); 97 | assert.deepStrictEqual(sorted, [1, 2, 3, 4, 5, 5]); 98 | 99 | const sorted2 = sort(flatArray).by({ desc: true }); 100 | assert.deepStrictEqual(sorted2, [5, 5, 4, 3, 2, 1]); 101 | 102 | // Passed array is not mutated 103 | assert.deepStrictEqual(flatArray, [1, 5, 3, 2, 4, 5]); 104 | 105 | // Can do in place sorting 106 | const sorted3 = inPlaceSort(flatArray).by({ desc: true }); 107 | assert.equal(sorted3, flatArray); 108 | assert.deepStrictEqual(flatArray, [5, 5, 4, 3, 2, 1]); 109 | }); 110 | 111 | it('Should sort by student name in ascending order', () => { 112 | const sorted = sort(students).asc(p => p.name.toLowerCase()); 113 | assert.deepStrictEqual(['Ante', 'Dino', 'Mate'], sorted.map(p => p.name)); 114 | }); 115 | 116 | it('Should sort by student name in descending order', () => { 117 | const sorted = sort(students).desc((p) => p.name.toLowerCase()); 118 | assert.deepStrictEqual(['Mate', 'Dino', 'Ante'], sorted.map(p => p.name)); 119 | }); 120 | 121 | it('Should sort nil values to the bottom', () => { 122 | const sorted1 = sort(students).asc((p) => p.address.streetNumber); 123 | assert.deepStrictEqual([1, 3, undefined], sorted1.map(p => p.address.streetNumber)); 124 | 125 | const sorted2 = sort(students).desc((p) => p.address.streetNumber); 126 | assert.deepStrictEqual([3, 1, undefined], sorted2.map(p => p.address.streetNumber)); 127 | 128 | assert.deepStrictEqual( 129 | sort([1, undefined, 3, null, 2]).asc(), 130 | [1, 2, 3, null, undefined], 131 | ); 132 | 133 | assert.deepStrictEqual( 134 | sort([1, undefined, 3, null, 2]).desc(), 135 | [3, 2, 1, null, undefined], 136 | ); 137 | }); 138 | 139 | it('Should ignore values that are not sortable', () => { 140 | assert.equal(sort('string' as any).asc(), 'string' as any); 141 | assert.equal(sort(undefined as any).desc(), undefined); 142 | assert.equal(sort(null as any).desc(), null); 143 | assert.equal(sort(33 as any).asc(), 33 as any); 144 | assert.deepStrictEqual(sort({ name: 'test' } as any).desc(), { name: 'test' } as any); 145 | assert.equal((sort(33 as any) as any).by({ asc: true }), 33 as any); 146 | }); 147 | 148 | it('Should sort dates correctly', () => { 149 | const sorted = sort(students).asc('dob'); 150 | assert.deepStrictEqual(sorted.map(p => p.dob), [ 151 | new Date(1987, 14, 9), 152 | new Date(1987, 14, 10), 153 | new Date(1987, 14, 11), 154 | ]); 155 | }); 156 | 157 | it('Should sort on single property when passed as array', () => { 158 | const sorted = sort(students).asc(['name']); 159 | assert.deepStrictEqual(['Ante', 'Dino', 'Mate'], sorted.map(p => p.name)); 160 | }); 161 | 162 | it('Should sort on multiple properties', () => { 163 | const sorted = sort(multiPropArray).asc([ 164 | p => p.name, 165 | p => p.lastName, 166 | p => p.age, 167 | ]); 168 | 169 | const sortedArray = sorted.map(arr => ({ 170 | name: arr.name, 171 | lastName: arr.lastName, 172 | age: arr.age, 173 | })); 174 | 175 | assert.deepStrictEqual(sortedArray, [ 176 | { name: 'aa', lastName: 'aa', age: 10 }, 177 | { name: 'aa', lastName: 'bb', age: 11 }, 178 | { name: 'aa', lastName: null, age: 9 }, 179 | { name: 'bb', lastName: 'aa', age: 6 }, 180 | ]); 181 | }); 182 | 183 | it('Should sort on multiple properties by string sorter', () => { 184 | const sorted = sort(multiPropArray).asc(['name', 'age', 'lastName']); 185 | const sortedArray = sorted.map(arr => ({ 186 | name: arr.name, 187 | lastName: arr.lastName, 188 | age: arr.age, 189 | })); 190 | 191 | assert.deepStrictEqual(sortedArray, [ 192 | { name: 'aa', lastName: null, age: 9 }, 193 | { name: 'aa', lastName: 'aa', age: 10 }, 194 | { name: 'aa', lastName: 'bb', age: 11 }, 195 | { name: 'bb', lastName: 'aa', age: 6 }, 196 | ]); 197 | }); 198 | 199 | it('Should sort on multiple mixed properties', () => { 200 | const sorted = sort(multiPropArray).asc(['name', p => p.lastName, 'age']); 201 | const sortedArray = sorted.map(arr => ({ 202 | name: arr.name, 203 | lastName: arr.lastName, 204 | age: arr.age, 205 | })); 206 | 207 | assert.deepStrictEqual(sortedArray, [ 208 | { name: 'aa', lastName: 'aa', age: 10 }, 209 | { name: 'aa', lastName: 'bb', age: 11 }, 210 | { name: 'aa', lastName: null, age: 9 }, 211 | { name: 'bb', lastName: 'aa', age: 6 }, 212 | ]); 213 | }); 214 | 215 | it('Should sort with all equal values', () => { 216 | const same = [ 217 | { name: 'a', age: 1 }, 218 | { name: 'a', age: 1 }, 219 | ]; 220 | 221 | const sorted = sort(same).asc(['name', 'age']); 222 | assert.deepStrictEqual(sorted, [ 223 | { name: 'a', age: 1 }, 224 | { name: 'a', age: 1 }, 225 | ]); 226 | }); 227 | 228 | it('Should sort descending by name and ascending by lastName', () => { 229 | const sorted = sort(multiPropArray).by([ 230 | { desc: 'name' }, 231 | { asc: 'lastName' }, 232 | ]); 233 | const sortedArray = sorted.map(arr => ({ 234 | name: arr.name, 235 | lastName: arr.lastName, 236 | })); 237 | 238 | assert.deepStrictEqual(sortedArray, [ 239 | { name: 'bb', lastName: 'aa' }, 240 | { name: 'aa', lastName: 'aa' }, 241 | { name: 'aa', lastName: 'bb' }, 242 | { name: 'aa', lastName: null }, 243 | ]); 244 | }); 245 | 246 | it('Should sort ascending by name and descending by age', () => { 247 | const sorted = sort(multiPropArray).by([ 248 | { asc: 'name' }, 249 | { desc: 'age' }, 250 | ]); 251 | 252 | const sortedArray = sorted.map(arr => ({ name: arr.name, age: arr.age })); 253 | assert.deepStrictEqual(sortedArray, [ 254 | { name: 'aa', age: 11 }, 255 | { name: 'aa', age: 10 }, 256 | { name: 'aa', age: 9 }, 257 | { name: 'bb', age: 6 }, 258 | ]); 259 | }); 260 | 261 | it('Should sort ascending by lastName, descending by name and ascending by age', () => { 262 | const sorted = sort(multiPropArray).by([ 263 | { asc: p => p.lastName }, 264 | { desc: p => p.name }, 265 | { asc: p => p.age }, 266 | ]); 267 | 268 | const sortedArray = sorted.map(arr => ({ 269 | name: arr.name, 270 | lastName: arr.lastName, 271 | age: arr.age, 272 | })); 273 | 274 | assert.deepStrictEqual(sortedArray, [ 275 | { name: 'bb', lastName: 'aa', age: 6 }, 276 | { name: 'aa', lastName: 'aa', age: 10 }, 277 | { name: 'aa', lastName: 'bb', age: 11 }, 278 | { name: 'aa', lastName: null, age: 9 }, 279 | ]); 280 | }); 281 | 282 | it('Should throw error if asc or desc props not provided with object config', () => { 283 | const errorMessage = 'Invalid sort config: Expected `asc` or `desc` property'; 284 | 285 | assert.throws( 286 | () => sort(multiPropArray).by([{ asci: 'name' }] as any), 287 | Error, 288 | errorMessage, 289 | ); 290 | assert.throws( 291 | () => sort(multiPropArray).by([{ asc: 'lastName' }, { ass: 'name' }] as any), 292 | Error, 293 | errorMessage, 294 | ); 295 | assert.throws(() => sort([1, 2]).asc(null as any), Error, errorMessage); 296 | assert.throws(() => sort([1, 2]).desc([1, 2, 3] as any), Error, errorMessage); 297 | }); 298 | 299 | it('Should throw error if both asc and dsc props provided with object config', () => { 300 | const errorMessage = 'Invalid sort config: Ambiguous object with `asc` and `desc` config properties'; 301 | 302 | assert.throws( 303 | () => sort(multiPropArray).by([{ asc: 'name', desc: 'lastName' }] as any), 304 | Error, 305 | errorMessage, 306 | ); 307 | }); 308 | 309 | it('Should throw error if using nested property with string syntax', () => { 310 | assert.throw( 311 | () => sort(students).desc('address.streetNumber' as any), 312 | Error, 313 | 'Invalid sort config: String syntax not allowed for nested properties.', 314 | ); 315 | }); 316 | 317 | it('Should sort ascending on single property with by sorter', () => { 318 | const sorted = sort(multiPropArray).by([{ asc: p => p.age }]); 319 | assert.deepStrictEqual([6, 9, 10, 11], sorted.map(m => m.age)); 320 | }); 321 | 322 | it('Should sort descending on single property with by sorter', () => { 323 | const sorted = sort(multiPropArray).by([{ desc: 'age' }]); 324 | assert.deepStrictEqual([11, 10, 9, 6], sorted.map(m => m.age)); 325 | }); 326 | 327 | it('Should sort flat array in asc order using natural sort comparer', () => { 328 | const sorted = sort(flatNaturalArray).by([{ 329 | asc: true, 330 | comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare, 331 | }]); 332 | 333 | assert.deepStrictEqual(sorted, ['A2', 'A10', 'B2', 'B10']); 334 | }); 335 | 336 | it('Should sort flat array in desc order using natural sort comparer', () => { 337 | const sorted = sort(flatNaturalArray).by([{ 338 | desc: true, 339 | comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare, 340 | }]); 341 | 342 | assert.deepStrictEqual(sorted, ['B10', 'B2', 'A10', 'A2']); 343 | }); 344 | 345 | it('Should sort object in asc order using natural sort comparer', () => { 346 | const sorted = sort(multiPropArray).by([{ 347 | asc: p => p.unit, 348 | comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare, 349 | }]); 350 | 351 | assert.deepStrictEqual(['A01', 'A10', 'B3', 'C2'], sorted.map(m => m.unit)); 352 | }); 353 | 354 | it('Should sort object in desc order using natural sort comparer', () => { 355 | const sorted = sort(multiPropArray).by([{ 356 | desc: p => p.unit, 357 | comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare, 358 | }]); 359 | 360 | assert.deepStrictEqual(['C2', 'B3', 'A10', 'A01'], sorted.map(m => m.unit)); 361 | }); 362 | 363 | it('Should sort object on multiple props using both default and custom comparer', () => { 364 | const testArr = [ 365 | { a: 'A2', b: 'A2' }, 366 | { a: 'A2', b: 'A10' }, 367 | { a: 'A10', b: 'A2' }, 368 | ]; 369 | 370 | const sorted = sort(testArr).by([{ 371 | desc: p => p.a, 372 | }, { 373 | asc: 'b', 374 | comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare, 375 | }]); 376 | 377 | assert.deepStrictEqual(sorted, [ 378 | { a: 'A2', b: 'A2' }, 379 | { a: 'A2', b: 'A10' }, // <= B is sorted using natural sort comparer 380 | { a: 'A10', b: 'A2' }, // <= A is sorted using default sort comparer 381 | ]); 382 | }); 383 | 384 | // BUG repo case: https://github.com/snovakovic/fast-sort/issues/18 385 | it('Sort by comparer should not override default sort of other array property', () => { 386 | const rows = [ 387 | { status: 0, title: 'A' }, 388 | { status: 0, title: 'D' }, 389 | { status: 0, title: 'B' }, 390 | { status: 1, title: 'C' }, 391 | ]; 392 | 393 | const sorted = sort(rows).by([{ 394 | asc: row => row.status, 395 | comparer: (a, b) => a - b, 396 | }, { 397 | asc: row => row.title, 398 | }]); 399 | 400 | assert.deepStrictEqual(sorted, [ 401 | { status: 0, title: 'A' }, 402 | { status: 0, title: 'B' }, 403 | { status: 0, title: 'D' }, 404 | { status: 1, title: 'C' }, 405 | ]); 406 | }); 407 | 408 | it('Should create natural sort instance and handle sorting correctly', () => { 409 | const naturalSort = createNewSortInstance({ 410 | comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare, 411 | }); 412 | 413 | const sorted1 = naturalSort(multiPropArray).desc('unit'); 414 | assert.deepStrictEqual(['C2', 'B3', 'A10', 'A01'], sorted1.map(m => m.unit)); 415 | 416 | const sorted2 = naturalSort(multiPropArray).by({ asc: 'unit' }); 417 | assert.deepStrictEqual(['A01', 'A10', 'B3', 'C2'], sorted2.map(m => m.unit)); 418 | 419 | const sorted3 = naturalSort(multiPropArray).asc('lastName'); 420 | assert.deepStrictEqual(['aa', 'aa', 'bb', null], sorted3.map(m => m.lastName)); 421 | 422 | const sorted4 = naturalSort(multiPropArray).desc(p => p.lastName); 423 | assert.deepStrictEqual([null, 'bb', 'aa', 'aa'], sorted4.map(m => m.lastName)); 424 | 425 | const sorted5 = naturalSort(flatArray).desc(); 426 | assert.deepStrictEqual(sorted5, [5, 5, 4, 3, 2, 1]); 427 | 428 | const sorted6 = naturalSort(flatNaturalArray).asc(); 429 | assert.deepStrictEqual(sorted6, ['A2', 'A10', 'B2', 'B10']); 430 | }); 431 | 432 | it('Should handle sorting on multiples props with custom sorter instance', () => { 433 | const naturalSort = createNewSortInstance({ 434 | comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare, 435 | }); 436 | 437 | const arr = [ 438 | { a: 'a', b: 'A2' }, 439 | { a: 'a', b: 'A20' }, 440 | { a: 'a', b: null }, 441 | { a: 'a', b: 'A3' }, 442 | { a: 'a', b: undefined }, 443 | ]; 444 | 445 | const sort1 = naturalSort(arr).asc('b'); 446 | assert.deepStrictEqual(sort1.map(a => a.b), ['A2', 'A3', 'A20', null, undefined]); 447 | 448 | const sorted2 = naturalSort(arr).asc(['a', 'b']); 449 | assert.deepStrictEqual(sorted2.map(a => a.b), ['A2', 'A3', 'A20', null, undefined]); 450 | 451 | const sorted3 = naturalSort(arr).desc('b'); 452 | assert.deepStrictEqual(sorted3.map(a => a.b), [undefined, null, 'A20', 'A3', 'A2']); 453 | 454 | const sorted4 = naturalSort(arr).desc(['a', 'b']); 455 | assert.deepStrictEqual(sorted4.map(a => a.b), [undefined, null, 'A20', 'A3', 'A2']); 456 | }); 457 | 458 | it('Should create custom tag sorter instance', () => { 459 | const tagImportance = { vip: 3, influencer: 2, captain: 1 }; 460 | const customTagComparer = (a, b) => (tagImportance[a] || 0) - (tagImportance[b] || 0); 461 | 462 | const tags = ['influencer', 'unknown', 'vip', 'captain']; 463 | 464 | const tagSorter = createNewSortInstance({ comparer: customTagComparer }); 465 | assert.deepStrictEqual(tagSorter(tags).asc(), ['unknown', 'captain', 'influencer', 'vip']); 466 | assert.deepStrictEqual(tagSorter(tags).desc(), ['vip', 'influencer', 'captain', 'unknown']); 467 | assert.deepStrictEqual(sort(tags).asc(tag => tagImportance[tag] || 0), ['unknown', 'captain', 'influencer', 'vip']); 468 | }); 469 | 470 | it('Should be able to override natural sort comparer', () => { 471 | const naturalSort = createNewSortInstance({ 472 | comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare, 473 | }); 474 | 475 | const sorted1 = naturalSort(multiPropArray).by([{ 476 | asc: 'name', 477 | }, { 478 | desc: 'unit', 479 | comparer(a, b) { // NOTE: override natural sort 480 | if (a === b) return 0; 481 | return a < b ? -1 : 1; 482 | }, 483 | }]); 484 | 485 | let sortedArray = sorted1.map(arr => ({ name: arr.name, unit: arr.unit })); 486 | assert.deepStrictEqual(sortedArray, [ 487 | { name: 'aa', unit: 'C2' }, 488 | { name: 'aa', unit: 'A10' }, 489 | { name: 'aa', unit: 'A01' }, 490 | { name: 'bb', unit: 'B3' }, 491 | ]); 492 | 493 | const sorted2 = naturalSort(multiPropArray).by([{ asc: 'name' }, { desc: 'unit' }]); 494 | sortedArray = sorted2.map(arr => ({ name: arr.name, unit: arr.unit })); 495 | assert.deepStrictEqual(sortedArray, [ 496 | { name: 'aa', unit: 'C2' }, 497 | { name: 'aa', unit: 'A10' }, 498 | { name: 'aa', unit: 'A01' }, 499 | { name: 'bb', unit: 'B3' }, 500 | ]); 501 | }); 502 | 503 | it('Should sort in asc order with by sorter if object config not provided', () => { 504 | const sorted = sort(multiPropArray).by(['name', 'unit'] as any); 505 | const sortedArray = sorted.map(arr => ({ name: arr.name, unit: arr.unit })); 506 | assert.deepStrictEqual(sortedArray, [ 507 | { name: 'aa', unit: 'A01' }, 508 | { name: 'aa', unit: 'A10' }, 509 | { name: 'aa', unit: 'C2' }, 510 | { name: 'bb', unit: 'B3' }, 511 | ]); 512 | }); 513 | 514 | it('Should ignore empty array as a sorting prop', () => { 515 | assert.deepStrictEqual(sort([2, 1, 4]).asc([]), [1, 2, 4]); 516 | }); 517 | 518 | it('Should sort by computed property', () => { 519 | const repos = [ 520 | { openIssues: 0, closedIssues: 5 }, 521 | { openIssues: 4, closedIssues: 4 }, 522 | { openIssues: 3, closedIssues: 3 }, 523 | ]; 524 | 525 | const sorted1 = sort(repos).asc(r => r.openIssues + r.closedIssues); 526 | assert.deepStrictEqual(sorted1, [ 527 | { openIssues: 0, closedIssues: 5 }, 528 | { openIssues: 3, closedIssues: 3 }, 529 | { openIssues: 4, closedIssues: 4 }, 530 | ]); 531 | 532 | const sorted2 = sort(repos).desc(r => r.openIssues + r.closedIssues); 533 | assert.deepStrictEqual(sorted2, [ 534 | { openIssues: 4, closedIssues: 4 }, 535 | { openIssues: 3, closedIssues: 3 }, 536 | { openIssues: 0, closedIssues: 5 }, 537 | ]); 538 | }); 539 | 540 | it('Should not mutate sort by array', () => { 541 | const sortBy = [{ asc: 'name' }, { asc: 'unit' }]; 542 | const sorted = sort(multiPropArray).by(sortBy as any); 543 | assert.deepStrictEqual(sortBy, [{ asc: 'name' }, { asc: 'unit' }]); 544 | 545 | const sortedArray = sorted.map(arr => ({ name: arr.name, unit: arr.unit })); 546 | assert.deepStrictEqual(sortedArray, [ 547 | { name: 'aa', unit: 'A01' }, 548 | { name: 'aa', unit: 'A10' }, 549 | { name: 'aa', unit: 'C2' }, 550 | { name: 'bb', unit: 'B3' }, 551 | ]); 552 | }); 553 | 554 | it('Should sort readme example for natural sort correctly', () => { 555 | const testArr = ['image-2.jpg', 'image-11.jpg', 'image-3.jpg']; 556 | 557 | // By default fast-sort is not doing natural sort 558 | const sorted1 = sort(testArr).desc(); // => 559 | assert.deepStrictEqual(sorted1, ['image-3.jpg', 'image-2.jpg', 'image-11.jpg']); 560 | 561 | const sorted2 = sort(testArr).by({ 562 | desc: true, 563 | comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare, 564 | }); 565 | assert.deepStrictEqual(sorted2, ['image-11.jpg', 'image-3.jpg', 'image-2.jpg']); 566 | 567 | // If we want to reuse natural sort in multiple places we can create new sort instance 568 | const naturalSort = createNewSortInstance({ 569 | comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare, 570 | }); 571 | 572 | const sorted3 = naturalSort(testArr).asc(); 573 | assert.deepStrictEqual(sorted3, ['image-2.jpg', 'image-3.jpg', 'image-11.jpg']); 574 | 575 | const sorted4 = naturalSort(testArr).desc(); 576 | assert.deepStrictEqual(sorted4, ['image-11.jpg', 'image-3.jpg', 'image-2.jpg']); 577 | 578 | assert.notEqual(sorted3, testArr); 579 | }); 580 | 581 | it('Should create sort instance that sorts nil value to the top in desc order', () => { 582 | const nilSort = createNewSortInstance({ 583 | comparer(a, b):number { 584 | if (a == null) return 1; 585 | if (b == null) return -1; 586 | if (a < b) return -1; 587 | if (a === b) return 0; 588 | 589 | return 1; 590 | }, 591 | }); 592 | 593 | const sorter1 = nilSort(multiPropArray).asc(p => p.lastName); 594 | assert.deepStrictEqual(['aa', 'aa', 'bb', null], sorter1.map(p => p.lastName)); 595 | 596 | const sorter2 = nilSort(multiPropArray).desc(p => p.lastName); 597 | assert.deepStrictEqual([null, 'bb', 'aa', 'aa'], sorter2.map(p => p.lastName)); 598 | 599 | // By default custom sorter should not mutate provided array 600 | assert.notEqual(sorter1, multiPropArray); 601 | assert.notEqual(sorter2, multiPropArray); 602 | }); 603 | 604 | it('Should mutate array with custom sorter if inPlaceSorting provided', () => { 605 | const customInPlaceSorting = createNewSortInstance({ 606 | comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare, 607 | inPlaceSorting: true, // <= NOTE 608 | }); 609 | 610 | const sorted = customInPlaceSorting(flatArray).asc(); 611 | 612 | assert.equal(sorted, flatArray); 613 | assert.deepStrictEqual(flatArray, [1, 2, 3, 4, 5, 5]); 614 | }); 615 | 616 | it('Should be able to sort readonly arrays when not using inPlaceSorting', () => { 617 | const readOnlyArray = Object.freeze([2, 1, 4, 3]); 618 | 619 | const sorted = sort(readOnlyArray).asc(); 620 | assert.deepEqual(sorted, [1, 2, 3, 4]); 621 | 622 | // We can sort it with custom sorter if inPlaceSorting is false 623 | const custom = createNewSortInstance({ 624 | comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare, 625 | inPlaceSorting: false, // <= NOTE 626 | }); 627 | 628 | const sorted2 = custom(readOnlyArray).asc(); 629 | assert.deepEqual(sorted2, [1, 2, 3, 4]); 630 | 631 | // NOTE: will throw error if trying to sort it in place 632 | assert.throws( 633 | () => inPlaceSort(readOnlyArray as any).asc(), 634 | Error, 635 | 'Cannot assign to read only property \'0\' of object \'[object Array]\'', 636 | ); 637 | }); 638 | 639 | it('Should sort dates correctly when the same dates', () => { 640 | const testArr = [ 641 | { d: new Date(2000, 0, 1), n: 3 }, 642 | { d: new Date(2000, 0, 1), n: 1 }, 643 | { d: new Date(2000, 0, 1), n: 0 }, 644 | { d: new Date(2000, 0, 1), n: 2 }, 645 | { d: new Date(2000, 0, 1), n: 5 }, 646 | { d: new Date(2000, 0, 1), n: 4 }, 647 | ]; 648 | const sorted = sort(testArr).asc([ 649 | arr => arr.d, 650 | arr => arr.n, 651 | ]); 652 | assert.deepStrictEqual(sorted, [ 653 | { d: new Date(2000, 0, 1), n: 0 }, 654 | { d: new Date(2000, 0, 1), n: 1 }, 655 | { d: new Date(2000, 0, 1), n: 2 }, 656 | { d: new Date(2000, 0, 1), n: 3 }, 657 | { d: new Date(2000, 0, 1), n: 4 }, 658 | { d: new Date(2000, 0, 1), n: 5 }, 659 | ]); 660 | }); 661 | 662 | // BUG repo case: https://github.com/snovakovic/fast-sort/issues/62 663 | it('Should sort flat array in ascending order with multiple types', () => { 664 | const sorted1 = sort(['b', 3, 2, 1, 5, 'a', 5, 4]).asc(); 665 | assert.deepStrictEqual(sorted1, [1, 2, 3, 4, 5, 5, 'a', 'b']); 666 | 667 | const sorted2 = sort(['b', 3, 2, 1, 5, 'a', 5, 4]).desc(); 668 | assert.deepStrictEqual(sorted2, ['b', 'a', 5, 5, 4, 3, 2, 1]); 669 | 670 | const sorted3 = sort([1, 3, 'a', 'c', [1, 2], { a: 1 }, { a: 2 }, 4, 2, 4]).asc(); 671 | assert.deepStrictEqual(sorted3, [1, 2, 3, 4, 4, [1, 2], { a: 1 }, { a: 2 }, 'a', 'c']); 672 | 673 | const sorted4 = sort([1, 3, 2, '1a', 'a', '2', '5', 6]).asc(); 674 | assert.deepStrictEqual(sorted4, [1, 2, 3, 6, '1a', '2', '5', 'a']); 675 | }); 676 | 677 | it('Should be able to override natural sort with default comparer', () => { 678 | const naturalSort = createNewSortInstance({ 679 | comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare, 680 | }); 681 | 682 | const sorted = naturalSort([ 683 | { name: 'b2', temperature: 1 }, 684 | { name: 'a10', temperature: -1 }, 685 | { name: 'a1', temperature: 5 }, 686 | { name: 'a1', temperature: -10 }, 687 | { name: 'b3', temperature: 2 }, 688 | { name: 'b2', temperature: 0 }, 689 | ]).by([ 690 | { asc: 'name' }, 691 | { asc: 'temperature', comparer: defaultComparer }, 692 | ]); 693 | 694 | assert.deepEqual(sorted, [ 695 | { name: 'a1', temperature: -10 }, 696 | { name: 'a1', temperature: 5 }, 697 | { name: 'a10', temperature: -1 }, 698 | { name: 'b2', temperature: 0 }, 699 | { name: 'b2', temperature: 1 }, 700 | { name: 'b3', temperature: 2 }, 701 | ]); 702 | }); 703 | }); 704 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "sourceMap": true, 7 | "allowSyntheticDefaultImports": true, 8 | "noImplicitThis": true, 9 | "declaration": true, 10 | "declarationDir": "./dist", 11 | "outDir": "./dist", 12 | "baseUrl": ".", 13 | "lib": ["es6", "es2017"], 14 | "typeRoots": [ 15 | "./node_modules/@types" 16 | ] 17 | }, 18 | "exclude": [ 19 | "node_modules" 20 | ], 21 | "include": [ 22 | "src/sort.ts" 23 | ] 24 | } 25 | --------------------------------------------------------------------------------