577 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			577 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| //
 | |
| //                              list
 | |
| //                            ┌──────┐
 | |
| //             ┌──────────────┼─head │
 | |
| //             │              │ tail─┼──────────────┐
 | |
| //             │              └──────┘              │
 | |
| //             ▼                                    ▼
 | |
| //            item        item        item        item
 | |
| //          ┌──────┐    ┌──────┐    ┌──────┐    ┌──────┐
 | |
| //  null ◀──┼─prev │◀───┼─prev │◀───┼─prev │◀───┼─prev │
 | |
| //          │ next─┼───▶│ next─┼───▶│ next─┼───▶│ next─┼──▶ null
 | |
| //          ├──────┤    ├──────┤    ├──────┤    ├──────┤
 | |
| //          │ data │    │ data │    │ data │    │ data │
 | |
| //          └──────┘    └──────┘    └──────┘    └──────┘
 | |
| //
 | |
| 
 | |
| function createItem(data) {
 | |
|     return {
 | |
|         prev: null,
 | |
|         next: null,
 | |
|         data: data
 | |
|     };
 | |
| }
 | |
| 
 | |
| function allocateCursor(node, prev, next) {
 | |
|     var cursor;
 | |
| 
 | |
|     if (cursors !== null) {
 | |
|         cursor = cursors;
 | |
|         cursors = cursors.cursor;
 | |
|         cursor.prev = prev;
 | |
|         cursor.next = next;
 | |
|         cursor.cursor = node.cursor;
 | |
|     } else {
 | |
|         cursor = {
 | |
|             prev: prev,
 | |
|             next: next,
 | |
|             cursor: node.cursor
 | |
|         };
 | |
|     }
 | |
| 
 | |
|     node.cursor = cursor;
 | |
| 
 | |
|     return cursor;
 | |
| }
 | |
| 
 | |
| function releaseCursor(node) {
 | |
|     var cursor = node.cursor;
 | |
| 
 | |
|     node.cursor = cursor.cursor;
 | |
|     cursor.prev = null;
 | |
|     cursor.next = null;
 | |
|     cursor.cursor = cursors;
 | |
|     cursors = cursor;
 | |
| }
 | |
| 
 | |
| var cursors = null;
 | |
| var List = function() {
 | |
|     this.cursor = null;
 | |
|     this.head = null;
 | |
|     this.tail = null;
 | |
| };
 | |
| 
 | |
| List.createItem = createItem;
 | |
| List.prototype.createItem = createItem;
 | |
| 
 | |
| List.prototype.updateCursors = function(prevOld, prevNew, nextOld, nextNew) {
 | |
|     var cursor = this.cursor;
 | |
| 
 | |
|     while (cursor !== null) {
 | |
|         if (cursor.prev === prevOld) {
 | |
|             cursor.prev = prevNew;
 | |
|         }
 | |
| 
 | |
|         if (cursor.next === nextOld) {
 | |
|             cursor.next = nextNew;
 | |
|         }
 | |
| 
 | |
|         cursor = cursor.cursor;
 | |
|     }
 | |
| };
 | |
| 
 | |
| List.prototype.getSize = function() {
 | |
|     var size = 0;
 | |
|     var cursor = this.head;
 | |
| 
 | |
|     while (cursor) {
 | |
|         size++;
 | |
|         cursor = cursor.next;
 | |
|     }
 | |
| 
 | |
|     return size;
 | |
| };
 | |
| 
 | |
| List.prototype.fromArray = function(array) {
 | |
|     var cursor = null;
 | |
| 
 | |
|     this.head = null;
 | |
| 
 | |
|     for (var i = 0; i < array.length; i++) {
 | |
|         var item = createItem(array[i]);
 | |
| 
 | |
|         if (cursor !== null) {
 | |
|             cursor.next = item;
 | |
|         } else {
 | |
|             this.head = item;
 | |
|         }
 | |
| 
 | |
|         item.prev = cursor;
 | |
|         cursor = item;
 | |
|     }
 | |
| 
 | |
|     this.tail = cursor;
 | |
| 
 | |
|     return this;
 | |
| };
 | |
| 
 | |
| List.prototype.toArray = function() {
 | |
|     var cursor = this.head;
 | |
|     var result = [];
 | |
| 
 | |
|     while (cursor) {
 | |
|         result.push(cursor.data);
 | |
|         cursor = cursor.next;
 | |
|     }
 | |
| 
 | |
|     return result;
 | |
| };
 | |
| 
 | |
| List.prototype.toJSON = List.prototype.toArray;
 | |
| 
 | |
| List.prototype.isEmpty = function() {
 | |
|     return this.head === null;
 | |
| };
 | |
| 
 | |
| List.prototype.first = function() {
 | |
|     return this.head && this.head.data;
 | |
| };
 | |
