├── .gitignore ├── .travis.yml ├── README.md ├── bower.json ├── index.d.ts ├── package.json ├── rollup.config.js ├── src ├── banner.js ├── stable.js └── test.js ├── stable.js └── stable.min.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | package-lock.json 3 | yarn.lock 4 | shrinkwrap.yaml 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 4 4 | - 6 5 | - 8 6 | - 9 7 | notifications: 8 | email: 9 | - stephan@angrybytes.com 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **NOTE: Modern JavaScript already guarantees `Array#sort()` is a stable sort, so this library has very little use and has been archived.** 2 | 3 | See [the compatibility table on MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility) for the exact versions of JavaScript runtimes that implement stable sort natively. 4 | 5 | --- 6 | 7 | ## Stable 8 | 9 | A stable array sort, because `Array#sort()` is not guaranteed stable. 10 | 11 | MIT licensed. 12 | 13 | [![Node.js CI](https://secure.travis-ci.org/Two-Screen/stable.png)](http://travis-ci.org/Two-Screen/stable) 14 | 15 | [![Browser CI](http://ci.testling.com/Two-Screen/stable.png)](http://ci.testling.com/Two-Screen/stable) 16 | 17 | #### From npm 18 | 19 | Install with: 20 | 21 | ```sh 22 | npm install stable 23 | ``` 24 | 25 | Then use it in Node.js or some other CommonJS environment as: 26 | 27 | ```js 28 | const stable = require('stable') 29 | ``` 30 | 31 | #### From the browser 32 | 33 | Include [`stable.js`] or the minified version [`stable.min.js`] 34 | in your page, then call `stable()`. 35 | 36 | [`stable.js`]: https://raw.github.com/Two-Screen/stable/master/stable.js 37 | [`stable.min.js`]: https://raw.github.com/Two-Screen/stable/master/stable.min.js 38 | 39 | #### Usage 40 | 41 | The default sort is, as with `Array#sort`, lexicographical: 42 | 43 | ```js 44 | stable(['foo', 'bar', 'baz']) // => ['bar', 'baz', 'foo'] 45 | stable([10, 1, 5]) // => [1, 10, 5] 46 | ``` 47 | 48 | Unlike `Array#sort`, the default sort is **NOT** in-place. To do an in-place 49 | sort, use `stable.inplace`, which otherwise works the same: 50 | 51 | ```js 52 | const arr = [10, 1, 5] 53 | stable(arr) === arr // => false 54 | stable.inplace(arr) === arr // => true 55 | ``` 56 | 57 | A comparator function can be specified: 58 | 59 | ```js 60 | // Regular sort() compatible comparator, that returns a number. 61 | // This demonstrates the default behavior. 62 | const lexCmp = (a, b) => String(a).localeCompare(b) 63 | stable(['foo', 'bar', 'baz'], lexCmp) // => ['bar', 'baz', 'foo'] 64 | 65 | // Boolean comparator. Sorts `b` before `a` if true. 66 | // This demonstrates a simple way to sort numerically. 67 | const greaterThan = (a, b) => a > b 68 | stable([10, 1, 5], greaterThan) // => [1, 5, 10] 69 | ``` 70 | 71 | #### License 72 | 73 | Copyright (C) 2018 Angry Bytes and contributors. 74 | 75 | Permission is hereby granted, free of charge, to any person obtaining a copy of 76 | this software and associated documentation files (the "Software"), to deal in 77 | the Software without restriction, including without limitation the rights to 78 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 79 | of the Software, and to permit persons to whom the Software is furnished to do 80 | so, subject to the following conditions: 81 | 82 | The above copyright notice and this permission notice shall be included in all 83 | copies or substantial portions of the Software. 84 | 85 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 86 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 87 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 88 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 89 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 90 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 91 | SOFTWARE. 92 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stable", 3 | "version": "0.1.8", 4 | "keywords": ["stable", "array", "sort"], 5 | "description": "A stable array sort for JavaScript", 6 | "homepage": "https://github.com/Two-Screen/stable", 7 | "main": "stable.js", 8 | "moduleType": ["globals","node"], 9 | "authors": [ 10 | "Angry Bytes ", 11 | "Domenic Denicola ", 12 | "Mattias Buelens ", 13 | "Stéphan Kochen ", 14 | "Yaffle" 15 | ], 16 | "license": "MIT", 17 | "ignore": [ 18 | "**/.*", 19 | "node_modules", 20 | "bower.json", 21 | "package.json", 22 | "rollup.config.js", 23 | "test.js" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export as namespace stable; 2 | export = stable; 3 | 4 | type Comparator = ((a : T, b : T)=>boolean) | ((a: T, b : T)=>number); 5 | 6 | declare function stable(array : T[], comparator? : Comparator) : T[]; 7 | declare namespace stable { 8 | export function inplace(array: T[], comparator? : Comparator) : T[]; 9 | } 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stable", 3 | "version": "0.1.8", 4 | "keywords": [ 5 | "stable", 6 | "array", 7 | "sort" 8 | ], 9 | "description": "A stable array sort for JavaScript", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/Two-Screen/stable.git" 13 | }, 14 | "main": "./stable.js", 15 | "types": "./index.d.ts", 16 | "files": [ 17 | "stable.js", 18 | "stable.min.js", 19 | "index.d.ts" 20 | ], 21 | "devDependencies": { 22 | "rollup": "^0.57.1", 23 | "standard": "^11.0.1", 24 | "tape": "^4.6.3", 25 | "uglify-js": "^3.3.21" 26 | }, 27 | "scripts": { 28 | "test": "standard src/ && node ./src/test.js", 29 | "prepare": "npm run build && npm run minify", 30 | "build": "rollup -c", 31 | "minify": "uglifyjs --comments \"/^!/\" -c -m -o ./stable.min.js ./stable.js" 32 | }, 33 | "testling": { 34 | "files": "./src/test.js", 35 | "browsers": [ 36 | "ie6", 37 | "ie7", 38 | "ie8", 39 | "ie9", 40 | "ie10", 41 | "firefox/25", 42 | "chrome/31", 43 | "safari/6.0", 44 | "opera/12.0", 45 | "opera/17.0", 46 | "iphone/6.0", 47 | "android-browser/4.2" 48 | ] 49 | }, 50 | "author": "Angry Bytes ", 51 | "contributors": [ 52 | "Domenic Denicola ", 53 | "Mattias Buelens ", 54 | "Stéphan Kochen ", 55 | "Yaffle" 56 | ], 57 | "license": "MIT" 58 | } 59 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const pkg = require('./package.json'); 3 | 4 | const banner = fs.readFileSync('./src/banner.js', 'utf8') 5 | .replace('${version}', pkg.version); 6 | 7 | module.exports = { 8 | input: './src/stable.js', 9 | output: [{ 10 | banner, 11 | file: pkg.main, 12 | format: 'umd', 13 | name: 'stable' 14 | }] 15 | }; 16 | -------------------------------------------------------------------------------- /src/banner.js: -------------------------------------------------------------------------------- 1 | //! stable.js ${version}, https://github.com/Two-Screen/stable 2 | //! © 2018 Angry Bytes and contributors. MIT licensed. 3 | -------------------------------------------------------------------------------- /src/stable.js: -------------------------------------------------------------------------------- 1 | // A stable array sort, because `Array#sort()` is not guaranteed stable. 2 | // This is an implementation of merge sort, without recursion. 3 | 4 | var stable = function (arr, comp) { 5 | return exec(arr.slice(), comp) 6 | } 7 | 8 | stable.inplace = function (arr, comp) { 9 | var result = exec(arr, comp) 10 | 11 | // This simply copies back if the result isn't in the original array, 12 | // which happens on an odd number of passes. 13 | if (result !== arr) { 14 | pass(result, null, arr.length, arr) 15 | } 16 | 17 | return arr 18 | } 19 | 20 | // Execute the sort using the input array and a second buffer as work space. 21 | // Returns one of those two, containing the final result. 22 | function exec(arr, comp) { 23 | if (typeof(comp) !== 'function') { 24 | comp = function (a, b) { 25 | return String(a).localeCompare(b) 26 | } 27 | } 28 | 29 | // Short-circuit when there's nothing to sort. 30 | var len = arr.length 31 | if (len <= 1) { 32 | return arr 33 | } 34 | 35 | // Rather than dividing input, simply iterate chunks of 1, 2, 4, 8, etc. 36 | // Chunks are the size of the left or right hand in merge sort. 37 | // Stop when the left-hand covers all of the array. 38 | var buffer = new Array(len) 39 | for (var chk = 1; chk < len; chk *= 2) { 40 | pass(arr, comp, chk, buffer) 41 | 42 | var tmp = arr 43 | arr = buffer 44 | buffer = tmp 45 | } 46 | 47 | return arr 48 | } 49 | 50 | // Run a single pass with the given chunk size. 51 | var pass = function (arr, comp, chk, result) { 52 | var len = arr.length 53 | var i = 0 54 | // Step size / double chunk size. 55 | var dbl = chk * 2 56 | // Bounds of the left and right chunks. 57 | var l, r, e 58 | // Iterators over the left and right chunk. 59 | var li, ri 60 | 61 | // Iterate over pairs of chunks. 62 | for (l = 0; l < len; l += dbl) { 63 | r = l + chk 64 | e = r + chk 65 | if (r > len) r = len 66 | if (e > len) e = len 67 | 68 | // Iterate both chunks in parallel. 69 | li = l 70 | ri = r 71 | while (true) { 72 | // Compare the chunks. 73 | if (li < r && ri < e) { 74 | // This works for a regular `sort()` compatible comparator, 75 | // but also for a simple comparator like: `a > b` 76 | if (comp(arr[li], arr[ri]) <= 0) { 77 | result[i++] = arr[li++] 78 | } 79 | else { 80 | result[i++] = arr[ri++] 81 | } 82 | } 83 | // Nothing to compare, just flush what's left. 84 | else if (li < r) { 85 | result[i++] = arr[li++] 86 | } 87 | else if (ri < e) { 88 | result[i++] = arr[ri++] 89 | } 90 | // Both iterators are at the chunk ends. 91 | else { 92 | break 93 | } 94 | } 95 | } 96 | } 97 | 98 | export default stable 99 | -------------------------------------------------------------------------------- /src/test.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var stable = require('../stable.js') 3 | 4 | function cmp (a, b) { 5 | if (a === b) return 0 6 | if (a > b) return 1 7 | return -1 8 | } 9 | 10 | function gt (a, b) { 11 | return a > b 12 | } 13 | 14 | function diff (a, b) { 15 | return a - b 16 | } 17 | 18 | function objCmp (a, b) { 19 | return a.x > b.x 20 | } 21 | 22 | test('always returns a new array', function (t) { 23 | var array 24 | 25 | array = [] 26 | t.doesNotEqual(array, stable(array)) 27 | 28 | array = [1] 29 | t.doesNotEqual(array, stable(array)) 30 | 31 | array = [1, 2] 32 | t.doesNotEqual(array, stable(array)) 33 | 34 | t.end() 35 | }) 36 | 37 | test('in-place always returns the same array', function (t) { 38 | var array 39 | 40 | array = [] 41 | t.equal(array, stable.inplace(array)) 42 | 43 | array = [1] 44 | t.equal(array, stable.inplace(array)) 45 | 46 | array = [1, 2] 47 | t.equal(array, stable.inplace(array)) 48 | 49 | t.end() 50 | }) 51 | 52 | test('basic sorting', function (t) { 53 | t.same( 54 | stable(['foo', 'bar', 'baz']), 55 | ['bar', 'baz', 'foo'] 56 | ) 57 | 58 | t.same( 59 | stable([9, 2, 10, 5, 4, 3, 0, 1, 8, 6, 7]), 60 | [0, 1, 10, 2, 3, 4, 5, 6, 7, 8, 9] 61 | ) 62 | 63 | t.end() 64 | }) 65 | 66 | test('comparators', function (t) { 67 | t.same( 68 | stable([9, 2, 10, 5, 4, 3, 0, 1, 8, 6, 7], cmp), 69 | [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 70 | ) 71 | 72 | t.same( 73 | stable([9, 2, 10, 5, 4, 3, 0, 1, 8, 6, 7], gt), 74 | [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 75 | ) 76 | 77 | t.same( 78 | stable([9, 2, 10, 5, 4, 3, 0, 1, 8, 6, 7], diff), 79 | [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 80 | ) 81 | 82 | t.same( 83 | stable([{ x: 4 }, { x: 3 }, { x: 5 }], objCmp), 84 | [{ x: 3 }, { x: 4 }, { x: 5 }] 85 | ) 86 | 87 | t.end() 88 | }) 89 | 90 | test('stable sorting', function (t) { 91 | function cmp (a, b) { 92 | return a.x > b.x 93 | } 94 | t.same( 95 | stable([{ x: 3, y: 1 }, { x: 4, y: 2 }, { x: 3, y: 3 }, { x: 5, y: 4 }, { x: 3, y: 5 }], cmp), 96 | [{ x: 3, y: 1 }, { x: 3, y: 3 }, { x: 3, y: 5 }, { x: 4, y: 2 }, { x: 5, y: 4 }] 97 | ) 98 | 99 | t.end() 100 | }) 101 | -------------------------------------------------------------------------------- /stable.js: -------------------------------------------------------------------------------- 1 | //! stable.js 0.1.8, https://github.com/Two-Screen/stable 2 | //! © 2018 Angry Bytes and contributors. MIT licensed. 3 | 4 | (function (global, factory) { 5 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 6 | typeof define === 'function' && define.amd ? define(factory) : 7 | (global.stable = factory()); 8 | }(this, (function () { 'use strict'; 9 | 10 | // A stable array sort, because `Array#sort()` is not guaranteed stable. 11 | // This is an implementation of merge sort, without recursion. 12 | 13 | var stable = function (arr, comp) { 14 | return exec(arr.slice(), comp) 15 | }; 16 | 17 | stable.inplace = function (arr, comp) { 18 | var result = exec(arr, comp); 19 | 20 | // This simply copies back if the result isn't in the original array, 21 | // which happens on an odd number of passes. 22 | if (result !== arr) { 23 | pass(result, null, arr.length, arr); 24 | } 25 | 26 | return arr 27 | }; 28 | 29 | // Execute the sort using the input array and a second buffer as work space. 30 | // Returns one of those two, containing the final result. 31 | function exec(arr, comp) { 32 | if (typeof(comp) !== 'function') { 33 | comp = function (a, b) { 34 | return String(a).localeCompare(b) 35 | }; 36 | } 37 | 38 | // Short-circuit when there's nothing to sort. 39 | var len = arr.length; 40 | if (len <= 1) { 41 | return arr 42 | } 43 | 44 | // Rather than dividing input, simply iterate chunks of 1, 2, 4, 8, etc. 45 | // Chunks are the size of the left or right hand in merge sort. 46 | // Stop when the left-hand covers all of the array. 47 | var buffer = new Array(len); 48 | for (var chk = 1; chk < len; chk *= 2) { 49 | pass(arr, comp, chk, buffer); 50 | 51 | var tmp = arr; 52 | arr = buffer; 53 | buffer = tmp; 54 | } 55 | 56 | return arr 57 | } 58 | 59 | // Run a single pass with the given chunk size. 60 | var pass = function (arr, comp, chk, result) { 61 | var len = arr.length; 62 | var i = 0; 63 | // Step size / double chunk size. 64 | var dbl = chk * 2; 65 | // Bounds of the left and right chunks. 66 | var l, r, e; 67 | // Iterators over the left and right chunk. 68 | var li, ri; 69 | 70 | // Iterate over pairs of chunks. 71 | for (l = 0; l < len; l += dbl) { 72 | r = l + chk; 73 | e = r + chk; 74 | if (r > len) r = len; 75 | if (e > len) e = len; 76 | 77 | // Iterate both chunks in parallel. 78 | li = l; 79 | ri = r; 80 | while (true) { 81 | // Compare the chunks. 82 | if (li < r && ri < e) { 83 | // This works for a regular `sort()` compatible comparator, 84 | // but also for a simple comparator like: `a > b` 85 | if (comp(arr[li], arr[ri]) <= 0) { 86 | result[i++] = arr[li++]; 87 | } 88 | else { 89 | result[i++] = arr[ri++]; 90 | } 91 | } 92 | // Nothing to compare, just flush what's left. 93 | else if (li < r) { 94 | result[i++] = arr[li++]; 95 | } 96 | else if (ri < e) { 97 | result[i++] = arr[ri++]; 98 | } 99 | // Both iterators are at the chunk ends. 100 | else { 101 | break 102 | } 103 | } 104 | } 105 | }; 106 | 107 | return stable; 108 | 109 | }))); 110 | -------------------------------------------------------------------------------- /stable.min.js: -------------------------------------------------------------------------------- 1 | //! stable.js 0.1.8, https://github.com/Two-Screen/stable 2 | //! © 2018 Angry Bytes and contributors. MIT licensed. 3 | !function(e,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):e.stable=n()}(this,function(){"use strict";var e=function(e,n){return t(e.slice(),n)};function t(e,n){"function"!=typeof n&&(n=function(e,n){return String(e).localeCompare(n)});var r=e.length;if(r<=1)return e;for(var t=new Array(r),f=1;f