474 lines
12 KiB
JavaScript
474 lines
12 KiB
JavaScript
|
'use strict';
|
||
|
|
||
|
//
|
||
|
// list
|
||
|
// ┌──────┐
|
||
|
// ┌──────────────┼─head │
|
||
|
// │ │ tail─┼──────────────┐
|
||
|
// │ └──────┘ │
|
||
|
// ▼ ▼
|
||
|
// item item item item
|
||
|
// ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
|
||
|
// null ◀──┼─prev │◀───┼─prev │◀───┼─prev │◀───┼─prev │
|
||
|
// │ next─┼───▶│ next─┼───▶│ next─┼───▶│ next─┼──▶ null
|
||
|
// ├──────┤ ├──────┤ ├──────┤ ├──────┤
|
||
|
// │ data │ │ data │ │ data │ │ data │
|
||
|
// └──────┘ └──────┘ └──────┘ └──────┘
|
||
|
//
|
||
|
|
||
|
let releasedCursors = null;
|
||
|
|
||
|
class List {
|
||
|
static createItem(data) {
|
||
|
return {
|
||
|
prev: null,
|
||
|
next: null,
|
||
|
data
|
||
|
};
|
||
|
}
|
||
|
|
||
|
constructor() {
|
||
|
this.head = null;
|
||
|
this.tail = null;
|
||
|
this.cursor = null;
|
||
|
}
|
||
|
createItem(data) {
|
||
|
return List.createItem(data);
|
||
|
}
|
||
|
|
||
|
// cursor helpers
|
||
|
allocateCursor(prev, next) {
|
||
|
let cursor;
|
||
|
|
||
|
if (releasedCursors !== null) {
|
||
|
cursor = releasedCursors;
|
||
|
releasedCursors = releasedCursors.cursor;
|
||
|
cursor.prev = prev;
|
||
|
cursor.next = next;
|
||
|
cursor.cursor = this.cursor;
|
||
|
} else {
|
||
|
cursor = {
|
||
|
prev,
|
||
|
next,
|
||
|
cursor: this.cursor
|
||
|
};
|
||
|
}
|
||
|
|
||
|
this.cursor = cursor;
|
||
|
|
||
|
return cursor;
|
||
|
}
|
||
|
releaseCursor() {
|
||
|
const { cursor } = this;
|
||
|
|
||
|
this.cursor = cursor.cursor;
|
||
|
cursor.prev = null;
|
||
|
cursor.next = null;
|
||
|
cursor.cursor = releasedCursors;
|
||
|
releasedCursors = cursor;
|
||
|
}
|
||
|
updateCursors(prevOld, prevNew, nextOld, nextNew) {
|
||
|
let { cursor } = this;
|
||
|
|
||
|
while (cursor !== null) {
|
||
|
if (cursor.prev === prevOld) {
|
||
|
cursor.prev = prevNew;
|
||
|
}
|
||
|
|
||
|
if (cursor.next === nextOld) {
|
||
|
cursor.next = nextNew;
|
||
|
}
|
||
|
|
||
|
cursor = cursor.cursor;
|
||
|
}
|
||
|
}
|
||
|
*[Symbol.iterator]() {
|
||
|
for (let cursor = this.head; cursor !== null; cursor = cursor.next) {
|
||
|
yield cursor.data;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// getters
|
||
|
get size() {
|
||
|
let size = 0;
|
||
|
|
||
|
for (let cursor = this.head; cursor !== null; cursor = cursor.next) {
|
||
|
size++;
|
||
|
}
|
||
|
|
||
|
return size;
|
||
|
}
|
||
|
get isEmpty() {
|
||
|
return this.head === null;
|
||
|
}
|
||
|
get first() {
|
||
|
return this.head && this.head.data;
|
||
|
}
|
||
|
get last() {
|
||
|
return this.tail && this.tail.data;
|
||
|
}
|
||
|
|
||
|
// convertors
|
||
|
fromArray(array) {
|
||
|
let cursor = null;
|
||
|
this.head = null;
|
||
|
|
||
|
for (let data of array) {
|
||
|
const item = List.createItem(data);
|
||
|
|
||
|
if (cursor !== null) {
|
||
|
cursor.next = item;
|
||
|
} else {
|
||
|
this.head = item;
|
||
|
}
|
||
|
|
||
|
item.prev = cursor;
|
||
|
cursor = item;
|
||
|
}
|
||
|
|
||
|
this.tail = cursor;
|
||
|
return this;
|
||
|
}
|
||
|
toArray() {
|
||
|
return [...this];
|
||
|
}
|
||
|
toJSON() {
|
||
|
return [...this];
|
||
|
}
|
||
|
|
||
|
// array-like methods
|
||
|
forEach(fn, thisArg = this) {
|
||
|
// push cursor
|
||
|
const cursor = this.allocateCursor(null, this.head);
|
||
|
|
||
|
while (cursor.next !== null) {
|
||
|
const item = cursor.next;
|
||
|
cursor.next = item.next;
|
||
|
fn.call(thisArg, item.data, item, this);
|
||
|
}
|
||
|
|
||
|
// pop cursor
|
||
|
this.releaseCursor();
|
||
|
}
|
||
|
forEachRight(fn, thisArg = this) {
|
||
|
// push cursor
|
||
|
const cursor = this.allocateCursor(this.tail, null);
|
||
|
|
||
|
while (cursor.prev !== null) {
|
||
|
const item = cursor.prev;
|
||
|
cursor.prev = item.prev;
|
||
|
fn.call(thisArg, item.data, item, this);
|
||
|
}
|
||
|
|
||
|
// pop cursor
|
||
|
this.releaseCursor();
|
||
|
}
|
||
|
reduce(fn, initialValue, thisArg = this) {
|
||
|
// push cursor
|
||
|
let cursor = this.allocateCursor(null, this.head);
|
||
|
let acc = initialValue;
|
||
|
let item;
|
||
|
|
||
|
while (cursor.next !== null) {
|
||
|
item = cursor.next;
|
||
|
cursor.next = item.next;
|
||
|
|
||
|
acc = fn.call(thisArg, acc, item.data, item, this);
|
||
|
}
|
||
|
|
||
|
// pop cursor
|
||
|
this.releaseCursor();
|
||
|
|
||
|
return acc;
|
||
|
}
|
||
|
reduceRight(fn, initialValue, thisArg = this) {
|
||
|
// push cursor
|
||
|
let cursor = this.allocateCursor(this.tail, null);
|
||
|
let acc = initialValue;
|
||
|
let item;
|
||
|
|
||
|
while (cursor.prev !== null) {
|
||
|
item = cursor.prev;
|
||
|
cursor.prev = item.prev;
|
||
|
|
||
|
acc = fn.call(thisArg, acc, item.data, item, this);
|
||
|
}
|
||
|
|
||
|
// pop cursor
|
||
|
this.releaseCursor();
|
||
|
|
||
|
return acc;
|
||
|
}
|
||
|
some(fn, thisArg = this) {
|
||
|
for (let cursor = this.head; cursor !== null; cursor = cursor.next) {
|
||
|
if (fn.call(thisArg, cursor.data, cursor, this)) {
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
map(fn, thisArg = this) {
|
||
|
const result = new List();
|
||
|
|
||
|
for (let cursor = this.head; cursor !== null; cursor = cursor.next) {
|
||
|
result.appendData(fn.call(thisArg, cursor.data, cursor, this));
|
||
|
}
|
||
|
|
||
|
return result;
|
||
|
}
|
||
|
filter(fn, thisArg = this) {
|
||
|
const result = new List();
|
||
|
|
||
|
for (let cursor = this.head; cursor !== null; cursor = cursor.next) {
|
||
|
if (fn.call(thisArg, cursor.data, cursor, this)) {
|
||
|
result.appendData(cursor.data);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
nextUntil(start, fn, thisArg = this) {
|
||
|
if (start === null) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// push cursor
|
||
|
const cursor = this.allocateCursor(null, start);
|
||
|
|
||
|
while (cursor.next !== null) {
|
||
|
const item = cursor.next;
|
||
|
cursor.next = item.next;
|
||
|
if (fn.call(thisArg, item.data, item, this)) {
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// pop cursor
|
||
|
this.releaseCursor();
|
||
|
}
|
||
|
prevUntil(start, fn, thisArg = this) {
|
||
|
if (start === null) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// push cursor
|
||
|
const cursor = this.allocateCursor(start, null);
|
||
|
|
||
|
while (cursor.prev !== null) {
|
||
|
const item = cursor.prev;
|
||
|
cursor.prev = item.prev;
|
||
|
if (fn.call(thisArg, item.data, item, this)) {
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// pop cursor
|
||
|
this.releaseCursor();
|
||
|
}
|
||
|
|
||
|
// mutation
|
||
|
clear() {
|
||
|
this.head = null;
|
||
|
this.tail = null;
|
||
|
}
|
||
|
copy() {
|
||
|
const result = new List();
|
||
|
|
||
|
for (let data of this) {
|
||
|
result.appendData(data);
|
||
|
}
|
||
|
|
||
|
return result;
|
||
|
}
|
||
|
prepend(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;
|
||
|
}
|
||
|
prependData(data) {
|
||
|
return this.prepend(List.createItem(data));
|
||
|
}
|
||
|
append(item) {
|
||
|
return this.insert(item);
|
||
|
}
|
||
|
appendData(data) {
|
||
|
return this.insert(List.createItem(data));
|
||
|
}
|
||
|
insert(item, before = null) {
|
||
|
if (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;
|
||
|
}
|
||
|
insertData(data, before) {
|
||
|
return this.insert(List.createItem(data), before);
|
||
|
}
|
||
|
remove(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;
|
||
|
}
|
||
|
push(data) {
|
||
|
this.insert(List.createItem(data));
|
||
|
}
|
||
|
pop() {
|
||
|
return this.tail !== null ? this.remove(this.tail) : null;
|
||
|
}
|
||
|
unshift(data) {
|
||
|
this.prepend(List.createItem(data));
|
||
|
}
|
||
|
shift() {
|
||
|
return this.head !== null ? this.remove(this.head) : null;
|
||
|
}
|
||
|
prependList(list) {
|
||
|
return this.insertList(list, this.head);
|
||
|
}
|
||
|
appendList(list) {
|
||
|
return this.insertList(list);
|
||
|
}
|
||
|
insertList(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;
|
||
|
}
|
||
|
replace(oldItem, newItemOrList) {
|
||
|
if ('head' in newItemOrList) {
|
||
|
this.insertList(newItemOrList, oldItem);
|
||
|
} else {
|
||
|
this.insert(newItemOrList, oldItem);
|
||
|
}
|
||
|
|
||
|
this.remove(oldItem);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
exports.List = List;
|