| 
 | |
| List.prototype.last = function() {
 | |
|     return this.tail && this.tail.data;
 | |
| };
 | |
| 
 | |
| List.prototype.each = function(fn, context) {
 | |
|     var item;
 | |
| 
 | |
|     if (context === undefined) {
 | |
|         context = this;
 | |
|     }
 | |
| 
 | |
|     // push cursor
 | |
|     var cursor = allocateCursor(this, null, this.head);
 | |
| 
 | |
|     while (cursor.next !== null) {
 | |
|         item = cursor.next;
 | |
|         cursor.next = item.next;
 | |
| 
 | |
|         fn.call(context, item.data, item, this);
 | |
|     }
 | |
| 
 | |
|     // pop cursor
 | |
|     releaseCursor(this);
 | |
| };
 | |
| 
 | |
| List.prototype.forEach = List.prototype.each;
 | |
| 
 | |
| List.prototype.eachRight = function(fn, context) {
 | |
|     var item;
 | |
| 
 | |
|     if (context === undefined) {
 | |
|         context = this;
 | |
|     }
 | |
| 
 | |
|     // push cursor
 | |
|     var cursor = allocateCursor(this, this.tail, null);
 | |
| 
 | |
|     while (cursor.prev !== null) {
 | |
|         item = cursor.prev;
 | |
|         cursor.prev = item.prev;
 | |
| 
 | |
|         fn.call(context, item.data, item, this);
 | |
|     }
 | |
| 
 | |
|     // pop cursor
 | |
|     releaseCursor(this);
 | |
| };
 | |
| 
 | |
| List.prototype.forEachRight = List.prototype.eachRight;
 | |
| 
 | |
| List.prototype.reduce = function(fn, initialValue, context) {
 | |
|     var item;
 | |
| 
 | |
|     if (context === undefined) {
 | |
|         context = this;
 | |
|     }
 | |
| 
 | |
|     // push cursor
 | |
|     var cursor = allocateCursor(this, null, this.head);
 | |
|     var acc = initialValue;
 | |
| 
 | |
|     while (cursor.next !== null) {
 | |
|         item = cursor.next;
 | |
|         cursor.next = item.next;
 | |
| 
 | |
|         acc = fn.call(context, acc, item.data, item, this);
 | |
|     }
 | |
| 
 | |
|     // pop cursor
 | |
|     releaseCursor(this);
 | |
| 
 | |
|     return acc;
 | |
| };
 | |
| 
 | |
| List.prototype.reduceRight = function(fn, initialValue, context) {
 | |
|     var item;
 | |
| 
 | |
|     if (context === undefined) {
 | |
|         context = this;
 | |
|     }
 | |
| 
 | |
|     // push cursor
 | |
|     var cursor = allocateCursor(this, this.tail, null);
 | |
|     var acc = initialValue;
 | |
| 
 | |
|     while (cursor.prev !== null) {
 | |
|         item = cursor.prev;
 | |
|         cursor.prev = item.prev;
 | |
| 
 | |
|         acc = fn.call(context, acc, item.data, item, this);
 | |
|     }
 | |
| 
 | |
|     // pop cursor
 | |
|     releaseCursor(this);
 | |
| 
 | |
|     return acc;
 | |
| };
 | |
| 
 | |
