├── .gitignore ├── LICENSE ├── README.md ├── index.js ├── package.json └── test └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | package-lock.json 3 | 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jonathan Gros-Dubois 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # proper-skip-list 2 | A fast skip list implementation which supports fetching and deleting multiple adjacent entries at a time. 3 | It's ideal for efficiently storing and fetching ordered data. 4 | 5 | ## Installation 6 | 7 | ```bash 8 | npm install proper-skip-list --save 9 | ``` 10 | 11 | ## Performance 12 | 13 | ### Time complexity 14 | 15 | Average case, relative to the total number of elements in the list: 16 | 17 | - **upsert**: `O(log n)` 18 | - **find**: `O(log n)` 19 | - **has**: `O(log n)` 20 | - **extract**: `O(log n)` 21 | - **delete**: `O(log n)` 22 | - **findEntries**: `O(log n)` 23 | - **minKey**: `O(1)` 24 | - **maxKey**: `O(1)` 25 | - **minValue**: `O(1)` 26 | - **maxValue**: `O(1)` 27 | - **findEntriesFromMin**: `O(1)` 28 | - **findEntriesFromMax**: `O(1)` 29 | - **deleteRange**: `O(log n)` 30 | - **clear**: `O(1)` 31 | - **get length**: `O(1)` 32 | 33 | Note that the **deleteRange** method is `O(log n)` relative to the number of elements in the list. 34 | The time complexity relative to the number of elements which will be removed from the list is different and it varies depending on whether the `updateLength` constructor option is `true` or `false`. If `true`, time complexity is `O(n)`, if `false`, it is `O(1)`. 35 | 36 | ### Space complexity 37 | 38 | - Average: `O(n)` 39 | - Worst case: `O(n log n)` 40 | 41 | The `stackUpProbability` option can be modified to optimize space usage and performance to suit more advanced use cases but it should be used cautiously. 42 | 43 | ## API 44 | 45 | Keys can be of type `string` and/or `number`. Internally, different types are handled separately. All numbers have priority over strings. 46 | If strings are used, the order is lexicographic. 47 | 48 | ### Constructor 49 | 50 | ```js 51 | const ProperSkipList = require('proper-skip-list'); 52 | 53 | // Default options: 54 | let skipList = new ProperSkipList(); 55 | 56 | // Or... 57 | 58 | // Sample custom options: 59 | let skipList = new ProperSkipList({ 60 | stackUpProbability: 0.5, // 0.25 by default 61 | updateLength: false // true by default 62 | }); 63 | ``` 64 | 65 | - The `stackUpProbability` option is the probability of an entry stacking up a single level when it is inserted into the skip list. 66 | - The `updateLength` option allows you to disable the `length` property of the skip list. Not updating the `length` of the skip list can make the `deleteRange` method faster for certain use cases which involve deleting large segments of the skip list in a single operation. 67 | 68 | ### Methods 69 | 70 | - **`upsert(key, value)`**: Insert a value into the skip list at the specified key. If a value already exists at that key, it will be replaced. 71 | - **`find(key)`**: Get the value stored at the specified key. This method returns `undefined` if a matching value is not found. 72 | - **`has(key)`**: Check if an entry with the specified key exists. 73 | - **`extract(key)`**: Remove a value at the specified key if it exits. This method returns the value or `undefined` if not found. 74 | - **`delete(key)`**: Remove a value at the specified key if it exits. This method returns a boolean to indicate whether or not a value existed at that key. 75 | - **`findEntries(fromKey)`**: Get iterators for entries starting at the specified key in ascending or descending order. The `fromKey` does not need to have an exact match in the list; this method can therefore be used to iterate over nearby keys. The return value is an object in the form `{matchingValue, asc, desc}`. If an exact match for `fromKey` was found, the `matchingValue` property will contain the value at that key, otherwise it will be `undefined`. The `asc` property is an `iterable` iterator which can be used to iterate over records in ascending order starting at `fromKey` (or the next highest value if no exact match is found). The `desc` property is an `iterable` iterator which can be used to iterate over records in descending order starting at `fromKey` (or the next lowest value if no exact match is found). 76 | - **`findEntriesFromMin()`**: Iterate over entries in ascending order starting at the lowest key. This method returns an iterable iterator. 77 | - **`findEntriesFromMax()`**: Iterate over entries in descending order starting at the highest key. This method returns an iterable iterator. 78 | - **`minKey()`**: Get the lowest key in the list. 79 | - **`maxKey()`**: Get the highest key in the list. 80 | - **`minValue()`**: Get the value stored at the lowest key in the list. 81 | - **`maxValue()`**: Get the value stored at the highest key in the list. 82 | - **`deleteRange(fromKey, toKey, deleteLeft, deleteRight)`**: Delete multiple entries with a single operation. The `fromKey` argument specifies the starting key in the range does not need to have an exact match in the list. The `toKey` argument is the end key, it also does not need to have an exact match. The `deleteLeft` argument can be used to specify whether or not the value at `fromKey` should also be deleted if found. The `deleteRight` argument argument can be used to specify whether or not the value at `toKey` should also be deleted if found. By default, only the in-between values will be deleted. If `fromKey` is null, it will delete from the beginning of the skip list. If `fromKey` is null, it will delete until the end of the skip list. 83 | - **`clear`**: Empty/reset the skip list. 84 | 85 | ### Properties 86 | 87 | Note that most of these properties were intended to be read-only. 88 | 89 | - **`length`**: The number of entries stored in the skip list. It will be `undefined` if the `updateLength` constructor option is `false`. 90 | - **`stackUpProbability`**: The probability of an entry stacking up a single level when it is inserted into the skip list. 91 | - **`updateLength`**: Whether or not the length property is being updated. 92 | - **`head`**: The head group of the skip list which holds an array of head nodes. This can be used to traverse all layers of the skip list for more advanced use cases. 93 | - **`tail`**: The tail group of the skip list which holds an array of tail nodes. This can be used to traverse all layers of the skip list for more advanced use cases. 94 | 95 | ### Iterators 96 | 97 | Iterators returned by methods like `findEntries`, `findEntriesFromMin` and `findEntriesFromMax` are iterable and can be looped over like this (example): 98 | 99 | ```js 100 | let {asc, desc} = this.findEntries(1234); 101 | for (let [key, value, i] of asc) { 102 | 103 | // ... Do something. 104 | 105 | if (i > 100) break; 106 | } 107 | ``` 108 | 109 | Alternatively, `asc.next()` could be called manually from inside a `while` loop. 110 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const DEFAULT_STACK_UP_PROBABILITY = 0.25; 2 | 3 | class ProperSkipList { 4 | constructor(options) { 5 | options = options || {}; 6 | this.stackUpProbability = options.stackUpProbability || DEFAULT_STACK_UP_PROBABILITY; 7 | this.updateLength = options.updateLength !== false; 8 | this.typePriorityMap = { 9 | 'undefined': 0, 10 | 'object': 1, 11 | 'number': 2, 12 | 'bigint': 2, 13 | 'string': 3 14 | }; 15 | this.clear(); 16 | } 17 | 18 | clear() { 19 | let headNode = { 20 | prev: null 21 | }; 22 | let tailNode = { 23 | next: null 24 | }; 25 | this.head = { 26 | isHead: true, 27 | key: undefined, 28 | value: undefined, 29 | nodes: [headNode] 30 | }; 31 | this.tail = { 32 | isTail: true, 33 | key: undefined, 34 | value: undefined, 35 | nodes: [tailNode] 36 | }; 37 | headNode.next = tailNode; 38 | tailNode.prev = headNode; 39 | headNode.group = this.head; 40 | tailNode.group = this.tail; 41 | this.length = this.updateLength ? 0 : undefined; 42 | } 43 | 44 | upsert(key, value) { 45 | let {matchingNode, prevNode, searchPath} = this._searchAndTrack(key); 46 | if (matchingNode) { 47 | let previousValue = matchingNode.group.value; 48 | matchingNode.group.value = value; 49 | return previousValue; 50 | } 51 | 52 | // Insert the entry. 53 | let newNode = { 54 | prev: prevNode, 55 | next: prevNode.next 56 | }; 57 | let newGroup = { 58 | key, 59 | value, 60 | nodes: [newNode] 61 | }; 62 | newNode.group = newGroup; 63 | prevNode.next = newNode; 64 | newNode.next.prev = newNode; 65 | 66 | // Stack up the entry for fast search. 67 | let layerIndex = 1; 68 | while (Math.random() < this.stackUpProbability) { 69 | let prevLayerNode = searchPath[layerIndex]; 70 | if (!prevLayerNode) { 71 | let newHeadNode = { 72 | prev: null, 73 | group: this.head 74 | }; 75 | let newTailNode = { 76 | next: null, 77 | group: this.tail 78 | }; 79 | newHeadNode.next = newTailNode; 80 | this.head.nodes.push(newHeadNode); 81 | newTailNode.prev = newHeadNode; 82 | this.tail.nodes.push(newTailNode); 83 | prevLayerNode = newHeadNode; 84 | } 85 | let newNode = { 86 | prev: prevLayerNode, 87 | next: prevLayerNode.next, 88 | group: newGroup 89 | }; 90 | prevLayerNode.next = newNode; 91 | newNode.next.prev = newNode; 92 | newGroup.nodes.push(newNode); 93 | layerIndex++; 94 | } 95 | if (this.updateLength) this.length++; 96 | 97 | return undefined; 98 | } 99 | 100 | find(key) { 101 | let {matchingNode} = this._search(key); 102 | return matchingNode ? matchingNode.group.value : undefined; 103 | } 104 | 105 | has(key) { 106 | return !!this.find(key); 107 | } 108 | 109 | _isAGreaterThanB(a, b) { 110 | let typeA = typeof a; 111 | let typeB = typeof b; 112 | if (typeA === typeB) { 113 | return a > b; 114 | } 115 | let typeAPriority = this.typePriorityMap[typeA]; 116 | let typeBPriority = this.typePriorityMap[typeB]; 117 | if (typeAPriority === typeBPriority) { 118 | return a > b; 119 | } 120 | return typeAPriority > typeBPriority; 121 | } 122 | 123 | // The two search methods are similar but were separated for performance reasons. 124 | _searchAndTrack(key) { 125 | let layerCount = this.head.nodes.length; 126 | let searchPath = new Array(layerCount); 127 | let layerIndex = layerCount - 1; 128 | let currentNode = this.head.nodes[layerIndex]; 129 | let prevNode = currentNode; 130 | 131 | while (true) { 132 | let currentNodeGroup = currentNode.group; 133 | let currentKey = currentNodeGroup.key; 134 | if (!currentNodeGroup.isTail) { 135 | if (this._isAGreaterThanB(key, currentKey) || currentNodeGroup.isHead) { 136 | prevNode = currentNode; 137 | currentNode = currentNode.next; 138 | continue; 139 | } 140 | if (key === currentKey) { 141 | let matchingNode = currentNodeGroup.nodes[0]; 142 | searchPath[layerIndex] = matchingNode; 143 | return {matchingNode, prevNode: matchingNode.prev, searchPath}; 144 | } 145 | } 146 | searchPath[layerIndex] = prevNode; 147 | if (--layerIndex < 0) { 148 | return {matchingNode: undefined, prevNode, searchPath}; 149 | } 150 | currentNode = prevNode.group.nodes[layerIndex]; 151 | } 152 | } 153 | 154 | _search(key) { 155 | let layerIndex = this.head.nodes.length - 1; 156 | let currentNode = this.head.nodes[layerIndex]; 157 | let prevNode = currentNode; 158 | while (true) { 159 | let currentNodeGroup = currentNode.group; 160 | let currentKey = currentNodeGroup.key; 161 | if (!currentNodeGroup.isTail) { 162 | if (this._isAGreaterThanB(key, currentKey) || currentNodeGroup.isHead) { 163 | prevNode = currentNode; 164 | currentNode = currentNode.next; 165 | continue; 166 | } 167 | if (key === currentKey) { 168 | let matchingNode = currentNodeGroup.nodes[0]; 169 | return {matchingNode, prevNode: matchingNode.prev}; 170 | } 171 | } 172 | if (--layerIndex < 0) { 173 | return {matchingNode: undefined, prevNode}; 174 | } 175 | currentNode = prevNode.group.nodes[layerIndex]; 176 | } 177 | } 178 | 179 | findEntriesFromMin() { 180 | return this._createEntriesIteratorAsc(this.head.nodes[0].next); 181 | } 182 | 183 | findEntriesFromMax() { 184 | return this._createEntriesIteratorDesc(this.tail.nodes[0].prev); 185 | } 186 | 187 | minEntry() { 188 | let [key, value] = this.findEntriesFromMin().next().value; 189 | return [key, value]; 190 | } 191 | 192 | maxEntry() { 193 | let [key, value] = this.findEntriesFromMax().next().value; 194 | return [key, value]; 195 | } 196 | 197 | minKey() { 198 | return this.minEntry()[0]; 199 | } 200 | 201 | maxKey() { 202 | return this.maxEntry()[0]; 203 | } 204 | 205 | minValue() { 206 | return this.minEntry()[1]; 207 | } 208 | 209 | maxValue() { 210 | return this.maxEntry()[1]; 211 | } 212 | 213 | _extractNode(matchingNode) { 214 | let nodes = matchingNode.group.nodes; 215 | for (let layerNode of nodes) { 216 | let prevNode = layerNode.prev; 217 | prevNode.next = layerNode.next; 218 | prevNode.next.prev = prevNode; 219 | } 220 | if (this.updateLength) this.length--; 221 | return matchingNode.group.value; 222 | } 223 | 224 | extract(key) { 225 | let {matchingNode} = this._search(key); 226 | if (matchingNode) { 227 | return this._extractNode(matchingNode); 228 | } 229 | return undefined; 230 | } 231 | 232 | delete(key) { 233 | return this.extract(key) !== undefined; 234 | } 235 | 236 | findEntries(fromKey) { 237 | let {matchingNode, prevNode} = this._search(fromKey); 238 | if (matchingNode) { 239 | return { 240 | matchingValue: matchingNode.group.value, 241 | asc: this._createEntriesIteratorAsc(matchingNode), 242 | desc: this._createEntriesIteratorDesc(matchingNode) 243 | }; 244 | } 245 | return { 246 | matchingValue: undefined, 247 | asc: this._createEntriesIteratorAsc(prevNode.next), 248 | desc: this._createEntriesIteratorDesc(prevNode) 249 | }; 250 | } 251 | 252 | deleteRange(fromKey, toKey, deleteLeft, deleteRight) { 253 | if (fromKey == null) { 254 | fromKey = this.minKey(); 255 | deleteLeft = true; 256 | } 257 | if (toKey == null) { 258 | toKey = this.maxKey(); 259 | deleteRight = true; 260 | } 261 | if (this._isAGreaterThanB(fromKey, toKey)) { 262 | return; 263 | } 264 | let {prevNode: fromNode, searchPath: leftSearchPath, matchingNode: matchingLeftNode} = this._searchAndTrack(fromKey); 265 | let {prevNode: toNode, searchPath: rightSearchPath, matchingNode: matchingRightNode} = this._searchAndTrack(toKey); 266 | let leftNode = matchingLeftNode ? matchingLeftNode : fromNode; 267 | let rightNode = matchingRightNode ? matchingRightNode : toNode.next; 268 | 269 | if (leftNode === rightNode) { 270 | if (deleteLeft) { 271 | this._extractNode(leftNode); 272 | } 273 | return; 274 | } 275 | 276 | if (this.updateLength) { 277 | let currentNode = leftNode; 278 | while (currentNode && currentNode.next !== rightNode) { 279 | this.length--; 280 | currentNode = currentNode.next; 281 | } 282 | } 283 | 284 | let leftGroupNodes = leftNode.group.nodes; 285 | let rightGroupNodes = rightNode.group.nodes; 286 | let layerCount = this.head.nodes.length; 287 | 288 | for (let layerIndex = 0; layerIndex < layerCount; layerIndex++) { 289 | let layerLeftNode = leftGroupNodes[layerIndex]; 290 | let layerRightNode = rightGroupNodes[layerIndex]; 291 | 292 | if (layerLeftNode && layerRightNode) { 293 | layerLeftNode.next = layerRightNode; 294 | layerRightNode.prev = layerLeftNode; 295 | continue; 296 | } 297 | if (layerLeftNode) { 298 | let layerRightmostNode = rightSearchPath[layerIndex]; 299 | if (!layerRightmostNode.group.isTail) { 300 | layerRightmostNode = layerRightmostNode.next; 301 | } 302 | layerLeftNode.next = layerRightmostNode; 303 | layerRightmostNode.prev = layerLeftNode; 304 | continue; 305 | } 306 | if (layerRightNode) { 307 | let layerLeftmostNode = leftSearchPath[layerIndex]; 308 | layerLeftmostNode.next = layerRightNode; 309 | layerRightNode.prev = layerLeftmostNode; 310 | continue; 311 | } 312 | // If neither left nor right nodes are present on the layer, connect based 313 | // on search path to remove in-between entries. 314 | let layerRightmostNode = rightSearchPath[layerIndex]; 315 | if (!layerRightmostNode.group.isTail) { 316 | layerRightmostNode = layerRightmostNode.next; 317 | } 318 | let layerLeftmostNode = leftSearchPath[layerIndex]; 319 | layerLeftmostNode.next = layerRightmostNode; 320 | layerRightmostNode.prev = layerLeftmostNode; 321 | } 322 | if (deleteLeft && matchingLeftNode) { 323 | this._extractNode(matchingLeftNode); 324 | } 325 | if (deleteRight && matchingRightNode) { 326 | this._extractNode(matchingRightNode); 327 | } 328 | } 329 | 330 | _createEntriesIteratorAsc(currentNode) { 331 | let i = 0; 332 | return { 333 | next: function () { 334 | let currentGroup = currentNode.group; 335 | if (currentGroup.isTail) { 336 | return { 337 | value: [currentNode.key, currentNode.value, i], 338 | done: true 339 | } 340 | } 341 | currentNode = currentNode.next; 342 | return { 343 | value: [currentGroup.key, currentGroup.value, i++], 344 | done: currentGroup.isTail 345 | }; 346 | }, 347 | [Symbol.iterator]: function () { return this; } 348 | }; 349 | } 350 | 351 | _createEntriesIteratorDesc(currentNode) { 352 | let i = 0; 353 | return { 354 | next: function () { 355 | let currentGroup = currentNode.group; 356 | if (currentGroup.isHead) { 357 | return { 358 | value: [currentNode.key, currentNode.value, i], 359 | done: true 360 | } 361 | } 362 | currentNode = currentNode.prev; 363 | return { 364 | value: [currentGroup.key, currentGroup.value, i++], 365 | done: currentGroup.isHead 366 | }; 367 | }, 368 | [Symbol.iterator]: function () { return this; } 369 | }; 370 | } 371 | } 372 | 373 | module.exports = ProperSkipList; 374 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "proper-skip-list", 3 | "version": "4.1.0", 4 | "description": "Fast skip list with flexible interface to traverse neighboring nodes bidirectionally.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha --reporter spec --timeout 3000 --slow 3000" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/jondubois/proper-skip-list.git" 12 | }, 13 | "keywords": [ 14 | "skip", 15 | "list", 16 | "proper", 17 | "fast" 18 | ], 19 | "author": "Jonathan Gros-Dubois", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/jondubois/proper-skip-list/issues" 23 | }, 24 | "homepage": "https://github.com/jondubois/proper-skip-list#readme", 25 | "devDependencies": { 26 | "mocha": "^7.0.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const ProperSkipList = require('../'); 3 | 4 | function getLayerEntries(skipList) { 5 | let layerIndex = 0; 6 | let layerHeadNode = skipList.head.nodes[layerIndex]; 7 | let layers = []; 8 | let c = 0; 9 | while (layerHeadNode) { 10 | let layerEntries = []; 11 | let currentNode = layerHeadNode; 12 | while (currentNode) { 13 | let {key, value} = currentNode.group; 14 | layerEntries.push([key, value]); 15 | currentNode = currentNode.next; 16 | } 17 | layers.push(layerEntries); 18 | layerIndex++; 19 | layerHeadNode = skipList.head.nodes[layerIndex]; 20 | } 21 | return layers; 22 | } 23 | 24 | function getLayerKeys(skipList) { 25 | return getLayerEntries(skipList).map((layer) => layer.map((entry) => entry[0])); 26 | } 27 | 28 | function logLayerKeys(skipList) { 29 | let layers = getLayerEntries(skipList).map((layer) => layer.map((entry) => { 30 | let key = entry[0]; 31 | if (typeof key === 'string') { 32 | return `"${key}"`; 33 | } 34 | return key; 35 | })).reverse(); 36 | for (let layer of layers) { 37 | console.log(layer.map(key => String(key)).join(',')); 38 | } 39 | } 40 | 41 | function logLayerEntries(skipList) { 42 | let layers = getLayerEntries(skipList).reverse(); 43 | for (let layer of layers) { 44 | console.log(layer.map(entry => `${entry[0]}=${entry[1]}`).join(',')); 45 | } 46 | } 47 | 48 | describe('ProperSkipList tests', function () { 49 | let skipList; 50 | let result; 51 | 52 | describe('#upsert', function () { 53 | beforeEach(async function () { 54 | skipList = new ProperSkipList(); 55 | }); 56 | 57 | it('should insert numeric keys in sorted order inside the skip list', async function () { 58 | for (let i = 0; i < 100; i++) { 59 | skipList.upsert(i, `value${i}`); 60 | } 61 | let currentNode = skipList.head.nodes[0].next; 62 | while (currentNode && currentNode.next) { 63 | assert(currentNode.group.value === `value${currentNode.group.key}`); 64 | assert(currentNode.group.key > currentNode.prev.group.key || currentNode.prev.group.key === undefined); 65 | currentNode = currentNode.next; 66 | } 67 | }); 68 | 69 | it('should allow an existing numeric key to be replaced', async function () { 70 | for (let i = 0; i < 100; i++) { 71 | skipList.upsert(i, `value${i}`); 72 | } 73 | result = skipList.find(11); 74 | assert(result, 'value11'); 75 | 76 | skipList.upsert(11, 'updated'); 77 | 78 | result = skipList.find(11); 79 | assert(result, 'updated'); 80 | }); 81 | 82 | it('should have multiple layers of decreasing size', async function () { 83 | for (let i = 0; i < 1000; i++) { 84 | skipList.upsert(i, `value${i}`); 85 | } 86 | let layers = getLayerKeys(skipList); 87 | 88 | let len = layers.length; 89 | for (let i = 1; i < len; i++) { 90 | assert(layers[i].length <= layers[i - 1].length); 91 | } 92 | }); 93 | 94 | it('should support inserting and updating values with strings as keys', async function () { 95 | for (let i = 0; i < 100; i++) { 96 | skipList.upsert(`key${i}`, `value${i}`); 97 | } 98 | result = skipList.find('key88'); 99 | assert(result, 'value88'); 100 | 101 | skipList.upsert('key88', 'updated'); 102 | 103 | result = skipList.find('key88'); 104 | assert(result, 'updated'); 105 | }); 106 | 107 | it('should store string keys based on lexicographical order', async function () { 108 | for (let i = 4; i >= 0; i--) { 109 | skipList.upsert(`key${i}`, `value${i}`); 110 | } 111 | let currentNode = skipList.head.nodes[0].next; 112 | assert(currentNode.group.key === 'key0'); 113 | assert(currentNode.group.value === 'value0'); 114 | currentNode = currentNode.next; 115 | assert(currentNode.group.key === 'key1'); 116 | assert(currentNode.group.value === 'value1'); 117 | currentNode = currentNode.next; 118 | assert(currentNode.group.key === 'key2'); 119 | assert(currentNode.group.value === 'value2'); 120 | currentNode = currentNode.next; 121 | assert(currentNode.group.key === 'key3'); 122 | assert(currentNode.group.value === 'value3'); 123 | currentNode = currentNode.next; 124 | assert(currentNode.group.key === 'key4'); 125 | assert(currentNode.group.value === 'value4'); 126 | }); 127 | 128 | it('should support mixing string and numeric keys', async function () { 129 | for (let i = 2; i >= 0; i--) { 130 | skipList.upsert(`key${i}`, `string${i}`); 131 | } 132 | for (let i = 2; i >= 0; i--) { 133 | skipList.upsert(i, `number${i}`); 134 | } 135 | let currentNode = skipList.head.nodes[0].next; 136 | assert(currentNode.group.key === 0); 137 | assert(currentNode.group.value === 'number0'); 138 | currentNode = currentNode.next; 139 | assert(currentNode.group.key === 1); 140 | assert(currentNode.group.value === 'number1'); 141 | currentNode = currentNode.next; 142 | assert(currentNode.group.key === 2); 143 | assert(currentNode.group.value === 'number2'); 144 | currentNode = currentNode.next; 145 | assert(currentNode.group.key === 'key0'); 146 | assert(currentNode.group.value === 'string0'); 147 | currentNode = currentNode.next; 148 | assert(currentNode.group.key === 'key1'); 149 | assert(currentNode.group.value === 'string1'); 150 | currentNode = currentNode.next; 151 | assert(currentNode.group.key === 'key2'); 152 | assert(currentNode.group.value === 'string2'); 153 | }); 154 | 155 | it('should support using null and undefined as keys', async function () { 156 | skipList.upsert(null, 'value1'); 157 | skipList.upsert(undefined, 'value2'); 158 | let layers = getLayerEntries(skipList); 159 | let bottomLayer = layers[0]; 160 | assert(bottomLayer[1][0] === undefined); 161 | assert(bottomLayer[2][0] === null); 162 | }); 163 | 164 | it('should support mixing null, undefined, strings and numbers as keys', async function () { 165 | skipList.upsert(undefined, '[undefined]'); 166 | skipList.upsert(null, '[null]'); 167 | skipList.upsert(3, 'number3'); 168 | skipList.upsert(10, 'number10'); 169 | skipList.upsert('3', 'string3'); 170 | skipList.upsert('4', 'string4'); 171 | skipList.upsert('hello', 'stringhello'); 172 | skipList.upsert('test', 'stringtest'); 173 | 174 | let layers = getLayerEntries(skipList); 175 | let bottomLayer = layers[0]; 176 | assert(bottomLayer[1][0] === undefined); 177 | assert(bottomLayer[2][0] === null); 178 | assert(bottomLayer[3][0] === 3); 179 | assert(bottomLayer[4][0] === 10); 180 | assert(bottomLayer[5][0] === '3'); 181 | assert(bottomLayer[6][0] === '4'); 182 | assert(bottomLayer[7][0] === 'hello'); 183 | assert(bottomLayer[8][0] === 'test'); 184 | }); 185 | 186 | it('should support mixing Number and BigInt as keys in sorted order', async function () { 187 | skipList.upsert(0, `value0`); 188 | skipList.upsert(1, `value1`); 189 | skipList.upsert(2, `value2`); 190 | skipList.upsert(3n, `value3`); 191 | skipList.upsert(4, `value4`); 192 | skipList.upsert(5n, `value5`); 193 | skipList.upsert(6n, `value6`); 194 | skipList.upsert(7, `value7`); 195 | let currentNode = skipList.head.nodes[0].next; 196 | while (currentNode && currentNode.next) { 197 | assert(currentNode.group.value === `value${currentNode.group.key.toString()}`); 198 | assert(currentNode.prev.group.key === undefined || Number(currentNode.group.key) > Number(currentNode.prev.group.key)); 199 | currentNode = currentNode.next; 200 | } 201 | }); 202 | }); 203 | 204 | describe('#find', function () { 205 | describe('when skip list contains many values', function () { 206 | beforeEach(async function () { 207 | skipList = new ProperSkipList(); 208 | for (let i = 0; i < 1000; i++) { 209 | skipList.upsert(i, `value${i}`); 210 | } 211 | skipList.upsert('hello', 'world'); 212 | skipList.upsert('foo', 'bar'); 213 | }); 214 | 215 | it('should be able to find an entry which was previously inserted', async function () { 216 | result = skipList.find(900); 217 | assert(result === 'value900'); 218 | result = skipList.find('foo'); 219 | assert(result === 'bar'); 220 | }); 221 | 222 | it('should return undefined if value is not found', async function () { 223 | result = skipList.find(1111); 224 | assert(result === undefined); 225 | }); 226 | }); 227 | 228 | describe('when skip list is empty', function () { 229 | beforeEach(async function () { 230 | skipList = new ProperSkipList(); 231 | }); 232 | 233 | it('should return undefined', async function () { 234 | result = skipList.find(900); 235 | assert(result === undefined); 236 | }); 237 | }); 238 | }); 239 | 240 | describe('#delete', function () { 241 | beforeEach(async function () { 242 | skipList = new ProperSkipList(); 243 | for (let i = 0; i < 20; i++) { 244 | skipList.upsert(i, `value${i}`); 245 | } 246 | }); 247 | 248 | it('should delete an entry from all layers', async function () { 249 | skipList.delete(10); 250 | let layers = getLayerKeys(skipList); 251 | for (let layer of layers) { 252 | for (let key of layer) { 253 | assert(key != 10); 254 | } 255 | } 256 | }); 257 | 258 | it('should be able to delete null key entries', async function () { 259 | skipList.upsert(null, 'value'); 260 | skipList.upsert(undefined, 'valueundefined'); 261 | skipList.delete(null); 262 | let layers = getLayerEntries(skipList); 263 | for (let layer of layers) { 264 | for (let [key, value] of layer) { 265 | assert(key !== null); 266 | assert(value !== 'value'); 267 | } 268 | } 269 | }); 270 | 271 | it('should continue to function after trying to delete an undefined key', async function () { 272 | skipList.delete(undefined); 273 | result = skipList.find(12); 274 | assert(result === 'value12'); 275 | skipList.upsert(1000, 'testing'); 276 | result = skipList.find(1000); 277 | assert(result === 'testing'); 278 | }); 279 | }); 280 | 281 | describe('#extract', function () { 282 | beforeEach(async function () { 283 | skipList = new ProperSkipList(); 284 | for (let i = 0; i < 20; i++) { 285 | skipList.upsert(i, `value${i}`); 286 | } 287 | }); 288 | 289 | it('should delete an entry from all layers and return the previous value', async function () { 290 | let oldValue = skipList.extract(11); 291 | assert(oldValue === 'value11'); 292 | 293 | let layers = getLayerKeys(skipList); 294 | for (let layer of layers) { 295 | for (let key of layer) { 296 | assert(key != 11); 297 | } 298 | } 299 | }); 300 | }); 301 | 302 | describe('#min', function () { 303 | describe('when skip list contains many values', function () { 304 | beforeEach(async function () { 305 | skipList = new ProperSkipList(); 306 | for (let i = 999; i >= 3; i--) { 307 | skipList.upsert(i, `value${i}`); 308 | } 309 | }); 310 | 311 | it('should be able to get the lowest key', async function () { 312 | result = skipList.minKey(); 313 | assert(result === 3); 314 | }); 315 | 316 | it('should be able to get the value at the lowest key', async function () { 317 | result = skipList.minValue(); 318 | assert(result === 'value3'); 319 | }); 320 | 321 | it('should be able to get the entry at the lowest key', async function () { 322 | let entry = skipList.minEntry(); 323 | assert(entry.length === 2); 324 | assert(entry[0] === 3); 325 | assert(entry[1] === 'value3'); 326 | }); 327 | }); 328 | 329 | describe('when skip list is empty', function () { 330 | beforeEach(async function () { 331 | skipList = new ProperSkipList(); 332 | }); 333 | 334 | it('should return [undefined, undefined] when minEntry is called', async function () { 335 | result = skipList.minEntry(); 336 | assert(result.length === 2); 337 | assert(result[0] === undefined); 338 | assert(result[1] === undefined); 339 | }); 340 | 341 | it('should return undefined when minKey is called', async function () { 342 | result = skipList.minKey(); 343 | assert(result === undefined); 344 | }); 345 | 346 | it('should return undefined when minValue is called', async function () { 347 | result = skipList.minValue(); 348 | assert(result === undefined); 349 | }); 350 | }); 351 | }); 352 | 353 | describe('#max', function () { 354 | describe('when skip list contains many values', function () { 355 | beforeEach(async function () { 356 | skipList = new ProperSkipList(); 357 | for (let i = 999; i >= 3; i--) { 358 | skipList.upsert(i, `value${i}`); 359 | } 360 | }); 361 | 362 | it('should be able to get the highest key', async function () { 363 | result = skipList.maxKey(); 364 | assert(result === 999); 365 | }); 366 | 367 | it('should be able to get the value at the highest key', async function () { 368 | result = skipList.maxValue(); 369 | assert(result === 'value999'); 370 | }); 371 | }); 372 | 373 | describe('when skip list is empty', function () { 374 | beforeEach(async function () { 375 | skipList = new ProperSkipList(); 376 | }); 377 | 378 | it('should return [undefined, undefined] when maxEntry is called', async function () { 379 | result = skipList.maxEntry(); 380 | assert(result.length === 2); 381 | assert(result[0] === undefined); 382 | assert(result[1] === undefined); 383 | }); 384 | 385 | it('should return undefined when maxKey is called', async function () { 386 | result = skipList.maxKey(); 387 | assert(result === undefined); 388 | }); 389 | 390 | it('should return undefined when maxValue is called', async function () { 391 | result = skipList.maxValue(); 392 | assert(result === undefined); 393 | }); 394 | }); 395 | }); 396 | 397 | describe('#findEntries', function () { 398 | describe('when all entries are adjacent', function () { 399 | beforeEach(async function () { 400 | skipList = new ProperSkipList(); 401 | for (let i = 7; i < 107; i++) { 402 | skipList.upsert(i, `value${i}`); 403 | } 404 | }); 405 | 406 | it('should be able to iterate over entries in ascending order starting from the specified key', async function () { 407 | result = skipList.findEntries(37); 408 | assert(result.matchingValue === 'value37'); 409 | let iterable = result.asc; 410 | let lastKey = -Infinity; 411 | for (let [key, value, i] of iterable) { 412 | if (i === 0) { 413 | assert(key === 37); 414 | } 415 | assert(key > lastKey); 416 | lastKey = key; 417 | } 418 | result = iterable.next(); 419 | result = iterable.next(); 420 | assert(JSON.stringify(result.value) === JSON.stringify([undefined, undefined, 70])); 421 | }); 422 | 423 | it('should be able to iterate over entries in descending order starting from the specified key', async function () { 424 | result = skipList.findEntries(88); 425 | assert(result.matchingValue === 'value88'); 426 | let iterable = result.desc; 427 | let lastKey = Infinity; 428 | let lastIndex; 429 | for (let [key, value, i] of iterable) { 430 | lastIndex = i; 431 | if (i === 0) { 432 | assert(key === 88); 433 | } 434 | assert(key < lastKey); 435 | lastKey = key; 436 | } 437 | assert(lastIndex === 81); 438 | result = iterable.next(); 439 | assert(JSON.stringify(result.value) === JSON.stringify([undefined, undefined, 82])); 440 | result = iterable.next(); 441 | assert(JSON.stringify(result.value) === JSON.stringify([undefined, undefined, 82])); 442 | }); 443 | }); 444 | 445 | describe('when there are gaps between entries', function () { 446 | beforeEach(async function () { 447 | skipList = new ProperSkipList(); 448 | for (let i = 10; i < 1000; i += 10) { 449 | skipList.upsert(i, `value${i}`); 450 | } 451 | }); 452 | 453 | it('should be able to iterate over nearby entries in ascending order even if the exact matching key cannot be found', async function () { 454 | result = skipList.findEntries(19); 455 | assert(result.matchingValue === undefined); 456 | let iterable = result.asc; 457 | let lastKey = -Infinity; 458 | for (let [key, value, i] of iterable) { 459 | if (i === 0) { 460 | assert(key === 20); 461 | } 462 | assert(key > lastKey); 463 | assert(key % 10 === 0); 464 | lastKey = key; 465 | } 466 | }); 467 | 468 | it('should be able to iterate over nearby entries in descending order even if the exact matching key cannot be found', async function () { 469 | result = skipList.findEntries(89); 470 | assert(result.matchingValue === undefined); 471 | let iterable = result.desc; 472 | let lastKey = Infinity; 473 | for (let [key, value, i] of iterable) { 474 | if (i === 0) { 475 | assert(key === 80); 476 | } 477 | assert(key < lastKey); 478 | assert(key % 10 === 0); 479 | lastKey = key; 480 | } 481 | }); 482 | }); 483 | 484 | describe('when strings are used as keys', function () { 485 | beforeEach(async function () { 486 | skipList = new ProperSkipList(); 487 | for (let i = 10; i < 1000; i += 10) { 488 | skipList.upsert(`key${i}`, `value${i}`); 489 | } 490 | }); 491 | 492 | it('should be able to iterate over nearby entries in ascending order even if the exact matching key cannot be found', async function () { 493 | result = skipList.findEntries('key15'); 494 | assert(result.matchingValue === undefined); 495 | let iterable = result.asc; 496 | let lastKey = ''; 497 | for (let [key, value, i] of iterable) { 498 | if (i === 0) { 499 | assert(key === 'key150'); 500 | } 501 | assert(key > lastKey); 502 | lastKey = key; 503 | } 504 | }); 505 | 506 | it('should be able to iterate over nearby entries in descending order even if the exact matching key cannot be found', async function () { 507 | result = skipList.findEntries('key89'); 508 | assert(result.matchingValue === undefined); 509 | let iterable = result.desc; 510 | let lastKey = 'z'; 511 | for (let [key, value, i] of iterable) { 512 | if (i === 0) { 513 | assert(key === 'key880'); 514 | } 515 | assert(key < lastKey); 516 | lastKey = key; 517 | } 518 | }); 519 | }); 520 | }); 521 | 522 | describe('#findEntriesFromMin', function () { 523 | beforeEach(async function () { 524 | skipList = new ProperSkipList(); 525 | for (let i = 4; i < 100; i++) { 526 | skipList.upsert(i, `value${i}`); 527 | } 528 | }); 529 | 530 | it('should be able to iterate over entries starting from the minimum key', async function () { 531 | let iterable = skipList.findEntriesFromMin(); 532 | let lastKey = -Infinity; 533 | for (let [key, value, i] of iterable) { 534 | if (i === 0) { 535 | assert(key === 4); 536 | } 537 | assert(key > lastKey); 538 | lastKey = key; 539 | } 540 | result = iterable.next(); 541 | assert(JSON.stringify(result.value) === JSON.stringify([undefined, undefined, 96])); 542 | result = iterable.next(); 543 | assert(JSON.stringify(result.value) === JSON.stringify([undefined, undefined, 96])); 544 | }); 545 | }); 546 | 547 | describe('#findEntriesFromMax', function () { 548 | beforeEach(async function () { 549 | skipList = new ProperSkipList(); 550 | for (let i = 4; i < 100; i++) { 551 | skipList.upsert(i, `value${i}`); 552 | } 553 | }); 554 | 555 | it('should be able to iterate over entries backwards starting from the maximum key', async function () { 556 | let iterable = skipList.findEntriesFromMax(); 557 | let lastKey = Infinity; 558 | for (let [key, value, i] of iterable) { 559 | if (i === 0) { 560 | assert(key === 99); 561 | } 562 | assert(key < lastKey); 563 | lastKey = key; 564 | } 565 | result = iterable.next(); 566 | assert(JSON.stringify(result.value) === JSON.stringify([undefined, undefined, 96])); 567 | result = iterable.next(); 568 | assert(JSON.stringify(result.value) === JSON.stringify([undefined, undefined, 96])); 569 | }); 570 | }); 571 | 572 | describe('#deleteRange', function () { 573 | let keyLookup; 574 | 575 | describe('when numeric keys are used', function () { 576 | beforeEach(async function () { 577 | skipList = new ProperSkipList(); 578 | keyLookup = {}; 579 | for (let i = 0; i < 50; i++) { 580 | skipList.upsert(i, `value${i}`); 581 | keyLookup[i] = true; 582 | } 583 | }); 584 | 585 | it('should be able to remove an entire range of entries in a single operation but keep both the left and right bounds', async function () { 586 | skipList.deleteRange(10, 20); 587 | 588 | let layers = getLayerKeys(skipList); 589 | for (let layer of layers) { 590 | for (let key of layer) { 591 | assert(key <= 10 || key >= 20 || key === undefined); 592 | } 593 | } 594 | 595 | Object.keys(keyLookup).forEach((key) => { 596 | key = Number(key); 597 | if (key <= 10 || key >= 20) { 598 | assert(skipList.has(key)); 599 | } 600 | }); 601 | }); 602 | 603 | it('should delete range to the end if the second argument is missing or null', async function () { 604 | skipList.deleteRange(10); 605 | 606 | let layers = getLayerKeys(skipList); 607 | for (let layer of layers) { 608 | for (let key of layer) { 609 | assert(key <= 10 || key === undefined); 610 | } 611 | } 612 | 613 | Object.keys(keyLookup).forEach((key) => { 614 | key = Number(key); 615 | if (key <= 10) { 616 | assert(skipList.has(key)); 617 | } 618 | }); 619 | }); 620 | 621 | it('should delete range from the beginning if the first argument is null', async function () { 622 | skipList.deleteRange(null, 10); 623 | 624 | let layers = getLayerKeys(skipList); 625 | for (let layer of layers) { 626 | for (let key of layer) { 627 | assert(key >= 10 || key === undefined); 628 | } 629 | } 630 | 631 | Object.keys(keyLookup).forEach((key) => { 632 | key = Number(key); 633 | if (key >= 10) { 634 | assert(skipList.has(key)); 635 | } 636 | }); 637 | }); 638 | 639 | it('should delete everything if all arguments are undefined', async function () { 640 | skipList.deleteRange(); 641 | 642 | let layers = getLayerKeys(skipList); 643 | for (let layer of layers) { 644 | for (let key of layer) { 645 | assert(key === undefined); 646 | } 647 | } 648 | }); 649 | 650 | it('should delete nothing if range finds a single item and deleteLeft is not true', async function () { 651 | skipList.deleteRange(47, 47); 652 | assert(skipList.find(47) === 'value47'); 653 | }); 654 | 655 | it('should delete item if range finds a single item and deleteLeft is true', async function () { 656 | skipList.deleteRange(47, 47, true); 657 | assert(skipList.find(47) === undefined); 658 | }); 659 | 660 | it('should delete nothing if left bound is greater than right bound', async function () { 661 | skipList.deleteRange(1, 0); 662 | assert(skipList.find(0) === 'value0'); 663 | assert(skipList.find(1) === 'value1'); 664 | }); 665 | 666 | it('should delete nothing if left bound is greater than right bound even if deleteLeft is true and deleteRight is true', async function () { 667 | skipList.deleteRange(1, 0, true, true); 668 | assert(skipList.find(0) === 'value0'); 669 | assert(skipList.find(1) === 'value1'); 670 | }); 671 | 672 | it('should be able to remove an entire range of entries in a single operation but keep both the left and right bounds', async function () { 673 | skipList.deleteRange(10, 20); 674 | 675 | let layers = getLayerKeys(skipList); 676 | for (let layer of layers) { 677 | for (let key of layer) { 678 | assert(key <= 10 || key >= 20 || key === undefined); 679 | } 680 | } 681 | 682 | Object.keys(keyLookup).forEach((key) => { 683 | key = Number(key); 684 | if (key <= 10 || key >= 20) { 685 | assert(skipList.has(key)); 686 | } 687 | }); 688 | }); 689 | 690 | it('should be able to remove an entire range of entries in a single operation including the left bound', async function () { 691 | skipList.deleteRange(10, 20, true); 692 | 693 | let layers = getLayerKeys(skipList); 694 | for (let layer of layers) { 695 | for (let key of layer) { 696 | assert(key < 10 || key >= 20 || key === undefined); 697 | } 698 | } 699 | 700 | Object.keys(keyLookup).forEach((key) => { 701 | key = Number(key); 702 | if (key < 10 || key >= 20) { 703 | assert(skipList.has(key)); 704 | } 705 | }); 706 | }); 707 | 708 | it('should be able to remove an entire range of entries in a single operation including the right bound', async function () { 709 | skipList.deleteRange(10, 20, false, true); 710 | 711 | let layers = getLayerKeys(skipList); 712 | for (let layer of layers) { 713 | for (let key of layer) { 714 | assert(key <= 10 || key > 20 || key === undefined); 715 | } 716 | } 717 | 718 | Object.keys(keyLookup).forEach((key) => { 719 | key = Number(key); 720 | if (key <= 10 || key > 20) { 721 | assert(skipList.has(key)); 722 | } 723 | }); 724 | }); 725 | 726 | it('should be able to remove an entire range of entries in a single operation including both the left and right bounds', async function () { 727 | skipList.deleteRange(10, 20, true, true); 728 | 729 | let layers = getLayerKeys(skipList); 730 | for (let layer of layers) { 731 | for (let key of layer) { 732 | assert(key < 10 || key > 20 || key === undefined); 733 | } 734 | } 735 | 736 | Object.keys(keyLookup).forEach((key) => { 737 | key = Number(key); 738 | if (key < 10 || key > 20) { 739 | assert(skipList.has(key)); 740 | } 741 | }); 742 | }); 743 | 744 | it('should be able to delete a single entry in the middle of the list', async function () { 745 | skipList.deleteRange(10, 10, true, true); 746 | 747 | let entries = [...skipList.findEntriesFromMin()]; 748 | assert(entries.length === 49); 749 | }); 750 | 751 | it('should be able to remove an entire range of entries in a single operation even if there are no exact matches for the left and right bounds', async function () { 752 | skipList.deleteRange(10.5, 19.5); 753 | 754 | let layers = getLayerKeys(skipList); 755 | for (let layer of layers) { 756 | for (let key of layer) { 757 | assert(key <= 10 || key >= 20 || key === undefined); 758 | } 759 | } 760 | 761 | Object.keys(keyLookup).forEach((key) => { 762 | key = Number(key); 763 | if (key <= 10 || key >= 20) { 764 | assert(skipList.has(key)); 765 | } 766 | }); 767 | }); 768 | }); 769 | 770 | describe('when string keys are used', function () { 771 | beforeEach(async function () { 772 | skipList = new ProperSkipList(); 773 | keyLookup = {}; 774 | for (let i = 0; i < 50; i++) { 775 | let key = `key${i}`; 776 | skipList.upsert(key, `value${i}`); 777 | keyLookup[key] = true; 778 | } 779 | }); 780 | 781 | it('should be able to remove an entire range of entries in a single operation but keep both the left and right bounds', async function () { 782 | skipList.deleteRange('key10', 'key20'); 783 | 784 | let layers = getLayerKeys(skipList); 785 | for (let layer of layers) { 786 | for (let key of layer) { 787 | assert(key <= 'key10' || key >= 'key20' || key === undefined); 788 | } 789 | } 790 | 791 | Object.keys(keyLookup).forEach((key) => { 792 | if (key <= 'key10' || key >= 'key20') { 793 | assert(skipList.has(key)); 794 | } 795 | }); 796 | }); 797 | 798 | it('should be able to remove an entire range of entries in a single operation including both the left and right bounds', async function () { 799 | skipList.deleteRange('key10', 'key20', true, true); 800 | 801 | let layers = getLayerKeys(skipList); 802 | for (let layer of layers) { 803 | for (let key of layer) { 804 | assert(key < 'key10' || key > 'key20' || key === undefined); 805 | } 806 | } 807 | 808 | Object.keys(keyLookup).forEach((key) => { 809 | if (key < 'key10' || key > 'key20') { 810 | assert(skipList.has(key)); 811 | } 812 | }); 813 | }); 814 | 815 | it('should be able to remove an entire range of entries in a single operation even if there are no exact matches for the left and right bounds', async function () { 816 | // Insert elements which are lexicographically between (key10 and key11) and between (key19 and key20). 817 | skipList.deleteRange('key10a', 'key19a'); 818 | 819 | let layers = getLayerKeys(skipList); 820 | for (let layer of layers) { 821 | for (let key of layer) { 822 | assert(key <= 'key10' || key >= 'key2' || key === undefined); 823 | } 824 | } 825 | 826 | Object.keys(keyLookup).forEach((key) => { 827 | if (key <= 'key10' || key >= 'key2') { 828 | assert(skipList.has(key)); 829 | } 830 | }); 831 | }); 832 | }); 833 | 834 | describe('when numeric and string keys are used together', function () { 835 | let numberKeyLookup; 836 | let stringKeyLookup; 837 | beforeEach(async function () { 838 | skipList = new ProperSkipList(); 839 | numberKeyLookup = {}; 840 | stringKeyLookup = {}; 841 | for (let i = 0; i < 50; i++) { 842 | skipList.upsert(i, `value${i}`); 843 | numberKeyLookup[i] = true; 844 | } 845 | for (let i = 0; i < 50; i++) { 846 | let key = `key${i}`; 847 | skipList.upsert(key, `value${i}`); 848 | stringKeyLookup[key] = true; 849 | } 850 | }); 851 | 852 | it('should be able to delete a range across type boundaries', async function () { 853 | // Insert elements which are lexicographically between (key10 and key11) and between (key19 and key20). 854 | skipList.deleteRange(10, 'key40'); 855 | 856 | let layers = getLayerKeys(skipList); 857 | for (let layer of layers) { 858 | for (let key of layer) { 859 | let isString = isNaN(key); 860 | if (isString) { 861 | assert(key >= 'key40' || key === undefined); 862 | } else { 863 | assert(key <= 10 || key === undefined); 864 | } 865 | } 866 | } 867 | 868 | Object.keys(numberKeyLookup).forEach((key) => { 869 | key = Number(key); 870 | if (key <= 10) { 871 | assert(skipList.has(key)); 872 | } 873 | }); 874 | 875 | Object.keys(stringKeyLookup).forEach((key) => { 876 | if (key >= 'key40') { 877 | assert(skipList.has(key)); 878 | } 879 | }); 880 | }); 881 | }); 882 | }); 883 | 884 | describe('#clear', function () { 885 | beforeEach(async function () { 886 | skipList = new ProperSkipList(); 887 | for (let i = 0; i < 50; i++) { 888 | skipList.upsert(i, `value${i}`); 889 | } 890 | }); 891 | 892 | it('should continue working after clear is called', async function () { 893 | skipList.clear(); 894 | for (let i = 0; i < 50; i++) { 895 | skipList.upsert(i, `value${i}`); 896 | } 897 | result = skipList.find(10); 898 | assert(result === 'value10'); 899 | }); 900 | }); 901 | 902 | describe('#length', function () { 903 | describe('when default settings are used', async function () { 904 | beforeEach(async function () { 905 | skipList = new ProperSkipList(); 906 | for (let i = 0; i < 50; i++) { 907 | skipList.upsert(i, `value${i}`); 908 | } 909 | }); 910 | 911 | it('should show the correct number of entries after elements are inserted', async function () { 912 | assert(skipList.length === 50); 913 | skipList.upsert('hello', 1); 914 | assert(skipList.length === 51); 915 | skipList.upsert('foo', 'bar'); 916 | assert(skipList.length === 52); 917 | skipList.upsert(null, 'bar'); 918 | assert(skipList.length === 53); 919 | skipList.upsert('foo', 'two'); 920 | assert(skipList.length === 53); 921 | skipList.upsert(null, 'test'); 922 | assert(skipList.length === 53); 923 | }); 924 | 925 | it('should show the correct number of entries after elements are deleted', async function () { 926 | skipList.delete(10); 927 | assert(skipList.length === 49); 928 | skipList.delete(100); 929 | assert(skipList.length === 49); 930 | skipList.delete(undefined); 931 | assert(skipList.length === 49); 932 | skipList.delete(null); 933 | assert(skipList.length === 49); 934 | }); 935 | 936 | it('should show the correct number of entries after ranges are deleted', async function () { 937 | skipList.deleteRange(10, 20); 938 | assert(skipList.length === 41); 939 | skipList.deleteRange(40, 50); 940 | assert(skipList.length === 32); 941 | skipList.deleteRange(); 942 | assert(skipList.length === 0); 943 | }); 944 | 945 | it('should reset length to 0 after clear is invoked', async function () { 946 | skipList.clear(); 947 | assert(skipList.length === 0); 948 | }); 949 | }); 950 | 951 | describe('when updateLength is false', async function () { 952 | beforeEach(async function () { 953 | skipList = new ProperSkipList({ 954 | updateLength: false 955 | }); 956 | for (let i = 0; i < 50; i++) { 957 | skipList.upsert(i, `value${i}`); 958 | } 959 | }); 960 | 961 | it('should show the length as undefined', async function () { 962 | assert(skipList.length === undefined); 963 | }); 964 | 965 | it('should stay as undefined when different methods are called', async function () { 966 | skipList.deleteRange(10, 20); 967 | assert(skipList.length === undefined); 968 | skipList.delete(2); 969 | assert(skipList.length === undefined); 970 | skipList.clear(); 971 | assert(skipList.length === undefined); 972 | }); 973 | }); 974 | }); 975 | }); 976 | --------------------------------------------------------------------------------