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