| List.prototype.nextUntil = function(start, fn, context) {
 | |
|     if (start === null) {
 | |
|         return;
 | |
|     }
 | |
| 
 | |
|     var item;
 | |
| 
 | |
|     if (context === undefined) {
 | |
|         context = this;
 | |
|     }
 | |
| 
 | |
|     // push cursor
 | |
|     var cursor = allocateCursor(this, null, start);
 | |
| 
 | |
|     while (cursor.next !== null) {
 | |
|         item = cursor.next;
 | |
|         cursor.next = item.next;
 | |
| 
 | |
|         if (fn.call(context, item.data, item, this)) {
 | |
|             break;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     // pop cursor
 | |
|     releaseCursor(this);
 | |
| };
 | |
| 
 | |
| List.prototype.prevUntil = function(start, fn, context) {
 | |
|     if (start === null) {
 | |
|         return;
 | |
|     }
 | |
| 
 | |
|     var item;
 | |
| 
 | |
|     if (context === undefined) {
 | |
|         context = this;
 | |
|     }
 | |
| 
 | |
|     // push cursor
 | |
|     var cursor = allocateCursor(this, start, null);
 | |
| 
 | |
|     while (cursor.prev !== null) {
 | |
|         item = cursor.prev;
 | |
|         cursor.prev = item.prev;
 | |
| 
 | |
|         if (fn.call(context, item.data, item, this)) {
 | |
|             break;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     // pop cursor
 | |
|     releaseCursor(this);
 | |
| };
 | |
| 
 | |
| List.prototype.some = function(fn, context) {
 | |
|     var cursor = this.head;
 | |
| 
 | |
|     if (context === undefined) {
 | |
|         context = this;
 | |
|     }
 | |
| 
 | |
|     while (cursor !== null) {
 | |
|         if (fn.call(context, cursor.data, cursor, this)) {
 | |
|             return true;
 | |
|         }
 | |
| 
 | |
|         cursor = cursor.next;
 | |
|     }
 | |
| 
 | |
|     return false;
 | |
| };
 | |
| 
 | |
| List.prototype.map = function(fn, context) {
 | |
|     var result = new List();
 | |
|     var cursor = this.head;
 | |
| 
 | |
|     if (context === undefined) {
 | |
|         context = this;
 | |
|     }
 | |
| 
 | |
|     while (cursor !== null) {
 | |
|         result.appendData(fn.call(context, cursor.data, cursor, this));
 | |
|         cursor = cursor.next;
 | |
|     }
 | |
| 
 | |
|     return result;
 | |
| };
 | |
| 
 | |
| List.prototype.filter = function(fn, context) {
 | |
|     var result = new List();
 | |
|     var cursor = this.head;
 | |
| 
 | |
|     if (context === undefined) {
 | |
|         context = this;
 | |
|     }
 | |
| 
 | |
|     while (cursor !== null) {
 | |
|         if (fn.call(context, cursor.data, cursor, this)) {
 | |
|             result.appendData(cursor.data);
 | |
|         }
 | |
|         cursor = cursor.next;
 | |
|     }
 | |
| 
 | |
|     return result;
 | |
| };
 | |
| 
 | |
| List.prototype.clear = function() {
 | |
|     this.head = null;
 | |
|     this.tail = null;
 | |
| };
 | |
| 
 | |
| List.prototype.copy = function() {
 | |
|     var result = new List();
 | |
|     var cursor = this.head;
 | |
| 
 | |
|     while (cursor !== null) {
 | |
|         result.insert(createItem(cursor.data));
 | |
|         cursor = cursor.next;
 | |
|     }
 | |
| 
 | |
|     return result;
 | |
| };
 | |
| 
 | |
| List.prototype.prepend = function(item) {
 | |
|     //      head
 | |
|     //    ^
 | |
|     // item
 | |
|     this.updateCursors(null, item, this.head, item);
 | |
| 
 | |
|     // insert to the beginning of the list
 | |
|     if (this.head !== null) {
 | |
|         // new item <- first item
 | |
|         this.head.prev = item;
 | |
| 
 | |
|         // new item -> first item
 | |
|         item.next = this.head;
 | |
|     } else {
 | |
|         // if list has no head, then it also has no tail
 | |
|         // in this case tail points to the new item
 | |
|         this.tail = item;
 | |
|     }
 | |
| 
 | |
|     // head always points to new item
 | |
|     this.head = item;
 | |
| 
 | |
|     return this;
 | |
| };
 | |
| 
 | |
| List.prototype.prependData = function(data) {
 | |
|     return this.prepend(createItem(data));
 | |
| };
 | |
| 
 | |
| List.prototype.append = function(item) {
 | |
|     return this.insert(item);
 | |
| };
 | |
| 
 | |
| List.prototype.appendData = function(data) {
 | |
|     return this.insert(createItem(data));
 | |
| };
 | |
| 
 | |
| List.prototype.insert = function(item, before) {
 | |
|     if (before !== undefined && before !== null) {
 | |
|         // prev   before
 | |
|         //      ^
 | |
|         //     item
 | |
|         this.updateCursors(before.prev, item, before, item);
 | |
| 
 | |
|         if (before.prev === null) {
 | |
|             // insert to the beginning of list
 | |
|             if (this.head !== before) {
 | |
|                 throw new Error('before doesn\'t belong to list');
 | |
|             }
 | |
| 
 | |
|             // since head points to before therefore list doesn't empty
 | |
|             // no need to check tail
 | |
|             this.head = item;
 | |
|             before.prev = item;
 | |
|             item.next = before;
 | |
| 
 | |
|             this.updateCursors(null, item);
 | |
|         } else {
 | |
| 
 | |
|             // insert between two items
 | |
|             before.prev.next = item;
 | |
|             item.prev = before.prev;
 | |
| 
 | |
|             before.prev = item;
 | |
|             item.next = before;
 | |
|         }
 | |
|     } else {
 | |
|         // tail
 | |
|         //      ^
 | |
|         //      item
 | |
|         this.updateCursors(this.tail, item, null, item);
 | |
| 
 | |
|         // insert to the ending of the list
 | |
|         if (this.tail !== null) {
 | |
|             // last item -> new item
 | |
|             this.tail.next = item;
 | |
| 
 | |
|             // last item <- new item
 | |
|             item.prev = this.tail;
 | |
|         } else {
 | |
|             // if list has no tail, then it also has no head
 | |
|             // in this case head points to new item
 | |
|             this.head = item;
 | |
|         }
 | |
| 
 | |
|         // tail always points to new item
 | |
|         this.tail = item;
 | |
|     }
 | |
| 
 | |
|     return this;
 | |
| };
 | |
| 
 | |
| List.prototype.insertData = function(data, before) {
 | |
|     return this.insert(createItem(data), before);
 | |
| };
 | |
| 
 | |
| List.prototype.remove = function(item) {
 | |
|     //      item
 | |
|     //       ^
 | |
|     // prev     next
 | |
|     this.updateCursors(item, item.prev, item, item.next);
 | |
| 
 | |
|     if (item.prev !== null) {
 | |
|         item.prev.next = item.next;
 | |
|     } else {
 | |
|         if (this.head !== item) {
 | |
|             throw new Error('item doesn\'t belong to list');
 | |
|         }
 | |
| 
 | |
|         this.head = item.next;
 | |
|     }
 | |
| 
 | |
|     if (item.next !== null) {
 | |
|         item.next.prev = item.prev;
 | |
|     } else {
 | |
|         if (this.tail !== item) {
 | |
|             throw new Error('item doesn\'t belong to list');
 | |
|         }
 | |
| 
 | |
|         this.tail = item.prev;
 | |
|     }
 | |
| 
 | |
|     item.prev = null;
 | |
|     item.next = null;
 | |
| 
 | |
|     return item;
 | |
| };
 | |
| 
 | |
| List.prototype.push = function(data) {
 | |
|     this.insert(createItem(data));
 | |
| };
 | |
| 
 | |
| List.prototype.pop = function() {
 | |
|     if (this.tail !== null) {
 | |
|         return this.remove(this.tail);
 | |
|     }
 | |
| };
 | |
| 
 | |
| List.prototype.unshift = function(data) {
 | |
|     this.prepend(createItem(data));
 | |
| };
 | |
| 
 | |
| List.prototype.shift = function() {
 | |
|     if (this.head !== null) {
 | |
|         return this.remove(this.head);
 | |
|     }
 | |
| };
 | |
| 
 | |
| List.prototype.prependList = function(list) {
 | |
|     return this.insertList(list, this.head);
 | |
| };
 | |
| 
 | |
| List.prototype.appendList = function(list) {
 | |
|     return this.insertList(list);
 | |
| };
 | |
| 
 | |
| List.prototype.insertList = function(list, before) {
 | |
|     // ignore empty lists
 | |
|     if (list.head === null) {
 | |
|         return this;
 | |
|     }
 | |
| 
 | |
|     if (before !== undefined && before !== null) {
 | |
|         this.updateCursors(before.prev, list.tail, before, list.head);
 | |
| 
 | |
|         // insert in the middle of dist list
 | |
|         if (before.prev !== null) {
 | |
|             // before.prev <-> list.head
 | |
|             before.prev.next = list.head;
 | |
|             list.head.prev = before.prev;
 | |
|         } else {
 | |
|             this.head = list.head;
 | |
|         }
 | |
| 
 | |
|         before.prev = list.tail;
 | |
|         list.tail.next = before;
 | |
|     } else {
 | |
|         this.updateCursors(this.tail, list.tail, null, list.head);
 | |
| 
 | |
|         // insert to end of the list
 | |
|         if (this.tail !== null) {
 | |
|             // if destination list has a tail, then it also has a head,
 | |
|             // but head doesn't change
 | |
| 
 | |
|             // dest tail -> source head
 | |
|             this.tail.next = list.head;
 | |
| 
 | |
|             // dest tail <- source head
 | |
|             list.head.prev = this.tail;
 | |
|         } else {
 | |
|             // if list has no a tail, then it also has no a head
 | |
|             // in this case points head to new item
 | |
|             this.head = list.head;
 | |
|         }
 | |
| 
 | |
|         // tail always start point to new item
 | |
|         this.tail = list.tail;
 | |
|     }
 | |
| 
 | |
|     list.head = null;
 | |
|     list.tail = null;
 | |
| 
 | |
|     return this;
 | |
| };
 | |
| 
 | |
| List.prototype.replace = function(oldItem, newItemOrList) {
 | |
|     if ('head' in newItemOrList) {
 | |
|         this.insertList(newItemOrList, oldItem);
 | |
|     } else {
 | |
|         this.insert(newItemOrList, oldItem);
 | |
|     }
 | |
| 
 | |
|     this.remove(oldItem);
 | |
| };
 | |
| 
 | |
| module.exports = List;
 |