Spaces:
Configuration error
Configuration error
// | |
// list | |
// ββββββββ | |
// ββββββββββββββββΌβhead β | |
// β β tailββΌβββββββββββββββ | |
// β ββββββββ β | |
// βΌ βΌ | |
// item item item item | |
// ββββββββ ββββββββ ββββββββ ββββββββ | |
// null ββββΌβprev ββββββΌβprev ββββββΌβprev ββββββΌβprev β | |
// β nextββΌββββΆβ nextββΌββββΆβ nextββΌββββΆβ nextββΌβββΆ null | |
// ββββββββ€ ββββββββ€ ββββββββ€ ββββββββ€ | |
// β data β β data β β data β β data β | |
// ββββββββ ββββββββ ββββββββ ββββββββ | |
// | |
let releasedCursors = null; | |
export 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); | |
} | |
} | |