UNPKG

@foxkit/list

Version:
626 lines (622 loc) 16.8 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { List: () => List }); module.exports = __toCommonJS(index_exports); // src/node.ts var ListNode = class { /** * Next node in the linked list */ next; /** * Previous node in the linked list */ prev; /** * Current value of this node */ value; constructor(value) { this.value = value; } }; // src/index.ts var inspectSym = Symbol.for("nodejs.util.inspect.custom"); var List = class _List { #head; #tail; #length = 0; constructor(from) { if (!from) return; const iterator = from[Symbol.iterator](); let result = iterator.next(); if (result.done) return; const head = new ListNode(result.value); let prev = head; let length = 1; while (!(result = iterator.next()).done) { const node = new ListNode(result.value); node.prev = prev; prev.next = node; prev = node; length++; } this.#head = head; this.#tail = prev; this.#length = length; } /** * Checks if an object (or any other value) is a List * @param list Object that may be a list * @returns boolean */ // eslint-disable-next-line @typescript-eslint/no-explicit-any static isList(list) { return list instanceof _List; } /** * First value of the List */ get head() { return this.#head?.value; } /** * Last value of the List */ get tail() { return this.#tail?.value; } /** * Current length of the List */ get length() { return this.#length; } /** * Adds value to the end of the List. * Further methods can be chained after this method. * @param value Value to add * @returns `this` Reference */ push(value) { const node = new ListNode(value); if (!this.#tail) { this.#head = node; } else { this.#tail.next = node; node.prev = this.#tail; } this.#tail = node; this.#length++; return this; } /** * Removes the last element from the List and returns it. * If the List is empty `undefined` is returned. * @returns Value or `undefined` */ pop() { if (!this.#tail) return; const node = this.#tail; const prev = node.prev; if (!prev) { this.#head = void 0; } else { prev.next = void 0; } this.#tail = prev; this.#length--; return node.value; } /** * Removes the first element from the List and returns it. * If the List is empty `undefined` is returned. * @returns Value or `undefined` */ shift() { const node = this.#head; if (!node) return; const next = node.next; node.next = void 0; if (!next) { this.#tail = void 0; } else { next.prev = void 0; } this.#head = next; this.#length--; return node.value; } /** * Adds value to the start of the List. * Further methods can be chained after this method. * @param value Value to add * @returns `this` Reference */ unshift(value) { const node = new ListNode(value); if (this.#head) { node.next = this.#head; this.#head.prev = node; } else { this.#tail = node; } this.#head = node; this.#length++; return this; } /** * Gets `ListNode` at specific index. If the index is outside of the * range of the List `undefined` is returned. * @param n Index of the element * @returns `ListNode` or `undefined` */ getNode(n) { if (n < 0 || n >= this.#length) return; const mid = this.#length / 2; let curr; if (n < mid) { curr = this.#head; for (let i = 0; i < n; i++) { if (!curr) return; curr = curr.next; } } else { curr = this.#tail; for (let i = this.#length - 1; i > n; i--) { if (!curr) return; curr = curr.prev; } } return curr; } /** * Gets value at specific index. If the index is outside of the range * of the List `undefined` is returned. * @param index Index of the element * @returns Value or `undefined` */ get(index) { const node = this.getNode(index); if (node) return node.value; return; } /** * Sets value at specific index and returns `true`. If the index is * outside of the range of the List `false` is returned. * If the index would correspond to the next new element this method * acts as an alias to `push`. * @param index Index of the element to set * @param value Value to set * @returns boolean */ set(index, value) { if (index == this.#length) { this.push(value); return true; } const node = this.getNode(index); if (!node) return false; node.value = value; return true; } /** * Inserts value at index and returns `true`. If the index is outside * of the range of the List `false` is returned. * If the index would correspond to the next new element this method * acts as an alias to `push`. * @param index Index of where to insert the new element * @param value Value of the new element * @returns boolean */ insert(index, value) { if (index < 0 || index > this.length) return false; switch (index) { case 0: return !!this.unshift(value); case this.length: return !!this.push(value); default: { const node = new ListNode(value); const prev = this.getNode(index - 1); if (!prev) return false; const next = prev.next; prev.next = node; node.prev = prev; if (next) next.prev = node; node.next = next; this.#length++; return true; } } } /** * Removes one or more elements starting at a given index and returns * `true`. If the index is outside the range of the List or a amount * smaller than one is given `false` is returned. * @param index Index at which to remove element(s) * @param amount (optional) Amount of elements to remove (default: 1) * @returns boolean */ remove(index, amount = 1) { if (index < 0 || index > this.length || amount < 1) return false; let curr = this.getNode(index); for (let i = 0; i < amount; i++) { if (!curr) return true; const { prev, next } = curr; curr.next = void 0; curr.prev = void 0; this.#length--; if (prev) { prev.next = next; } else { this.#head = next; } if (next) { next.prev = prev; } else { this.#tail = prev; break; } curr = next; } return true; } /** * Creates Array with all List values in order. * @returns Array */ toArray() { return Array.from(this); } /** * Inserts all values from another iterable (List, Array, etc.) into List at a * given index. If the index is out of range values will be inserted at the * start or end of the List as applicable. * @param index * @param iterable * @returns `this` Reference */ insertMany(index, iterable) { let prev; let next; if (index <= 0) { prev = void 0; next = this.#head; this.#head = void 0; } else if (index >= this.length) { prev = this.#tail; next = void 0; } else { next = this.getNode(index); prev = next?.prev; } for (const val of iterable) { const node = new ListNode(val); if (prev) { node.prev = prev; prev.next = node; } else { this.#head = node; } prev = node; this.#length++; } if (prev) { if (next) { next.prev = prev; prev.next = next; } else { this.#tail = prev; } } return this; } /** * Creates a copy of the current List * @returns new List */ clone() { const newList = new _List(this); return newList; } /** * Combines List or Array with current List. The existing List is not modified. * @param value List or Array of Values * @returns new List */ concat(value) { const newList = this.clone(); newList.insertMany(newList.length, value); return newList; } /** * Checks if a test callback returns `true` for every value * @param callback Test callback * @returns boolean */ every(callback) { let index = 0; let curr = this.#head; while (curr) { if (!callback(curr.value, index, this, curr)) return false; curr = curr.next; index++; } return true; } /** * Checks if a test callback returns `true` for at least one value * @param callback Test callback * @returns boolean */ some(callback) { let index = 0; let curr = this.#head; while (curr) { if (callback(curr.value, index, this, curr)) return true; curr = curr.next; index++; } return false; } /** * Creates new list with only the values that the test callback * returns `true` for. * @param callback Test callback * @returns Filtered List */ filter(callback) { const newList = new _List(); let index = 0; let curr = this.#head; while (curr) { if (callback(curr.value, index, this, curr)) newList.push(curr.value); curr = curr.next; index++; } return newList; } /** * Finds the index of the first value to satisfy the test callback. * If no value satisfies the test `-1` is returned. * @param callback Test callback * @returns number */ findIndex(callback) { let curr = this.#head; let index = 0; while (curr) { if (callback(curr.value, index, this, curr)) return index; curr = curr.next; index++; } return -1; } /** * Find the first value to satisfy the test callback and returns it. * If no value satisfies the test `undefined` is returned. * @param callback Test callback * @returns Value or `undefined` */ find(callback) { let curr = this.#head; let index = 0; while (curr) { if (callback(curr.value, index, this, curr)) return curr.value; curr = curr.next; index++; } return; } /** * Returns `true` if a value exists in the List, `false` if not. Values * are compared with strict equality (`===`). * @param value Value to search * @returns boolean */ includes(value) { return this.findIndex((v) => v === value) >= 0; } /** * Searches for value in the List and returns its index. If the value * is not found `-1` is returned. Values are compared with strict * equality (`===`). * @param value Value to search * @returns number */ indexOf(value) { return this.findIndex((v) => v === value); } /** * Runs a function on every element of the list. The function is passed the * current value, index, a reference to the full List and a reference to the * ListNode corresponding to the value. The return value of the callback is * ignored. * @param callback Function to run */ forEach(callback) { let curr = this.#head; let index = 0; while (curr) { callback(curr.value, index, this, curr); curr = curr.next; index++; } return; } /** * Creates a new list where each value is transformed by a callback * function. The function is passed the current value, index, a reference to * the full List and a reference to the ListNode corresponding to the value. * @param callback Callback to transform value * @returns new List */ map(callback) { const newList = new _List(); let curr = this.#head; let index = 0; while (curr) { newList.push(callback(curr.value, index, this, curr)); curr = curr.next; index++; } return newList; } /** * Reduces a List to a single new value with a reducer function callback. The * callback is passed the current accumulated value, the next value taken from * the list, the index of that value, a reference to the full List and the * ListNode that corresponds to the value and index. The return value is used * as the new accumulated value or return from this method. * @param callback Reducer callback function * @param initialValue initial value to pass to the first iteration * @returns accumulated value */ reduce(callback, initialValue) { let currentValue = initialValue; let curr = this.#head; let index = 0; while (curr) { currentValue = callback(currentValue, curr.value, index, this, curr); curr = curr.next; index++; } return currentValue; } /** * Creates a copy of the current List in reverse order. The original List is * not modified. * @returns new List */ reverse() { const newList = new _List(); let curr = this.#tail; while (curr) { newList.push(curr.value); curr = curr.prev; } return newList; } /** * Creates a new List that contains a slice of value from the original List * starting at a given index up to an optional end index. If no end index is * given the rest of the List is included in the new List. Negative indexes * are handled the same as `Array.prototype.slice`. Values are copied to the * new List, meaning that modifying either List will not manipulate the other! * @param start Start index * @param end (optional) End index * @returns new List */ slice(start, end) { const newList = new _List(); const startAt = start < 0 ? Math.max(0, this.#length + start) : start; const endAt = (end ?? 0) < 0 ? Math.max(0, this.#length + (end ?? 0)) : end ?? this.#length; let curr = this.getNode(startAt); let index = startAt; while (curr && index < endAt) { newList.push(curr.value); curr = curr.next; index++; } return newList; } /** * Creates a new List with all values sorted by a comparison callback function. * The function should return a number value the first argument should appear * before the second argument, `0` if they're equal, or a positive number if * the first argument should appear after the second in the new List. * If no callback is passed values are sorted in ascending ASCII character order. * @param callback * @returns new sorted List */ sort(callback) { const arr = this.toArray().sort(callback); return new _List(arr); } /** * Joins the List into a string, separating values with a configurable * separator string (`","` by default). * @param separator (optional) Custom separator string * @returns string */ join(separator = ",") { let curr = this.#head; if (!curr) return ""; let str = ""; str = `${curr.value}`; curr = curr.next; while (curr) { str = `${str}${separator}${curr.value}`; curr = curr.next; } return str; } /** * Joins the List into a string using `","` as the separator. Use `join` if * you would like to use a different separator. * @returns */ toString() { return this.join(","); } /** * Method for the Node.js Inspector * @param _ ignored depth parameter * @param options Options for the inspector to use on the list values (rendered as array) * @param inspect Please pass `node:util.inspect` here, thank you * @returns */ [inspectSym](_, options, inspect) { return `List(${this.#length}) ${inspect(this.toArray(), options)}`; } *[Symbol.iterator]() { const seenNodes = /* @__PURE__ */ new Set(); let curr = this.#head; let idx = 0; while (idx < this.length) { if (curr) { yield curr.value; seenNodes.add(curr); curr = curr.next; idx++; continue; } if (idx >= this.length) return; let temp = this.#head; let i = 0; while (temp && i < idx) { temp = temp.next; i++; } if (temp && i == idx) { curr = temp; continue; } } } }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { List });