// @file list.js
// @ingroup utility
// Doubly linked list.
// @date 12/05/2025
function ListNode(value) {
this._next = this;
this._prev = this;
this.value = value;
}
Object.defineProperty(ListNode.prototype, 'next', {
get: function () {
return this._next;
}
});
Object.defineProperty(ListNode.prototype, 'prev', {
get: function () {
return this._prev;
}
});
ListNode.prototype.insert = function (value) {
let node = new ListNode(value);
node._next = this;
node._prev = this._prev;
this._prev._next = node;
this._prev = node;
return this;
};
ListNode.prototype.remove = function (last = this._next) {
let next = last;
let prev = this._prev;
next._prev = prev;
prev._next = next;
return next;
};
ListNode.prototype.splice = function (first, last = first._next) {
if (first === last || last === this)
return;
last._prev._next = this;
first._prev._next = last;
this._prev._next = first;
let temp = this._prev;
this._prev = last._prev;
last._prev = first._prev;
first._prev = temp;
return this;
};
ListNode.prototype.step = function (n = 1) {
let result = this;
for (let i = 0; i < n; i++) {
result = result._next;
}
for (let i = 0; i > n; i--) {
result = result._prev;
}
return result;
};
ListNode.prototype.distance = function (last) {
let result = 0;
for (let first = this; first !== last; first = first._next) {
result++;
}
return result;
};
// List.
function List(iterable = []) {
this.head = new ListNode();
for (const x of iterable) {
this.pushBack(x);
}
}
Object.defineProperty(List.prototype, 'begin', {
get: function () {
return this.head.next;
}
});
Object.defineProperty(List.prototype, 'end', {
get: function () {
return this.head;
}
});
Object.defineProperty(List.prototype, 'empty', {
get: function () {
return this.begin === this.end;
}
});
Object.defineProperty(List.prototype, 'front', {
get: function () {
if (this.empty)
throw new Error("Data access in empty list");
return this.begin.value;
}
});
Object.defineProperty(List.prototype, 'back', {
get: function () {
if (this.empty)
throw new Error("Data access in empty list");
return this.end.prev.value;
}
});
List.prototype.pushFront = function (value) {
this.begin.insert(value);
return this;
};
List.prototype.pushBack = function (value) {
this.end.insert(value);
return this;
};
List.prototype.popFront = function () {
if (this.empty)
throw new Error("Pop in empty list");
this.begin.remove();
return this;
};
List.prototype.popBack = function () {
if (this.empty)
throw new Error("Pop in empty list");
this.end.prev.remove();
return this;
};
List.prototype.clear = function () {
this.begin.remove(this.end);
return this;
};
List.prototype[Symbol.iterator] = function* () {
const last = this.end;
for (let first = this.begin; first !== last; first = first.next) {
yield first.value;
}
};
List.prototype.map = function (callbackFn, ...args) {
let result = new List();
for (const x of this) {
result.pushBack(callbackFn(x, ...args));
}
return result;
};
List.prototype.forEach = function (callbackFn, ...args) {
for (const x of this) {
callbackFn(x, ...args);
}
};
List.prototype.reduce = function (callbackFn, initial, ...args) {
for (const x of this) {
initial = callbackFn(initial, x, ...args);
}
return initial;
}
List.prototype.toString = function () {
return `List {${Array.from(this).join(", ")}}`;
};
// Exports.
module.exports = {
List,
ListNode
};
// END
// @file list_test.js
// @ingroup testing
// Test List module.
// const { List } = require("list");
// const colorize = require("colorize");
const colorize = (str, color) => str;
// Utility.
function report(condition, message) {
console.log(message.padEnd(30, ".")
+ (condition
? colorize("Pass", "green")
: colorize("Fail", "red")));
}
function iota(n, start = 0, step = 1) {
return Array.from({length: n}, (_, i) => i*step + start);
}
function listSize(list) {
return list.begin.distance(list.end);
}
function arrayEquals(a1, a2) {
if (a1.length !== a2.length)
return false;
return a1.every((v, i) => v === a2[i]);
}
function listEqualsArray(list, array) {
return arrayEquals(array, Array.from(list));
}
// Test.
function testConstructor() {
console.log(colorize("constructor", "blue"));
let keys = iota(10);
let list = new List();
report(listSize(list) === 0, "size (default)");
report(list.empty, "empty (default)");
list = new List(keys);
report(listSize(list) === keys.length, "size (iterable)");
report(!list.empty, "empty (iterable)");
report(listEqualsArray(list, keys), "order");
}
function testPushFront() {
console.log(colorize("pushFront", "blue"));
let keys = iota(10);
let list = new List();
keys.forEach(v => list.pushFront(v));
report(listSize(list) === keys.length, "size");
report(!list.empty, "empty");
report(listEqualsArray(list, keys.reverse()), "order");
}
function testPushBack() {
console.log(colorize("pushBack", "blue"));
let keys = iota(10);
let list = new List();
keys.forEach(v => list.pushBack(v));
report(listSize(list) === keys.length, "size");
report(!list.empty, "empty");
report(listEqualsArray(list, keys), "order");
}
function testPopFront() {
console.log(colorize("popFront", "blue"));
let keys = iota(10);
let list = new List(keys);
for (let i = 0; i < 5; i++) {
list.popFront();
}
report(listSize(list) === 5, "size (midpoint)");
report(!list.empty, "empty (midpoint)");
report(listEqualsArray(list, keys.slice(5)), "order");
for (let i = 0; i < 5; i++) {
list.popFront();
}
report(listSize(list) === 0, "size");
report(list.empty, "empty");
let except = false;
try {
list.popFront();
} catch (_) {
except = true;
}
report(except, "exception");
}
function testPopBack() {
console.log(colorize("popBack", "blue"));
let keys = iota(10);
let list = new List(keys);
for (let i = 0; i < 5; i++) {
list.popBack();
}
report(listSize(list) === 5, "size (midpoint)");
report(!list.empty, "empty (midpoint)");
report(listEqualsArray(list, keys.slice(0, 5)), "order");
for (let i = 0; i < 5; i++) {
list.popBack();
}
report(listSize(list) === 0, "size");
report(list.empty, "empty");
let except = false;
try {
list.popBack();
} catch (_) {
except = true;
}
report(except, "exception");
}
function testFront() {
console.log(colorize("front", "blue"));
let keys = iota(3, 1);
let list = new List(keys);
for (let i of keys) {
report(list.front == i, `value (${i})`);
list.popFront();
}
let except = false;
try {
list.front;
} catch (_) {
except = true;
}
report(except, "exception");
}
function testBack() {
console.log(colorize("back", "blue"));
let keys = iota(3, 1);
let list = new List(keys);
for (let i of keys.reverse()) {
report(list.back == i, `value (${i})`);
list.popBack();
}
let except = false;
try {
list.back;
} catch (_) {
except = true;
}
report(except, "exception");
}
function testForEach() {
console.log(colorize("forEach", "blue"));
let keys = iota(10);
let list = new List(keys);
let temp = [];
list.forEach((x, a) => a.push(x), temp);
report(temp.length === keys.length, "size");
report(arrayEquals(temp, keys), "order");
}
function testMap() {
console.log(colorize("map", "blue"));
let keys = iota(10);
let list = new List(keys);
let temp = list.map(x => x, list);
report(listSize(temp) === keys.length, "size");
report(listEqualsArray(temp, keys), "order");
}
function testReduce() {
console.log(colorize("reduce", "blue"));
let keys = iota(10);
let list = new List(keys);
let result1 = keys.reduce((a, x) => a + x, 0);
let result2 = list.reduce((a, x) => a + x, 0);
report(result1 == result2, "value");
}
function testInsert() {
console.log(colorize("insert", "blue"));
let keys = iota(2, 1, 3);
let list = new List(keys);
keys.splice(1, 0, 2);
list.begin.next.insert(2);
report(listSize(list) === keys.length, "size [1]");
report(listEqualsArray(list, keys), "order [1]");
keys.splice(keys.length-1, 0, 3);
list.end.prev.insert(3);
report(listSize(list) === keys.length, "size [n-1]");
report(listEqualsArray(list, keys), "order [n-1]");
}
function testRemove() {
console.log(colorize("remove", "blue"));
let keys = iota(6, 1);
let list = new List(keys);
keys.splice(1, 1);
list.begin.next.remove();
report(listSize(list) === keys.length, "size [1]");
report(listEqualsArray(list, keys), "order [1]");
keys.splice(keys.length-1, 1);
list.end.prev.remove();
report(listSize(list) === keys.length, "size [n-1]");
report(listEqualsArray(list, keys), "order [n-1]");
keys.splice(1, keys.length-2);
list.begin.next.remove(list.end.prev);
report(listSize(list) === keys.length, "size [1, n-1]");
report(listEqualsArray(list, keys), "order [1, n-1]");
}
function testSplice() {
console.log(colorize("splice", "blue"));
let keys1 = iota(10);
let keys2 = iota(2, 11, 11);
let list1 = new List(keys1);
let list2 = new List(keys2);
let temp;
temp = keys1.slice();
temp.splice(0, 0, ...keys2);
list1.begin.splice(list2.begin, list2.end);
report(listSize(list1) === temp.length, "size [0] (dst)");
report(listSize(list2) === 0, "size [0] (src)");
report(listEqualsArray(list1, temp), "order [0]");
list1 = new List(keys1);
list2 = new List(keys2);
temp = keys1.slice();
temp.splice(temp.length, 0, ...keys2);
list1.end.splice(list2.begin, list2.end);
report(listSize(list1) === temp.length, "size [n] (dst)");
report(listSize(list2) === 0, "size [n] (src)");
report(listEqualsArray(list1, temp), "order [n]");
list1 = new List(keys1);
list2 = new List(keys2);
temp = keys1.slice();
temp.splice(5, 0, ...keys2);
list1.begin.step(5).splice(list2.begin, list2.end);
report(listSize(list1) === temp.length, "size [5] (dst)");
report(listSize(list2) === 0, "size [5] (src)");
report(listEqualsArray(list1, temp), "order [5]");
}
// Entry.
testConstructor();
testPushFront();
testPushBack();
testPopFront();
testPopBack();
testFront();
testBack();
testForEach();
testMap();
testReduce();
testInsert();
testRemove();
testSplice();