mirror of
https://github.com/robonen/tools.git
synced 2026-03-20 10:54:44 +00:00
feat(core/stdlib): implement LinkedList, PriorityQueue, and Queue data structures
This commit is contained in:
229
core/stdlib/src/structs/BinaryHeap/index.test.ts
Normal file
229
core/stdlib/src/structs/BinaryHeap/index.test.ts
Normal file
@@ -0,0 +1,229 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { BinaryHeap } from '.';
|
||||
|
||||
describe('BinaryHeap', () => {
|
||||
describe('constructor', () => {
|
||||
it('should create an empty heap', () => {
|
||||
const heap = new BinaryHeap<number>();
|
||||
|
||||
expect(heap.length).toBe(0);
|
||||
expect(heap.isEmpty).toBe(true);
|
||||
});
|
||||
|
||||
it('should create a heap from single value', () => {
|
||||
const heap = new BinaryHeap(42);
|
||||
|
||||
expect(heap.length).toBe(1);
|
||||
expect(heap.peek()).toBe(42);
|
||||
});
|
||||
|
||||
it('should create a heap from array (heapify)', () => {
|
||||
const heap = new BinaryHeap([5, 3, 8, 1, 4]);
|
||||
|
||||
expect(heap.length).toBe(5);
|
||||
expect(heap.peek()).toBe(1);
|
||||
});
|
||||
|
||||
it('should accept a custom comparator for max-heap', () => {
|
||||
const heap = new BinaryHeap([5, 3, 8, 1, 4], {
|
||||
comparator: (a, b) => b - a,
|
||||
});
|
||||
|
||||
expect(heap.peek()).toBe(8);
|
||||
});
|
||||
});
|
||||
|
||||
describe('push', () => {
|
||||
it('should insert elements maintaining heap property', () => {
|
||||
const heap = new BinaryHeap<number>();
|
||||
|
||||
heap.push(5);
|
||||
heap.push(3);
|
||||
heap.push(8);
|
||||
heap.push(1);
|
||||
|
||||
expect(heap.peek()).toBe(1);
|
||||
expect(heap.length).toBe(4);
|
||||
});
|
||||
|
||||
it('should handle duplicate values', () => {
|
||||
const heap = new BinaryHeap<number>();
|
||||
|
||||
heap.push(3);
|
||||
heap.push(3);
|
||||
heap.push(3);
|
||||
|
||||
expect(heap.length).toBe(3);
|
||||
expect(heap.peek()).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pop', () => {
|
||||
it('should return undefined for empty heap', () => {
|
||||
const heap = new BinaryHeap<number>();
|
||||
|
||||
expect(heap.pop()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should extract elements in min-heap order', () => {
|
||||
const heap = new BinaryHeap([5, 3, 8, 1, 4, 2, 7, 6]);
|
||||
const sorted: number[] = [];
|
||||
|
||||
while (!heap.isEmpty) {
|
||||
sorted.push(heap.pop()!);
|
||||
}
|
||||
|
||||
expect(sorted).toEqual([1, 2, 3, 4, 5, 6, 7, 8]);
|
||||
});
|
||||
|
||||
it('should extract elements in max-heap order with custom comparator', () => {
|
||||
const heap = new BinaryHeap([5, 3, 8, 1, 4], {
|
||||
comparator: (a, b) => b - a,
|
||||
});
|
||||
const sorted: number[] = [];
|
||||
|
||||
while (!heap.isEmpty) {
|
||||
sorted.push(heap.pop()!);
|
||||
}
|
||||
|
||||
expect(sorted).toEqual([8, 5, 4, 3, 1]);
|
||||
});
|
||||
|
||||
it('should handle single element', () => {
|
||||
const heap = new BinaryHeap(42);
|
||||
|
||||
expect(heap.pop()).toBe(42);
|
||||
expect(heap.isEmpty).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('peek', () => {
|
||||
it('should return undefined for empty heap', () => {
|
||||
const heap = new BinaryHeap<number>();
|
||||
|
||||
expect(heap.peek()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return root without removing it', () => {
|
||||
const heap = new BinaryHeap([5, 3, 1]);
|
||||
|
||||
expect(heap.peek()).toBe(1);
|
||||
expect(heap.length).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('clear', () => {
|
||||
it('should remove all elements', () => {
|
||||
const heap = new BinaryHeap([1, 2, 3]);
|
||||
|
||||
const result = heap.clear();
|
||||
|
||||
expect(heap.length).toBe(0);
|
||||
expect(heap.isEmpty).toBe(true);
|
||||
expect(result).toBe(heap);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toArray', () => {
|
||||
it('should return empty array for empty heap', () => {
|
||||
const heap = new BinaryHeap<number>();
|
||||
|
||||
expect(heap.toArray()).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return a shallow copy', () => {
|
||||
const heap = new BinaryHeap([3, 1, 2]);
|
||||
const arr = heap.toArray();
|
||||
|
||||
arr.push(99);
|
||||
|
||||
expect(heap.length).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toString', () => {
|
||||
it('should return formatted string', () => {
|
||||
const heap = new BinaryHeap([1, 2, 3]);
|
||||
|
||||
expect(heap.toString()).toBe('BinaryHeap(3)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('iterator', () => {
|
||||
it('should iterate over heap elements', () => {
|
||||
const heap = new BinaryHeap([5, 3, 8, 1]);
|
||||
const elements = [...heap];
|
||||
|
||||
expect(elements.length).toBe(4);
|
||||
expect(elements[0]).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('custom comparator', () => {
|
||||
it('should work with string length comparator', () => {
|
||||
const heap = new BinaryHeap(['banana', 'apple', 'kiwi', 'fig'], {
|
||||
comparator: (a, b) => a.length - b.length,
|
||||
});
|
||||
|
||||
expect(heap.pop()).toBe('fig');
|
||||
expect(heap.pop()).toBe('kiwi');
|
||||
});
|
||||
|
||||
it('should work with object comparator', () => {
|
||||
interface Task {
|
||||
priority: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const heap = new BinaryHeap<Task>(
|
||||
[
|
||||
{ priority: 3, name: 'low' },
|
||||
{ priority: 1, name: 'high' },
|
||||
{ priority: 2, name: 'medium' },
|
||||
],
|
||||
{ comparator: (a, b) => a.priority - b.priority },
|
||||
);
|
||||
|
||||
expect(heap.pop()?.name).toBe('high');
|
||||
expect(heap.pop()?.name).toBe('medium');
|
||||
expect(heap.pop()?.name).toBe('low');
|
||||
});
|
||||
});
|
||||
|
||||
describe('heapify', () => {
|
||||
it('should correctly heapify large arrays', () => {
|
||||
const values = Array.from({ length: 1000 }, () => Math.random() * 1000 | 0);
|
||||
const heap = new BinaryHeap(values);
|
||||
const sorted: number[] = [];
|
||||
|
||||
while (!heap.isEmpty) {
|
||||
sorted.push(heap.pop()!);
|
||||
}
|
||||
|
||||
const expected = [...values].sort((a, b) => a - b);
|
||||
|
||||
expect(sorted).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('interleaved operations', () => {
|
||||
it('should maintain heap property with mixed push and pop', () => {
|
||||
const heap = new BinaryHeap<number>();
|
||||
|
||||
heap.push(10);
|
||||
heap.push(5);
|
||||
expect(heap.pop()).toBe(5);
|
||||
|
||||
heap.push(3);
|
||||
heap.push(7);
|
||||
expect(heap.pop()).toBe(3);
|
||||
|
||||
heap.push(1);
|
||||
expect(heap.pop()).toBe(1);
|
||||
expect(heap.pop()).toBe(7);
|
||||
expect(heap.pop()).toBe(10);
|
||||
expect(heap.pop()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
220
core/stdlib/src/structs/BinaryHeap/index.ts
Normal file
220
core/stdlib/src/structs/BinaryHeap/index.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
import { first } from '../../arrays';
|
||||
import { isArray } from '../../types';
|
||||
import type { BinaryHeapLike, Comparator } from './types';
|
||||
|
||||
export type { BinaryHeapLike, Comparator } from './types';
|
||||
|
||||
export interface BinaryHeapOptions<T> {
|
||||
comparator?: Comparator<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default min-heap comparator for numeric values
|
||||
*
|
||||
* @param {number} a First element
|
||||
* @param {number} b Second element
|
||||
* @returns {number} Negative if a < b, positive if a > b, zero if equal
|
||||
*/
|
||||
const defaultComparator: Comparator<any> = (a: number, b: number) => a - b;
|
||||
|
||||
/**
|
||||
* @name BinaryHeap
|
||||
* @category Data Structures
|
||||
* @description Binary heap backed by a flat array with configurable comparator
|
||||
*
|
||||
* @since 0.0.8
|
||||
*
|
||||
* @template T The type of elements stored in the heap
|
||||
*/
|
||||
export class BinaryHeap<T> implements BinaryHeapLike<T> {
|
||||
/**
|
||||
* The comparator function used to order elements
|
||||
*
|
||||
* @private
|
||||
* @type {Comparator<T>}
|
||||
*/
|
||||
private readonly comparator: Comparator<T>;
|
||||
|
||||
/**
|
||||
* Internal flat array backing the heap
|
||||
*
|
||||
* @private
|
||||
* @type {T[]}
|
||||
*/
|
||||
private readonly heap: T[] = [];
|
||||
|
||||
/**
|
||||
* Creates an instance of BinaryHeap
|
||||
*
|
||||
* @param {(T[] | T)} [initialValues] The initial values to heapify
|
||||
* @param {BinaryHeapOptions<T>} [options] Heap configuration
|
||||
*/
|
||||
constructor(initialValues?: T[] | T, options?: BinaryHeapOptions<T>) {
|
||||
this.comparator = options?.comparator ?? defaultComparator;
|
||||
|
||||
if (initialValues != null) {
|
||||
const items = isArray(initialValues) ? initialValues : [initialValues];
|
||||
this.heap.push(...items);
|
||||
this.heapify();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of elements in the heap
|
||||
* @returns {number} The number of elements in the heap
|
||||
*/
|
||||
public get length(): number {
|
||||
return this.heap.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the heap is empty
|
||||
* @returns {boolean} `true` if the heap is empty, `false` otherwise
|
||||
*/
|
||||
public get isEmpty(): boolean {
|
||||
return this.heap.length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes an element into the heap
|
||||
* @param {T} element The element to insert
|
||||
*/
|
||||
public push(element: T): void {
|
||||
this.heap.push(element);
|
||||
this.siftUp(this.heap.length - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes and returns the root element (min or max depending on comparator)
|
||||
* @returns {T | undefined} The root element, or `undefined` if the heap is empty
|
||||
*/
|
||||
public pop(): T | undefined {
|
||||
if (this.heap.length === 0) return undefined;
|
||||
|
||||
const root = first(this.heap)!;
|
||||
const last = this.heap.pop()!;
|
||||
|
||||
if (this.heap.length > 0) {
|
||||
this.heap[0] = last;
|
||||
this.siftDown(0);
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the root element without removing it
|
||||
* @returns {T | undefined} The root element, or `undefined` if the heap is empty
|
||||
*/
|
||||
public peek(): T | undefined {
|
||||
return first(this.heap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all elements from the heap
|
||||
* @returns {this} The heap instance for chaining
|
||||
*/
|
||||
public clear(): this {
|
||||
this.heap.length = 0;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a shallow copy of the heap elements as an array (heap order, not sorted)
|
||||
* @returns {T[]} Array of elements in heap order
|
||||
*/
|
||||
public toArray(): T[] {
|
||||
return this.heap.slice();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of the heap
|
||||
* @returns {string} String representation
|
||||
*/
|
||||
public toString(): string {
|
||||
return `BinaryHeap(${this.heap.length})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterator over heap elements in heap order
|
||||
*/
|
||||
public *[Symbol.iterator](): Iterator<T> {
|
||||
yield* this.heap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Async iterator over heap elements in heap order
|
||||
*/
|
||||
public async *[Symbol.asyncIterator](): AsyncIterator<T> {
|
||||
for (const element of this.heap)
|
||||
yield element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores heap property by sifting an element up
|
||||
*
|
||||
* @private
|
||||
* @param {number} index The index of the element to sift up
|
||||
*/
|
||||
private siftUp(index: number): void {
|
||||
const heap = this.heap;
|
||||
const cmp = this.comparator;
|
||||
|
||||
while (index > 0) {
|
||||
const parent = (index - 1) >> 1;
|
||||
|
||||
if (cmp(heap[index]!, heap[parent]!) >= 0) break;
|
||||
|
||||
const temp = heap[index]!;
|
||||
heap[index] = heap[parent]!;
|
||||
heap[parent] = temp;
|
||||
|
||||
index = parent;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores heap property by sifting an element down
|
||||
*
|
||||
* @private
|
||||
* @param {number} index The index of the element to sift down
|
||||
*/
|
||||
private siftDown(index: number): void {
|
||||
const heap = this.heap;
|
||||
const cmp = this.comparator;
|
||||
const length = heap.length;
|
||||
|
||||
while (true) {
|
||||
let smallest = index;
|
||||
const left = 2 * index + 1;
|
||||
const right = 2 * index + 2;
|
||||
|
||||
if (left < length && cmp(heap[left]!, heap[smallest]!) < 0) {
|
||||
smallest = left;
|
||||
}
|
||||
|
||||
if (right < length && cmp(heap[right]!, heap[smallest]!) < 0) {
|
||||
smallest = right;
|
||||
}
|
||||
|
||||
if (smallest === index) break;
|
||||
|
||||
const temp = heap[index]!;
|
||||
heap[index] = heap[smallest]!;
|
||||
heap[smallest] = temp;
|
||||
|
||||
index = smallest;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds heap from unordered array in O(n) using Floyd's algorithm
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private heapify(): void {
|
||||
for (let i = (this.heap.length >> 1) - 1; i >= 0; i--) {
|
||||
this.siftDown(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
13
core/stdlib/src/structs/BinaryHeap/types.ts
Normal file
13
core/stdlib/src/structs/BinaryHeap/types.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export type Comparator<T> = (a: T, b: T) => number;
|
||||
|
||||
export interface BinaryHeapLike<T> extends Iterable<T>, AsyncIterable<T> {
|
||||
readonly length: number;
|
||||
readonly isEmpty: boolean;
|
||||
|
||||
push(element: T): void;
|
||||
pop(): T | undefined;
|
||||
peek(): T | undefined;
|
||||
clear(): this;
|
||||
toArray(): T[];
|
||||
toString(): string;
|
||||
}
|
||||
247
core/stdlib/src/structs/CircularBuffer/index.test.ts
Normal file
247
core/stdlib/src/structs/CircularBuffer/index.test.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { CircularBuffer } from '.';
|
||||
|
||||
describe('circularBuffer', () => {
|
||||
describe('constructor', () => {
|
||||
it('create an empty buffer', () => {
|
||||
const buf = new CircularBuffer<number>();
|
||||
|
||||
expect(buf.length).toBe(0);
|
||||
expect(buf.isEmpty).toBe(true);
|
||||
expect(buf.capacity).toBeGreaterThanOrEqual(4);
|
||||
});
|
||||
|
||||
it('create a buffer with initial array', () => {
|
||||
const buf = new CircularBuffer([1, 2, 3]);
|
||||
|
||||
expect(buf.length).toBe(3);
|
||||
expect(buf.peekFront()).toBe(1);
|
||||
expect(buf.peekBack()).toBe(3);
|
||||
});
|
||||
|
||||
it('create a buffer with a single value', () => {
|
||||
const buf = new CircularBuffer(42);
|
||||
|
||||
expect(buf.length).toBe(1);
|
||||
expect(buf.peekFront()).toBe(42);
|
||||
});
|
||||
|
||||
it('create a buffer with initial capacity hint', () => {
|
||||
const buf = new CircularBuffer<number>(undefined, 32);
|
||||
|
||||
expect(buf.capacity).toBe(32);
|
||||
});
|
||||
|
||||
it('round capacity up to next power of two', () => {
|
||||
const buf = new CircularBuffer<number>(undefined, 5);
|
||||
|
||||
expect(buf.capacity).toBe(8);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pushBack / popFront', () => {
|
||||
it('FIFO order', () => {
|
||||
const buf = new CircularBuffer<number>();
|
||||
buf.pushBack(1);
|
||||
buf.pushBack(2);
|
||||
buf.pushBack(3);
|
||||
|
||||
expect(buf.popFront()).toBe(1);
|
||||
expect(buf.popFront()).toBe(2);
|
||||
expect(buf.popFront()).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pushFront / popBack', () => {
|
||||
it('LIFO order', () => {
|
||||
const buf = new CircularBuffer<number>();
|
||||
buf.pushFront(1);
|
||||
buf.pushFront(2);
|
||||
buf.pushFront(3);
|
||||
|
||||
expect(buf.popBack()).toBe(1);
|
||||
expect(buf.popBack()).toBe(2);
|
||||
expect(buf.popBack()).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('popFront', () => {
|
||||
it('return undefined if empty', () => {
|
||||
const buf = new CircularBuffer<number>();
|
||||
|
||||
expect(buf.popFront()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('popBack', () => {
|
||||
it('return undefined if empty', () => {
|
||||
const buf = new CircularBuffer<number>();
|
||||
|
||||
expect(buf.popBack()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('peekFront / peekBack', () => {
|
||||
it('return elements without removing', () => {
|
||||
const buf = new CircularBuffer([1, 2, 3]);
|
||||
|
||||
expect(buf.peekFront()).toBe(1);
|
||||
expect(buf.peekBack()).toBe(3);
|
||||
expect(buf.length).toBe(3);
|
||||
});
|
||||
|
||||
it('return undefined if empty', () => {
|
||||
const buf = new CircularBuffer<number>();
|
||||
|
||||
expect(buf.peekFront()).toBeUndefined();
|
||||
expect(buf.peekBack()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('get', () => {
|
||||
it('access element by logical index', () => {
|
||||
const buf = new CircularBuffer([10, 20, 30]);
|
||||
|
||||
expect(buf.get(0)).toBe(10);
|
||||
expect(buf.get(1)).toBe(20);
|
||||
expect(buf.get(2)).toBe(30);
|
||||
});
|
||||
|
||||
it('return undefined for out-of-bounds', () => {
|
||||
const buf = new CircularBuffer([1, 2]);
|
||||
|
||||
expect(buf.get(-1)).toBeUndefined();
|
||||
expect(buf.get(2)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('work correctly after wrap-around', () => {
|
||||
const buf = new CircularBuffer<number>(undefined, 4);
|
||||
|
||||
buf.pushBack(1);
|
||||
buf.pushBack(2);
|
||||
buf.pushBack(3);
|
||||
buf.pushBack(4);
|
||||
buf.popFront();
|
||||
buf.popFront();
|
||||
buf.pushBack(5);
|
||||
buf.pushBack(6);
|
||||
|
||||
expect(buf.get(0)).toBe(3);
|
||||
expect(buf.get(1)).toBe(4);
|
||||
expect(buf.get(2)).toBe(5);
|
||||
expect(buf.get(3)).toBe(6);
|
||||
});
|
||||
});
|
||||
|
||||
describe('clear', () => {
|
||||
it('clear the buffer', () => {
|
||||
const buf = new CircularBuffer([1, 2, 3]);
|
||||
buf.clear();
|
||||
|
||||
expect(buf.length).toBe(0);
|
||||
expect(buf.isEmpty).toBe(true);
|
||||
});
|
||||
|
||||
it('return this for chaining', () => {
|
||||
const buf = new CircularBuffer([1]);
|
||||
|
||||
expect(buf.clear()).toBe(buf);
|
||||
});
|
||||
});
|
||||
|
||||
describe('auto-grow', () => {
|
||||
it('grow when capacity is exceeded', () => {
|
||||
const buf = new CircularBuffer<number>();
|
||||
const initialCapacity = buf.capacity;
|
||||
|
||||
for (let i = 0; i < initialCapacity + 1; i++)
|
||||
buf.pushBack(i);
|
||||
|
||||
expect(buf.length).toBe(initialCapacity + 1);
|
||||
expect(buf.capacity).toBe(initialCapacity * 2);
|
||||
});
|
||||
|
||||
it('preserve order after grow', () => {
|
||||
const buf = new CircularBuffer<number>(undefined, 4);
|
||||
|
||||
buf.pushBack(1);
|
||||
buf.pushBack(2);
|
||||
buf.popFront();
|
||||
buf.pushBack(3);
|
||||
buf.pushBack(4);
|
||||
buf.pushBack(5);
|
||||
buf.pushBack(6);
|
||||
|
||||
expect(buf.toArray()).toEqual([2, 3, 4, 5, 6]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('wrap-around', () => {
|
||||
it('handle wrap-around correctly', () => {
|
||||
const buf = new CircularBuffer<number>(undefined, 4);
|
||||
|
||||
buf.pushBack(1);
|
||||
buf.pushBack(2);
|
||||
buf.pushBack(3);
|
||||
buf.pushBack(4);
|
||||
buf.popFront();
|
||||
buf.popFront();
|
||||
buf.pushBack(5);
|
||||
buf.pushBack(6);
|
||||
|
||||
expect(buf.toArray()).toEqual([3, 4, 5, 6]);
|
||||
});
|
||||
|
||||
it('handle alternating front/back', () => {
|
||||
const buf = new CircularBuffer<number>();
|
||||
|
||||
buf.pushFront(3);
|
||||
buf.pushBack(4);
|
||||
buf.pushFront(2);
|
||||
buf.pushBack(5);
|
||||
buf.pushFront(1);
|
||||
|
||||
expect(buf.toArray()).toEqual([1, 2, 3, 4, 5]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toArray', () => {
|
||||
it('return elements front to back', () => {
|
||||
const buf = new CircularBuffer([1, 2, 3]);
|
||||
|
||||
expect(buf.toArray()).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
it('return empty array if empty', () => {
|
||||
const buf = new CircularBuffer<number>();
|
||||
|
||||
expect(buf.toArray()).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toString', () => {
|
||||
it('return comma-separated string', () => {
|
||||
const buf = new CircularBuffer([1, 2, 3]);
|
||||
|
||||
expect(buf.toString()).toBe('1,2,3');
|
||||
});
|
||||
});
|
||||
|
||||
describe('iteration', () => {
|
||||
it('iterate front to back', () => {
|
||||
const buf = new CircularBuffer([1, 2, 3]);
|
||||
|
||||
expect([...buf]).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
it('iterate asynchronously', async () => {
|
||||
const buf = new CircularBuffer([1, 2, 3]);
|
||||
const elements: number[] = [];
|
||||
|
||||
for await (const element of buf)
|
||||
elements.push(element);
|
||||
|
||||
expect(elements).toEqual([1, 2, 3]);
|
||||
});
|
||||
});
|
||||
});
|
||||
277
core/stdlib/src/structs/CircularBuffer/index.ts
Normal file
277
core/stdlib/src/structs/CircularBuffer/index.ts
Normal file
@@ -0,0 +1,277 @@
|
||||
import { isArray } from '../../types';
|
||||
import type { CircularBufferLike } from './types';
|
||||
|
||||
export type { CircularBufferLike } from './types';
|
||||
|
||||
const MIN_CAPACITY = 4;
|
||||
|
||||
/**
|
||||
* @name CircularBuffer
|
||||
* @category Data Structures
|
||||
* @description A circular (ring) buffer with automatic growth, O(1) push/pop on both ends
|
||||
*
|
||||
* @since 0.0.8
|
||||
*
|
||||
* @template T The type of elements stored in the buffer
|
||||
*/
|
||||
export class CircularBuffer<T> implements CircularBufferLike<T> {
|
||||
/**
|
||||
* The internal storage
|
||||
*
|
||||
* @private
|
||||
* @type {(T | undefined)[]}
|
||||
*/
|
||||
private buffer: (T | undefined)[];
|
||||
|
||||
/**
|
||||
* The index of the front element
|
||||
*
|
||||
* @private
|
||||
* @type {number}
|
||||
*/
|
||||
private head: number;
|
||||
|
||||
/**
|
||||
* The number of elements in the buffer
|
||||
*
|
||||
* @private
|
||||
* @type {number}
|
||||
*/
|
||||
private count: number;
|
||||
|
||||
/**
|
||||
* Creates an instance of CircularBuffer
|
||||
*
|
||||
* @param {(T[] | T)} [initialValues] The initial values to add to the buffer
|
||||
* @param {number} [initialCapacity] The initial capacity hint (rounded up to next power of two)
|
||||
*/
|
||||
constructor(initialValues?: T[] | T, initialCapacity?: number) {
|
||||
this.head = 0;
|
||||
this.count = 0;
|
||||
|
||||
const items = isArray(initialValues) ? initialValues : initialValues !== undefined ? [initialValues] : [];
|
||||
const requested = Math.max(items.length, initialCapacity ?? 0);
|
||||
const cap = Math.max(MIN_CAPACITY, nextPowerOfTwo(requested));
|
||||
|
||||
this.buffer = new Array(cap);
|
||||
|
||||
for (const item of items)
|
||||
this.pushBack(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of elements in the buffer
|
||||
* @returns {number}
|
||||
*/
|
||||
get length() {
|
||||
return this.count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current capacity of the buffer
|
||||
* @returns {number}
|
||||
*/
|
||||
get capacity() {
|
||||
return this.buffer.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the buffer is empty
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get isEmpty() {
|
||||
return this.count === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the buffer is at capacity (before auto-grow)
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get isFull() {
|
||||
return this.count === this.buffer.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an element to the back of the buffer
|
||||
* @param {T} element The element to add
|
||||
*/
|
||||
pushBack(element: T) {
|
||||
if (this.count === this.buffer.length)
|
||||
this.grow();
|
||||
|
||||
this.buffer[(this.head + this.count) & (this.buffer.length - 1)] = element;
|
||||
this.count++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an element to the front of the buffer
|
||||
* @param {T} element The element to add
|
||||
*/
|
||||
pushFront(element: T) {
|
||||
if (this.count === this.buffer.length)
|
||||
this.grow();
|
||||
|
||||
this.head = (this.head - 1 + this.buffer.length) & (this.buffer.length - 1);
|
||||
this.buffer[this.head] = element;
|
||||
this.count++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes and returns the back element
|
||||
* @returns {T | undefined} The back element, or undefined if empty
|
||||
*/
|
||||
popBack() {
|
||||
if (this.isEmpty)
|
||||
return undefined;
|
||||
|
||||
const index = (this.head + this.count - 1) & (this.buffer.length - 1);
|
||||
const element = this.buffer[index];
|
||||
|
||||
this.buffer[index] = undefined;
|
||||
this.count--;
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes and returns the front element
|
||||
* @returns {T | undefined} The front element, or undefined if empty
|
||||
*/
|
||||
popFront() {
|
||||
if (this.isEmpty)
|
||||
return undefined;
|
||||
|
||||
const element = this.buffer[this.head];
|
||||
|
||||
this.buffer[this.head] = undefined;
|
||||
this.head = (this.head + 1) & (this.buffer.length - 1);
|
||||
this.count--;
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the back element without removing it
|
||||
* @returns {T | undefined}
|
||||
*/
|
||||
peekBack() {
|
||||
if (this.isEmpty)
|
||||
return undefined;
|
||||
|
||||
return this.buffer[(this.head + this.count - 1) & (this.buffer.length - 1)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the front element without removing it
|
||||
* @returns {T | undefined}
|
||||
*/
|
||||
peekFront() {
|
||||
if (this.isEmpty)
|
||||
return undefined;
|
||||
|
||||
return this.buffer[this.head];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets element at logical index (0 = front)
|
||||
* @param {number} index The logical index
|
||||
* @returns {T | undefined}
|
||||
*/
|
||||
get(index: number) {
|
||||
if (index < 0 || index >= this.count)
|
||||
return undefined;
|
||||
|
||||
return this.buffer[(this.head + index) & (this.buffer.length - 1)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the buffer
|
||||
*
|
||||
* @returns {this}
|
||||
*/
|
||||
clear() {
|
||||
this.buffer = new Array(MIN_CAPACITY);
|
||||
this.head = 0;
|
||||
this.count = 0;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the buffer to an array from front to back
|
||||
*
|
||||
* @returns {T[]}
|
||||
*/
|
||||
toArray() {
|
||||
const result = new Array<T>(this.count);
|
||||
|
||||
for (let i = 0; i < this.count; i++)
|
||||
result[i] = this.buffer[(this.head + i) & (this.buffer.length - 1)] as T;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
toString() {
|
||||
return this.toArray().toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an iterator (front to back)
|
||||
*
|
||||
* @returns {IterableIterator<T>}
|
||||
*/
|
||||
[Symbol.iterator]() {
|
||||
return this.toArray()[Symbol.iterator]();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an async iterator (front to back)
|
||||
*
|
||||
* @returns {AsyncIterableIterator<T>}
|
||||
*/
|
||||
async *[Symbol.asyncIterator]() {
|
||||
for (const element of this)
|
||||
yield element;
|
||||
}
|
||||
|
||||
/**
|
||||
* Doubles the buffer capacity and linearizes elements
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private grow() {
|
||||
const newCapacity = this.buffer.length << 1;
|
||||
const newBuffer = new Array<T | undefined>(newCapacity);
|
||||
|
||||
for (let i = 0; i < this.count; i++)
|
||||
newBuffer[i] = this.buffer[(this.head + i) & (this.buffer.length - 1)];
|
||||
|
||||
this.buffer = newBuffer;
|
||||
this.head = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next power of two >= n
|
||||
*
|
||||
* @param {number} n
|
||||
* @returns {number}
|
||||
*/
|
||||
function nextPowerOfTwo(n: number): number {
|
||||
if (n <= 0)
|
||||
return 1;
|
||||
|
||||
n--;
|
||||
n |= n >> 1;
|
||||
n |= n >> 2;
|
||||
n |= n >> 4;
|
||||
n |= n >> 8;
|
||||
n |= n >> 16;
|
||||
|
||||
return n + 1;
|
||||
}
|
||||
17
core/stdlib/src/structs/CircularBuffer/types.ts
Normal file
17
core/stdlib/src/structs/CircularBuffer/types.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export interface CircularBufferLike<T> extends Iterable<T>, AsyncIterable<T> {
|
||||
readonly length: number;
|
||||
readonly capacity: number;
|
||||
readonly isEmpty: boolean;
|
||||
readonly isFull: boolean;
|
||||
|
||||
pushBack(element: T): void;
|
||||
pushFront(element: T): void;
|
||||
popBack(): T | undefined;
|
||||
popFront(): T | undefined;
|
||||
peekBack(): T | undefined;
|
||||
peekFront(): T | undefined;
|
||||
get(index: number): T | undefined;
|
||||
clear(): this;
|
||||
toArray(): T[];
|
||||
toString(): string;
|
||||
}
|
||||
288
core/stdlib/src/structs/Deque/index.test.ts
Normal file
288
core/stdlib/src/structs/Deque/index.test.ts
Normal file
@@ -0,0 +1,288 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { Deque } from '.';
|
||||
|
||||
describe('deque', () => {
|
||||
describe('constructor', () => {
|
||||
it('create an empty deque if no initial values are provided', () => {
|
||||
const deque = new Deque<number>();
|
||||
|
||||
expect(deque.length).toBe(0);
|
||||
expect(deque.isEmpty).toBe(true);
|
||||
});
|
||||
|
||||
it('create a deque with the provided initial values', () => {
|
||||
const deque = new Deque([1, 2, 3]);
|
||||
|
||||
expect(deque.length).toBe(3);
|
||||
expect(deque.peekFront()).toBe(1);
|
||||
expect(deque.peekBack()).toBe(3);
|
||||
});
|
||||
|
||||
it('create a deque with a single initial value', () => {
|
||||
const deque = new Deque(42);
|
||||
|
||||
expect(deque.length).toBe(1);
|
||||
expect(deque.peekFront()).toBe(42);
|
||||
});
|
||||
|
||||
it('create a deque with the provided options', () => {
|
||||
const deque = new Deque<number>(undefined, { maxSize: 5 });
|
||||
|
||||
expect(deque.length).toBe(0);
|
||||
expect(deque.isFull).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pushBack', () => {
|
||||
it('add an element to the back', () => {
|
||||
const deque = new Deque<number>();
|
||||
deque.pushBack(1).pushBack(2);
|
||||
|
||||
expect(deque.peekFront()).toBe(1);
|
||||
expect(deque.peekBack()).toBe(2);
|
||||
expect(deque.length).toBe(2);
|
||||
});
|
||||
|
||||
it('throw an error if the deque is full', () => {
|
||||
const deque = new Deque<number>(undefined, { maxSize: 1 });
|
||||
deque.pushBack(1);
|
||||
|
||||
expect(() => deque.pushBack(2)).toThrow(new RangeError('Deque is full'));
|
||||
});
|
||||
|
||||
it('return this for chaining', () => {
|
||||
const deque = new Deque<number>();
|
||||
|
||||
expect(deque.pushBack(1)).toBe(deque);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pushFront', () => {
|
||||
it('add an element to the front', () => {
|
||||
const deque = new Deque<number>();
|
||||
deque.pushFront(1).pushFront(2);
|
||||
|
||||
expect(deque.peekFront()).toBe(2);
|
||||
expect(deque.peekBack()).toBe(1);
|
||||
expect(deque.length).toBe(2);
|
||||
});
|
||||
|
||||
it('throw an error if the deque is full', () => {
|
||||
const deque = new Deque<number>(undefined, { maxSize: 1 });
|
||||
deque.pushFront(1);
|
||||
|
||||
expect(() => deque.pushFront(2)).toThrow(new RangeError('Deque is full'));
|
||||
});
|
||||
|
||||
it('return this for chaining', () => {
|
||||
const deque = new Deque<number>();
|
||||
|
||||
expect(deque.pushFront(1)).toBe(deque);
|
||||
});
|
||||
});
|
||||
|
||||
describe('popBack', () => {
|
||||
it('remove and return the back element', () => {
|
||||
const deque = new Deque([1, 2, 3]);
|
||||
|
||||
expect(deque.popBack()).toBe(3);
|
||||
expect(deque.length).toBe(2);
|
||||
});
|
||||
|
||||
it('return undefined if the deque is empty', () => {
|
||||
const deque = new Deque<number>();
|
||||
|
||||
expect(deque.popBack()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('popFront', () => {
|
||||
it('remove and return the front element', () => {
|
||||
const deque = new Deque([1, 2, 3]);
|
||||
|
||||
expect(deque.popFront()).toBe(1);
|
||||
expect(deque.length).toBe(2);
|
||||
});
|
||||
|
||||
it('return undefined if the deque is empty', () => {
|
||||
const deque = new Deque<number>();
|
||||
|
||||
expect(deque.popFront()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('peekBack', () => {
|
||||
it('return the back element without removing it', () => {
|
||||
const deque = new Deque([1, 2, 3]);
|
||||
|
||||
expect(deque.peekBack()).toBe(3);
|
||||
expect(deque.length).toBe(3);
|
||||
});
|
||||
|
||||
it('return undefined if the deque is empty', () => {
|
||||
const deque = new Deque<number>();
|
||||
|
||||
expect(deque.peekBack()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('peekFront', () => {
|
||||
it('return the front element without removing it', () => {
|
||||
const deque = new Deque([1, 2, 3]);
|
||||
|
||||
expect(deque.peekFront()).toBe(1);
|
||||
expect(deque.length).toBe(3);
|
||||
});
|
||||
|
||||
it('return undefined if the deque is empty', () => {
|
||||
const deque = new Deque<number>();
|
||||
|
||||
expect(deque.peekFront()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('clear', () => {
|
||||
it('clear the deque', () => {
|
||||
const deque = new Deque([1, 2, 3]);
|
||||
deque.clear();
|
||||
|
||||
expect(deque.length).toBe(0);
|
||||
expect(deque.isEmpty).toBe(true);
|
||||
});
|
||||
|
||||
it('return this for chaining', () => {
|
||||
const deque = new Deque([1, 2, 3]);
|
||||
|
||||
expect(deque.clear()).toBe(deque);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toArray', () => {
|
||||
it('return elements from front to back', () => {
|
||||
const deque = new Deque([1, 2, 3]);
|
||||
|
||||
expect(deque.toArray()).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
it('return correct order after mixed operations', () => {
|
||||
const deque = new Deque<number>();
|
||||
deque.pushBack(2);
|
||||
deque.pushBack(3);
|
||||
deque.pushFront(1);
|
||||
deque.pushFront(0);
|
||||
|
||||
expect(deque.toArray()).toEqual([0, 1, 2, 3]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toString', () => {
|
||||
it('return comma-separated string', () => {
|
||||
const deque = new Deque([1, 2, 3]);
|
||||
|
||||
expect(deque.toString()).toBe('1,2,3');
|
||||
});
|
||||
});
|
||||
|
||||
describe('iteration', () => {
|
||||
it('iterate in front-to-back order', () => {
|
||||
const deque = new Deque([1, 2, 3]);
|
||||
|
||||
expect([...deque]).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
it('iterate asynchronously', async () => {
|
||||
const deque = new Deque([1, 2, 3]);
|
||||
const elements: number[] = [];
|
||||
|
||||
for await (const element of deque)
|
||||
elements.push(element);
|
||||
|
||||
expect(elements).toEqual([1, 2, 3]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('circular buffer behavior', () => {
|
||||
it('handle wrap-around correctly', () => {
|
||||
const deque = new Deque<number>();
|
||||
|
||||
for (let i = 0; i < 4; i++)
|
||||
deque.pushBack(i);
|
||||
|
||||
deque.popFront();
|
||||
deque.popFront();
|
||||
deque.pushBack(4);
|
||||
deque.pushBack(5);
|
||||
|
||||
expect(deque.toArray()).toEqual([2, 3, 4, 5]);
|
||||
});
|
||||
|
||||
it('grow the buffer when needed', () => {
|
||||
const deque = new Deque<number>();
|
||||
|
||||
for (let i = 0; i < 100; i++)
|
||||
deque.pushBack(i);
|
||||
|
||||
expect(deque.length).toBe(100);
|
||||
expect(deque.peekFront()).toBe(0);
|
||||
expect(deque.peekBack()).toBe(99);
|
||||
});
|
||||
|
||||
it('handle alternating front/back operations', () => {
|
||||
const deque = new Deque<number>();
|
||||
|
||||
deque.pushFront(3);
|
||||
deque.pushBack(4);
|
||||
deque.pushFront(2);
|
||||
deque.pushBack(5);
|
||||
deque.pushFront(1);
|
||||
|
||||
expect(deque.toArray()).toEqual([1, 2, 3, 4, 5]);
|
||||
|
||||
expect(deque.popFront()).toBe(1);
|
||||
expect(deque.popBack()).toBe(5);
|
||||
expect(deque.toArray()).toEqual([2, 3, 4]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mixed operations', () => {
|
||||
it('use as a stack (LIFO)', () => {
|
||||
const deque = new Deque<number>();
|
||||
deque.pushBack(1).pushBack(2).pushBack(3);
|
||||
|
||||
expect(deque.popBack()).toBe(3);
|
||||
expect(deque.popBack()).toBe(2);
|
||||
expect(deque.popBack()).toBe(1);
|
||||
});
|
||||
|
||||
it('use as a queue (FIFO)', () => {
|
||||
const deque = new Deque<number>();
|
||||
deque.pushBack(1).pushBack(2).pushBack(3);
|
||||
|
||||
expect(deque.popFront()).toBe(1);
|
||||
expect(deque.popFront()).toBe(2);
|
||||
expect(deque.popFront()).toBe(3);
|
||||
});
|
||||
|
||||
it('reuse deque after clear', () => {
|
||||
const deque = new Deque([1, 2, 3]);
|
||||
deque.clear();
|
||||
deque.pushBack(4);
|
||||
|
||||
expect(deque.length).toBe(1);
|
||||
expect(deque.peekFront()).toBe(4);
|
||||
});
|
||||
|
||||
it('maxSize limits capacity', () => {
|
||||
const deque = new Deque<number>(undefined, { maxSize: 3 });
|
||||
deque.pushBack(1).pushBack(2).pushBack(3);
|
||||
|
||||
expect(deque.isFull).toBe(true);
|
||||
expect(() => deque.pushFront(0)).toThrow(new RangeError('Deque is full'));
|
||||
|
||||
deque.popFront();
|
||||
deque.pushFront(0);
|
||||
|
||||
expect(deque.toArray()).toEqual([0, 2, 3]);
|
||||
});
|
||||
});
|
||||
});
|
||||
180
core/stdlib/src/structs/Deque/index.ts
Normal file
180
core/stdlib/src/structs/Deque/index.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import { CircularBuffer } from '../CircularBuffer';
|
||||
import type { DequeLike } from './types';
|
||||
|
||||
export type { DequeLike } from './types';
|
||||
|
||||
export interface DequeOptions {
|
||||
maxSize?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @name Deque
|
||||
* @category Data Structures
|
||||
* @description Represents a double-ended queue backed by a circular buffer
|
||||
*
|
||||
* @since 0.0.8
|
||||
*
|
||||
* @template T The type of elements stored in the deque
|
||||
*/
|
||||
export class Deque<T> implements DequeLike<T> {
|
||||
/**
|
||||
* The maximum number of elements that the deque can hold
|
||||
*
|
||||
* @private
|
||||
* @type {number}
|
||||
*/
|
||||
private readonly maxSize: number;
|
||||
|
||||
/**
|
||||
* The underlying circular buffer
|
||||
*
|
||||
* @private
|
||||
* @type {CircularBuffer<T>}
|
||||
*/
|
||||
private readonly buffer: CircularBuffer<T>;
|
||||
|
||||
/**
|
||||
* Creates an instance of Deque
|
||||
*
|
||||
* @param {(T[] | T)} [initialValues] The initial values to add to the deque
|
||||
* @param {DequeOptions} [options] The options for the deque
|
||||
*/
|
||||
constructor(initialValues?: T[] | T, options?: DequeOptions) {
|
||||
this.maxSize = options?.maxSize ?? Infinity;
|
||||
this.buffer = new CircularBuffer(initialValues);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of elements in the deque
|
||||
* @returns {number} The number of elements in the deque
|
||||
*/
|
||||
get length() {
|
||||
return this.buffer.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the deque is empty
|
||||
* @returns {boolean} `true` if the deque is empty, `false` otherwise
|
||||
*/
|
||||
get isEmpty() {
|
||||
return this.buffer.isEmpty;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the deque is full
|
||||
* @returns {boolean} `true` if the deque is full, `false` otherwise
|
||||
*/
|
||||
get isFull() {
|
||||
return this.buffer.length === this.maxSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an element to the back of the deque
|
||||
* @param {T} element The element to add
|
||||
* @returns {this}
|
||||
* @throws {RangeError} If the deque is full
|
||||
*/
|
||||
pushBack(element: T) {
|
||||
if (this.isFull)
|
||||
throw new RangeError('Deque is full');
|
||||
|
||||
this.buffer.pushBack(element);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an element to the front of the deque
|
||||
* @param {T} element The element to add
|
||||
* @returns {this}
|
||||
* @throws {RangeError} If the deque is full
|
||||
*/
|
||||
pushFront(element: T) {
|
||||
if (this.isFull)
|
||||
throw new RangeError('Deque is full');
|
||||
|
||||
this.buffer.pushFront(element);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes and returns the back element of the deque
|
||||
* @returns {T | undefined} The back element, or undefined if empty
|
||||
*/
|
||||
popBack() {
|
||||
return this.buffer.popBack();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes and returns the front element of the deque
|
||||
* @returns {T | undefined} The front element, or undefined if empty
|
||||
*/
|
||||
popFront() {
|
||||
return this.buffer.popFront();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the back element without removing it
|
||||
* @returns {T | undefined} The back element, or undefined if empty
|
||||
*/
|
||||
peekBack() {
|
||||
return this.buffer.peekBack();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the front element without removing it
|
||||
* @returns {T | undefined} The front element, or undefined if empty
|
||||
*/
|
||||
peekFront() {
|
||||
return this.buffer.peekFront();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the deque
|
||||
*
|
||||
* @returns {this}
|
||||
*/
|
||||
clear() {
|
||||
this.buffer.clear();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the deque to an array from front to back
|
||||
*
|
||||
* @returns {T[]}
|
||||
*/
|
||||
toArray() {
|
||||
return this.buffer.toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of the deque
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
toString() {
|
||||
return this.buffer.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an iterator for the deque (front to back)
|
||||
*
|
||||
* @returns {IterableIterator<T>}
|
||||
*/
|
||||
[Symbol.iterator]() {
|
||||
return this.buffer[Symbol.iterator]();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an async iterator for the deque (front to back)
|
||||
*
|
||||
* @returns {AsyncIterableIterator<T>}
|
||||
*/
|
||||
async *[Symbol.asyncIterator]() {
|
||||
for (const element of this.buffer)
|
||||
yield element;
|
||||
}
|
||||
}
|
||||
15
core/stdlib/src/structs/Deque/types.ts
Normal file
15
core/stdlib/src/structs/Deque/types.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export interface DequeLike<T> extends Iterable<T>, AsyncIterable<T> {
|
||||
readonly length: number;
|
||||
readonly isEmpty: boolean;
|
||||
readonly isFull: boolean;
|
||||
|
||||
pushBack(element: T): this;
|
||||
pushFront(element: T): this;
|
||||
popBack(): T | undefined;
|
||||
popFront(): T | undefined;
|
||||
peekBack(): T | undefined;
|
||||
peekFront(): T | undefined;
|
||||
clear(): this;
|
||||
toArray(): T[];
|
||||
toString(): string;
|
||||
}
|
||||
406
core/stdlib/src/structs/LinkedList/index.test.ts
Normal file
406
core/stdlib/src/structs/LinkedList/index.test.ts
Normal file
@@ -0,0 +1,406 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { LinkedList } from '.';
|
||||
|
||||
describe('LinkedList', () => {
|
||||
describe('constructor', () => {
|
||||
it('should create an empty list', () => {
|
||||
const list = new LinkedList<number>();
|
||||
|
||||
expect(list.length).toBe(0);
|
||||
expect(list.isEmpty).toBe(true);
|
||||
expect(list.head).toBeUndefined();
|
||||
expect(list.tail).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should create a list from single value', () => {
|
||||
const list = new LinkedList(42);
|
||||
|
||||
expect(list.length).toBe(1);
|
||||
expect(list.peekFront()).toBe(42);
|
||||
expect(list.peekBack()).toBe(42);
|
||||
});
|
||||
|
||||
it('should create a list from array', () => {
|
||||
const list = new LinkedList([1, 2, 3]);
|
||||
|
||||
expect(list.length).toBe(3);
|
||||
expect(list.peekFront()).toBe(1);
|
||||
expect(list.peekBack()).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pushBack', () => {
|
||||
it('should append to empty list', () => {
|
||||
const list = new LinkedList<number>();
|
||||
|
||||
const node = list.pushBack(1);
|
||||
|
||||
expect(list.length).toBe(1);
|
||||
expect(node.value).toBe(1);
|
||||
expect(list.head).toBe(node);
|
||||
expect(list.tail).toBe(node);
|
||||
});
|
||||
|
||||
it('should append to non-empty list', () => {
|
||||
const list = new LinkedList([1, 2]);
|
||||
|
||||
list.pushBack(3);
|
||||
|
||||
expect(list.length).toBe(3);
|
||||
expect(list.peekBack()).toBe(3);
|
||||
expect(list.peekFront()).toBe(1);
|
||||
});
|
||||
|
||||
it('should return the created node', () => {
|
||||
const list = new LinkedList<number>();
|
||||
|
||||
const node = list.pushBack(5);
|
||||
|
||||
expect(node.value).toBe(5);
|
||||
expect(node.prev).toBeUndefined();
|
||||
expect(node.next).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('pushFront', () => {
|
||||
it('should prepend to empty list', () => {
|
||||
const list = new LinkedList<number>();
|
||||
|
||||
const node = list.pushFront(1);
|
||||
|
||||
expect(list.length).toBe(1);
|
||||
expect(list.head).toBe(node);
|
||||
expect(list.tail).toBe(node);
|
||||
});
|
||||
|
||||
it('should prepend to non-empty list', () => {
|
||||
const list = new LinkedList([2, 3]);
|
||||
|
||||
list.pushFront(1);
|
||||
|
||||
expect(list.length).toBe(3);
|
||||
expect(list.peekFront()).toBe(1);
|
||||
expect(list.peekBack()).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('popBack', () => {
|
||||
it('should return undefined for empty list', () => {
|
||||
const list = new LinkedList<number>();
|
||||
|
||||
expect(list.popBack()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should remove and return last value', () => {
|
||||
const list = new LinkedList([1, 2, 3]);
|
||||
|
||||
expect(list.popBack()).toBe(3);
|
||||
expect(list.length).toBe(2);
|
||||
expect(list.peekBack()).toBe(2);
|
||||
});
|
||||
|
||||
it('should handle single element', () => {
|
||||
const list = new LinkedList(1);
|
||||
|
||||
expect(list.popBack()).toBe(1);
|
||||
expect(list.isEmpty).toBe(true);
|
||||
expect(list.head).toBeUndefined();
|
||||
expect(list.tail).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('popFront', () => {
|
||||
it('should return undefined for empty list', () => {
|
||||
const list = new LinkedList<number>();
|
||||
|
||||
expect(list.popFront()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should remove and return first value', () => {
|
||||
const list = new LinkedList([1, 2, 3]);
|
||||
|
||||
expect(list.popFront()).toBe(1);
|
||||
expect(list.length).toBe(2);
|
||||
expect(list.peekFront()).toBe(2);
|
||||
});
|
||||
|
||||
it('should handle single element', () => {
|
||||
const list = new LinkedList(1);
|
||||
|
||||
expect(list.popFront()).toBe(1);
|
||||
expect(list.isEmpty).toBe(true);
|
||||
expect(list.head).toBeUndefined();
|
||||
expect(list.tail).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('peekBack', () => {
|
||||
it('should return undefined for empty list', () => {
|
||||
const list = new LinkedList<number>();
|
||||
|
||||
expect(list.peekBack()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return last value without removing', () => {
|
||||
const list = new LinkedList([1, 2, 3]);
|
||||
|
||||
expect(list.peekBack()).toBe(3);
|
||||
expect(list.length).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('peekFront', () => {
|
||||
it('should return undefined for empty list', () => {
|
||||
const list = new LinkedList<number>();
|
||||
|
||||
expect(list.peekFront()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return first value without removing', () => {
|
||||
const list = new LinkedList([1, 2, 3]);
|
||||
|
||||
expect(list.peekFront()).toBe(1);
|
||||
expect(list.length).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('insertBefore', () => {
|
||||
it('should insert before head', () => {
|
||||
const list = new LinkedList<number>();
|
||||
const node = list.pushBack(2);
|
||||
|
||||
list.insertBefore(node, 1);
|
||||
|
||||
expect(list.peekFront()).toBe(1);
|
||||
expect(list.peekBack()).toBe(2);
|
||||
expect(list.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should insert before middle node', () => {
|
||||
const list = new LinkedList([1, 3]);
|
||||
const tail = list.tail!;
|
||||
|
||||
list.insertBefore(tail, 2);
|
||||
|
||||
expect(list.toArray()).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
it('should return the created node', () => {
|
||||
const list = new LinkedList<number>();
|
||||
const existing = list.pushBack(2);
|
||||
|
||||
const newNode = list.insertBefore(existing, 1);
|
||||
|
||||
expect(newNode.value).toBe(1);
|
||||
expect(newNode.next).toBe(existing);
|
||||
});
|
||||
});
|
||||
|
||||
describe('insertAfter', () => {
|
||||
it('should insert after tail', () => {
|
||||
const list = new LinkedList<number>();
|
||||
const node = list.pushBack(1);
|
||||
|
||||
list.insertAfter(node, 2);
|
||||
|
||||
expect(list.peekFront()).toBe(1);
|
||||
expect(list.peekBack()).toBe(2);
|
||||
expect(list.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should insert after middle node', () => {
|
||||
const list = new LinkedList([1, 3]);
|
||||
const head = list.head!;
|
||||
|
||||
list.insertAfter(head, 2);
|
||||
|
||||
expect(list.toArray()).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
it('should return the created node', () => {
|
||||
const list = new LinkedList<number>();
|
||||
const existing = list.pushBack(1);
|
||||
|
||||
const newNode = list.insertAfter(existing, 2);
|
||||
|
||||
expect(newNode.value).toBe(2);
|
||||
expect(newNode.prev).toBe(existing);
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
it('should remove head node', () => {
|
||||
const list = new LinkedList([1, 2, 3]);
|
||||
const head = list.head!;
|
||||
|
||||
const value = list.remove(head);
|
||||
|
||||
expect(value).toBe(1);
|
||||
expect(list.length).toBe(2);
|
||||
expect(list.peekFront()).toBe(2);
|
||||
});
|
||||
|
||||
it('should remove tail node', () => {
|
||||
const list = new LinkedList([1, 2, 3]);
|
||||
const tail = list.tail!;
|
||||
|
||||
const value = list.remove(tail);
|
||||
|
||||
expect(value).toBe(3);
|
||||
expect(list.length).toBe(2);
|
||||
expect(list.peekBack()).toBe(2);
|
||||
});
|
||||
|
||||
it('should remove middle node', () => {
|
||||
const list = new LinkedList([1, 2, 3]);
|
||||
const middle = list.head!.next!;
|
||||
|
||||
const value = list.remove(middle);
|
||||
|
||||
expect(value).toBe(2);
|
||||
expect(list.toArray()).toEqual([1, 3]);
|
||||
});
|
||||
|
||||
it('should remove single element', () => {
|
||||
const list = new LinkedList<number>();
|
||||
const node = list.pushBack(1);
|
||||
|
||||
list.remove(node);
|
||||
|
||||
expect(list.isEmpty).toBe(true);
|
||||
expect(list.head).toBeUndefined();
|
||||
expect(list.tail).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should detach the removed node', () => {
|
||||
const list = new LinkedList([1, 2, 3]);
|
||||
const middle = list.head!.next!;
|
||||
|
||||
list.remove(middle);
|
||||
|
||||
expect(middle.prev).toBeUndefined();
|
||||
expect(middle.next).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('clear', () => {
|
||||
it('should remove all elements', () => {
|
||||
const list = new LinkedList([1, 2, 3]);
|
||||
|
||||
const result = list.clear();
|
||||
|
||||
expect(list.length).toBe(0);
|
||||
expect(list.isEmpty).toBe(true);
|
||||
expect(list.head).toBeUndefined();
|
||||
expect(list.tail).toBeUndefined();
|
||||
expect(result).toBe(list);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toArray', () => {
|
||||
it('should return empty array for empty list', () => {
|
||||
const list = new LinkedList<number>();
|
||||
|
||||
expect(list.toArray()).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return values from head to tail', () => {
|
||||
const list = new LinkedList([1, 2, 3]);
|
||||
|
||||
expect(list.toArray()).toEqual([1, 2, 3]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toString', () => {
|
||||
it('should return comma-separated values', () => {
|
||||
const list = new LinkedList([1, 2, 3]);
|
||||
|
||||
expect(list.toString()).toBe('1,2,3');
|
||||
});
|
||||
});
|
||||
|
||||
describe('iterator', () => {
|
||||
it('should iterate from head to tail', () => {
|
||||
const list = new LinkedList([1, 2, 3]);
|
||||
|
||||
expect([...list]).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
it('should yield nothing for empty list', () => {
|
||||
const list = new LinkedList<number>();
|
||||
|
||||
expect([...list]).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('async iterator', () => {
|
||||
it('should async iterate from head to tail', async () => {
|
||||
const list = new LinkedList([1, 2, 3]);
|
||||
const result: number[] = [];
|
||||
|
||||
for await (const value of list)
|
||||
result.push(value);
|
||||
|
||||
expect(result).toEqual([1, 2, 3]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('node linking', () => {
|
||||
it('should maintain correct prev/next references', () => {
|
||||
const list = new LinkedList<number>();
|
||||
const a = list.pushBack(1);
|
||||
const b = list.pushBack(2);
|
||||
const c = list.pushBack(3);
|
||||
|
||||
expect(a.next).toBe(b);
|
||||
expect(b.prev).toBe(a);
|
||||
expect(b.next).toBe(c);
|
||||
expect(c.prev).toBe(b);
|
||||
expect(a.prev).toBeUndefined();
|
||||
expect(c.next).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should update links after removal', () => {
|
||||
const list = new LinkedList<number>();
|
||||
const a = list.pushBack(1);
|
||||
const b = list.pushBack(2);
|
||||
const c = list.pushBack(3);
|
||||
|
||||
list.remove(b);
|
||||
|
||||
expect(a.next).toBe(c);
|
||||
expect(c.prev).toBe(a);
|
||||
});
|
||||
});
|
||||
|
||||
describe('interleaved operations', () => {
|
||||
it('should handle mixed push/pop from both ends', () => {
|
||||
const list = new LinkedList<number>();
|
||||
|
||||
list.pushBack(1);
|
||||
list.pushBack(2);
|
||||
list.pushFront(0);
|
||||
|
||||
expect(list.popFront()).toBe(0);
|
||||
expect(list.popBack()).toBe(2);
|
||||
expect(list.popFront()).toBe(1);
|
||||
expect(list.isEmpty).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle insert and remove by node reference', () => {
|
||||
const list = new LinkedList<number>();
|
||||
const a = list.pushBack(1);
|
||||
const c = list.pushBack(3);
|
||||
const b = list.insertAfter(a, 2);
|
||||
const d = list.insertBefore(c, 2.5);
|
||||
|
||||
expect(list.toArray()).toEqual([1, 2, 2.5, 3]);
|
||||
|
||||
list.remove(b);
|
||||
list.remove(d);
|
||||
|
||||
expect(list.toArray()).toEqual([1, 3]);
|
||||
});
|
||||
});
|
||||
});
|
||||
324
core/stdlib/src/structs/LinkedList/index.ts
Normal file
324
core/stdlib/src/structs/LinkedList/index.ts
Normal file
@@ -0,0 +1,324 @@
|
||||
import { isArray } from '../../types';
|
||||
import type { LinkedListLike, LinkedListNode } from './types';
|
||||
|
||||
export type { LinkedListLike, LinkedListNode } from './types';
|
||||
|
||||
/**
|
||||
* Creates a new doubly linked list node
|
||||
*
|
||||
* @template T The type of the value
|
||||
* @param {T} value The value to store
|
||||
* @returns {LinkedListNode<T>} The created node
|
||||
*/
|
||||
function createNode<T>(value: T): LinkedListNode<T> {
|
||||
return { value, prev: undefined, next: undefined };
|
||||
}
|
||||
|
||||
/**
|
||||
* @name LinkedList
|
||||
* @category Data Structures
|
||||
* @description Doubly linked list with O(1) push/pop on both ends and O(1) insert/remove by node reference
|
||||
*
|
||||
* @since 0.0.8
|
||||
*
|
||||
* @template T The type of elements stored in the list
|
||||
*/
|
||||
export class LinkedList<T> implements LinkedListLike<T> {
|
||||
/**
|
||||
* The number of elements in the list
|
||||
*
|
||||
* @private
|
||||
* @type {number}
|
||||
*/
|
||||
private count: number = 0;
|
||||
|
||||
/**
|
||||
* The first node in the list
|
||||
*
|
||||
* @private
|
||||
* @type {LinkedListNode<T> | undefined}
|
||||
*/
|
||||
private first: LinkedListNode<T> | undefined;
|
||||
|
||||
/**
|
||||
* The last node in the list
|
||||
*
|
||||
* @private
|
||||
* @type {LinkedListNode<T> | undefined}
|
||||
*/
|
||||
private last: LinkedListNode<T> | undefined;
|
||||
|
||||
/**
|
||||
* Creates an instance of LinkedList
|
||||
*
|
||||
* @param {(T[] | T)} [initialValues] The initial values to add to the list
|
||||
*/
|
||||
constructor(initialValues?: T[] | T) {
|
||||
if (initialValues != null) {
|
||||
const items = isArray(initialValues) ? initialValues : [initialValues];
|
||||
|
||||
for (const item of items)
|
||||
this.pushBack(item);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of elements in the list
|
||||
* @returns {number} The number of elements in the list
|
||||
*/
|
||||
public get length(): number {
|
||||
return this.count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the list is empty
|
||||
* @returns {boolean} `true` if the list is empty, `false` otherwise
|
||||
*/
|
||||
public get isEmpty(): boolean {
|
||||
return this.count === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the first node
|
||||
* @returns {LinkedListNode<T> | undefined} The first node, or `undefined` if the list is empty
|
||||
*/
|
||||
public get head(): LinkedListNode<T> | undefined {
|
||||
return this.first;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the last node
|
||||
* @returns {LinkedListNode<T> | undefined} The last node, or `undefined` if the list is empty
|
||||
*/
|
||||
public get tail(): LinkedListNode<T> | undefined {
|
||||
return this.last;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a value to the end of the list
|
||||
* @param {T} value The value to append
|
||||
* @returns {LinkedListNode<T>} The created node
|
||||
*/
|
||||
public pushBack(value: T): LinkedListNode<T> {
|
||||
const node = createNode(value);
|
||||
|
||||
if (this.last) {
|
||||
node.prev = this.last;
|
||||
this.last.next = node;
|
||||
this.last = node;
|
||||
} else {
|
||||
this.first = node;
|
||||
this.last = node;
|
||||
}
|
||||
|
||||
this.count++;
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepends a value to the beginning of the list
|
||||
* @param {T} value The value to prepend
|
||||
* @returns {LinkedListNode<T>} The created node
|
||||
*/
|
||||
public pushFront(value: T): LinkedListNode<T> {
|
||||
const node = createNode(value);
|
||||
|
||||
if (this.first) {
|
||||
node.next = this.first;
|
||||
this.first.prev = node;
|
||||
this.first = node;
|
||||
} else {
|
||||
this.first = node;
|
||||
this.last = node;
|
||||
}
|
||||
|
||||
this.count++;
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes and returns the last value
|
||||
* @returns {T | undefined} The last value, or `undefined` if the list is empty
|
||||
*/
|
||||
public popBack(): T | undefined {
|
||||
if (!this.last) return undefined;
|
||||
|
||||
const node = this.last;
|
||||
|
||||
this.detach(node);
|
||||
|
||||
return node.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes and returns the first value
|
||||
* @returns {T | undefined} The first value, or `undefined` if the list is empty
|
||||
*/
|
||||
public popFront(): T | undefined {
|
||||
if (!this.first) return undefined;
|
||||
|
||||
const node = this.first;
|
||||
|
||||
this.detach(node);
|
||||
|
||||
return node.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last value without removing it
|
||||
* @returns {T | undefined} The last value, or `undefined` if the list is empty
|
||||
*/
|
||||
public peekBack(): T | undefined {
|
||||
return this.last?.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first value without removing it
|
||||
* @returns {T | undefined} The first value, or `undefined` if the list is empty
|
||||
*/
|
||||
public peekFront(): T | undefined {
|
||||
return this.first?.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a value before the given node
|
||||
* @param {LinkedListNode<T>} node The reference node
|
||||
* @param {T} value The value to insert
|
||||
* @returns {LinkedListNode<T>} The created node
|
||||
*/
|
||||
public insertBefore(node: LinkedListNode<T>, value: T): LinkedListNode<T> {
|
||||
const newNode = createNode(value);
|
||||
|
||||
newNode.next = node;
|
||||
newNode.prev = node.prev;
|
||||
|
||||
if (node.prev) {
|
||||
node.prev.next = newNode;
|
||||
} else {
|
||||
this.first = newNode;
|
||||
}
|
||||
|
||||
node.prev = newNode;
|
||||
this.count++;
|
||||
|
||||
return newNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a value after the given node
|
||||
* @param {LinkedListNode<T>} node The reference node
|
||||
* @param {T} value The value to insert
|
||||
* @returns {LinkedListNode<T>} The created node
|
||||
*/
|
||||
public insertAfter(node: LinkedListNode<T>, value: T): LinkedListNode<T> {
|
||||
const newNode = createNode(value);
|
||||
|
||||
newNode.prev = node;
|
||||
newNode.next = node.next;
|
||||
|
||||
if (node.next) {
|
||||
node.next.prev = newNode;
|
||||
} else {
|
||||
this.last = newNode;
|
||||
}
|
||||
|
||||
node.next = newNode;
|
||||
this.count++;
|
||||
|
||||
return newNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a node from the list by reference in O(1)
|
||||
* @param {LinkedListNode<T>} node The node to remove
|
||||
* @returns {T} The value of the removed node
|
||||
*/
|
||||
public remove(node: LinkedListNode<T>): T {
|
||||
this.detach(node);
|
||||
|
||||
return node.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all elements from the list
|
||||
* @returns {this} The list instance for chaining
|
||||
*/
|
||||
public clear(): this {
|
||||
this.first = undefined;
|
||||
this.last = undefined;
|
||||
this.count = 0;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a shallow copy of the list values as an array
|
||||
* @returns {T[]} Array of values from head to tail
|
||||
*/
|
||||
public toArray(): T[] {
|
||||
const result = new Array<T>(this.count);
|
||||
let current = this.first;
|
||||
let i = 0;
|
||||
|
||||
while (current) {
|
||||
result[i++] = current.value;
|
||||
current = current.next;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of the list
|
||||
* @returns {string} String representation
|
||||
*/
|
||||
public toString(): string {
|
||||
return this.toArray().toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterator over list values from head to tail
|
||||
*/
|
||||
public *[Symbol.iterator](): Iterator<T> {
|
||||
let current = this.first;
|
||||
|
||||
while (current) {
|
||||
yield current.value;
|
||||
current = current.next;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Async iterator over list values from head to tail
|
||||
*/
|
||||
public async *[Symbol.asyncIterator](): AsyncIterator<T> {
|
||||
for (const value of this)
|
||||
yield value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detaches a node from the list, updating head/tail and count
|
||||
*
|
||||
* @private
|
||||
* @param {LinkedListNode<T>} node The node to detach
|
||||
*/
|
||||
private detach(node: LinkedListNode<T>): void {
|
||||
if (node.prev) {
|
||||
node.prev.next = node.next;
|
||||
} else {
|
||||
this.first = node.next;
|
||||
}
|
||||
|
||||
if (node.next) {
|
||||
node.next.prev = node.prev;
|
||||
} else {
|
||||
this.last = node.prev;
|
||||
}
|
||||
|
||||
node.prev = undefined;
|
||||
node.next = undefined;
|
||||
this.count--;
|
||||
}
|
||||
}
|
||||
28
core/stdlib/src/structs/LinkedList/types.ts
Normal file
28
core/stdlib/src/structs/LinkedList/types.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
export interface LinkedListNode<T> {
|
||||
value: T;
|
||||
prev: LinkedListNode<T> | undefined;
|
||||
next: LinkedListNode<T> | undefined;
|
||||
}
|
||||
|
||||
export interface LinkedListLike<T> extends Iterable<T>, AsyncIterable<T> {
|
||||
readonly length: number;
|
||||
readonly isEmpty: boolean;
|
||||
|
||||
readonly head: LinkedListNode<T> | undefined;
|
||||
readonly tail: LinkedListNode<T> | undefined;
|
||||
|
||||
pushBack(value: T): LinkedListNode<T>;
|
||||
pushFront(value: T): LinkedListNode<T>;
|
||||
popBack(): T | undefined;
|
||||
popFront(): T | undefined;
|
||||
peekBack(): T | undefined;
|
||||
peekFront(): T | undefined;
|
||||
|
||||
insertBefore(node: LinkedListNode<T>, value: T): LinkedListNode<T>;
|
||||
insertAfter(node: LinkedListNode<T>, value: T): LinkedListNode<T>;
|
||||
remove(node: LinkedListNode<T>): T;
|
||||
|
||||
clear(): this;
|
||||
toArray(): T[];
|
||||
toString(): string;
|
||||
}
|
||||
213
core/stdlib/src/structs/PriorityQueue/index.test.ts
Normal file
213
core/stdlib/src/structs/PriorityQueue/index.test.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { PriorityQueue } from '.';
|
||||
|
||||
describe('PriorityQueue', () => {
|
||||
describe('constructor', () => {
|
||||
it('should create an empty queue', () => {
|
||||
const pq = new PriorityQueue<number>();
|
||||
|
||||
expect(pq.length).toBe(0);
|
||||
expect(pq.isEmpty).toBe(true);
|
||||
expect(pq.isFull).toBe(false);
|
||||
});
|
||||
|
||||
it('should create a queue from single value', () => {
|
||||
const pq = new PriorityQueue(42);
|
||||
|
||||
expect(pq.length).toBe(1);
|
||||
expect(pq.peek()).toBe(42);
|
||||
});
|
||||
|
||||
it('should create a queue from array', () => {
|
||||
const pq = new PriorityQueue([5, 3, 8, 1, 4]);
|
||||
|
||||
expect(pq.length).toBe(5);
|
||||
expect(pq.peek()).toBe(1);
|
||||
});
|
||||
|
||||
it('should throw if initial values exceed maxSize', () => {
|
||||
expect(() => new PriorityQueue([1, 2, 3], { maxSize: 2 }))
|
||||
.toThrow('Initial values exceed maxSize');
|
||||
});
|
||||
});
|
||||
|
||||
describe('enqueue', () => {
|
||||
it('should enqueue elements by priority', () => {
|
||||
const pq = new PriorityQueue<number>();
|
||||
|
||||
pq.enqueue(5);
|
||||
pq.enqueue(1);
|
||||
pq.enqueue(3);
|
||||
|
||||
expect(pq.peek()).toBe(1);
|
||||
expect(pq.length).toBe(3);
|
||||
});
|
||||
|
||||
it('should throw when queue is full', () => {
|
||||
const pq = new PriorityQueue<number>(undefined, { maxSize: 2 });
|
||||
|
||||
pq.enqueue(1);
|
||||
pq.enqueue(2);
|
||||
|
||||
expect(() => pq.enqueue(3)).toThrow('PriorityQueue is full');
|
||||
});
|
||||
});
|
||||
|
||||
describe('dequeue', () => {
|
||||
it('should return undefined for empty queue', () => {
|
||||
const pq = new PriorityQueue<number>();
|
||||
|
||||
expect(pq.dequeue()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should dequeue elements in priority order (min-heap)', () => {
|
||||
const pq = new PriorityQueue([5, 3, 8, 1, 4]);
|
||||
const result: number[] = [];
|
||||
|
||||
while (!pq.isEmpty) {
|
||||
result.push(pq.dequeue()!);
|
||||
}
|
||||
|
||||
expect(result).toEqual([1, 3, 4, 5, 8]);
|
||||
});
|
||||
|
||||
it('should dequeue elements in priority order (max-heap)', () => {
|
||||
const pq = new PriorityQueue([5, 3, 8, 1, 4], {
|
||||
comparator: (a, b) => b - a,
|
||||
});
|
||||
const result: number[] = [];
|
||||
|
||||
while (!pq.isEmpty) {
|
||||
result.push(pq.dequeue()!);
|
||||
}
|
||||
|
||||
expect(result).toEqual([8, 5, 4, 3, 1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('peek', () => {
|
||||
it('should return undefined for empty queue', () => {
|
||||
const pq = new PriorityQueue<number>();
|
||||
|
||||
expect(pq.peek()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return highest-priority element without removing', () => {
|
||||
const pq = new PriorityQueue([5, 1, 3]);
|
||||
|
||||
expect(pq.peek()).toBe(1);
|
||||
expect(pq.length).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isFull', () => {
|
||||
it('should be false when no maxSize', () => {
|
||||
const pq = new PriorityQueue([1, 2, 3]);
|
||||
|
||||
expect(pq.isFull).toBe(false);
|
||||
});
|
||||
|
||||
it('should be true when at maxSize', () => {
|
||||
const pq = new PriorityQueue([1, 2], { maxSize: 2 });
|
||||
|
||||
expect(pq.isFull).toBe(true);
|
||||
});
|
||||
|
||||
it('should become false after dequeue', () => {
|
||||
const pq = new PriorityQueue([1, 2], { maxSize: 2 });
|
||||
|
||||
pq.dequeue();
|
||||
|
||||
expect(pq.isFull).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('clear', () => {
|
||||
it('should remove all elements', () => {
|
||||
const pq = new PriorityQueue([1, 2, 3]);
|
||||
|
||||
const result = pq.clear();
|
||||
|
||||
expect(pq.length).toBe(0);
|
||||
expect(pq.isEmpty).toBe(true);
|
||||
expect(result).toBe(pq);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toArray', () => {
|
||||
it('should return empty array for empty queue', () => {
|
||||
const pq = new PriorityQueue<number>();
|
||||
|
||||
expect(pq.toArray()).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return a shallow copy', () => {
|
||||
const pq = new PriorityQueue([3, 1, 2]);
|
||||
const arr = pq.toArray();
|
||||
|
||||
arr.push(99);
|
||||
|
||||
expect(pq.length).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toString', () => {
|
||||
it('should return formatted string', () => {
|
||||
const pq = new PriorityQueue([1, 2, 3]);
|
||||
|
||||
expect(pq.toString()).toBe('PriorityQueue(3)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('iterator', () => {
|
||||
it('should iterate over elements', () => {
|
||||
const pq = new PriorityQueue([5, 3, 1]);
|
||||
const elements = [...pq];
|
||||
|
||||
expect(elements.length).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('custom comparator', () => {
|
||||
it('should work with object priority', () => {
|
||||
interface Job {
|
||||
priority: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const pq = new PriorityQueue<Job>(
|
||||
[
|
||||
{ priority: 3, name: 'low' },
|
||||
{ priority: 1, name: 'critical' },
|
||||
{ priority: 2, name: 'normal' },
|
||||
],
|
||||
{ comparator: (a, b) => a.priority - b.priority },
|
||||
);
|
||||
|
||||
expect(pq.dequeue()?.name).toBe('critical');
|
||||
expect(pq.dequeue()?.name).toBe('normal');
|
||||
expect(pq.dequeue()?.name).toBe('low');
|
||||
});
|
||||
});
|
||||
|
||||
describe('interleaved operations', () => {
|
||||
it('should maintain priority with mixed enqueue and dequeue', () => {
|
||||
const pq = new PriorityQueue<number>();
|
||||
|
||||
pq.enqueue(10);
|
||||
pq.enqueue(5);
|
||||
expect(pq.dequeue()).toBe(5);
|
||||
|
||||
pq.enqueue(3);
|
||||
pq.enqueue(7);
|
||||
expect(pq.dequeue()).toBe(3);
|
||||
|
||||
pq.enqueue(1);
|
||||
expect(pq.dequeue()).toBe(1);
|
||||
expect(pq.dequeue()).toBe(7);
|
||||
expect(pq.dequeue()).toBe(10);
|
||||
expect(pq.dequeue()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
144
core/stdlib/src/structs/PriorityQueue/index.ts
Normal file
144
core/stdlib/src/structs/PriorityQueue/index.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import { BinaryHeap } from '../BinaryHeap';
|
||||
import type { Comparator, PriorityQueueLike } from './types';
|
||||
|
||||
export type { PriorityQueueLike } from './types';
|
||||
export type { Comparator } from './types';
|
||||
|
||||
export interface PriorityQueueOptions<T> {
|
||||
comparator?: Comparator<T>;
|
||||
maxSize?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @name PriorityQueue
|
||||
* @category Data Structures
|
||||
* @description Priority queue backed by a binary heap with configurable comparator and optional max size
|
||||
*
|
||||
* @since 0.0.8
|
||||
*
|
||||
* @template T The type of elements stored in the queue
|
||||
*/
|
||||
export class PriorityQueue<T> implements PriorityQueueLike<T> {
|
||||
/**
|
||||
* The maximum number of elements the queue can hold
|
||||
*
|
||||
* @private
|
||||
* @type {number}
|
||||
*/
|
||||
private readonly maxSize: number;
|
||||
|
||||
/**
|
||||
* Internal binary heap backing the queue
|
||||
*
|
||||
* @private
|
||||
* @type {BinaryHeap<T>}
|
||||
*/
|
||||
private readonly heap: BinaryHeap<T>;
|
||||
|
||||
/**
|
||||
* Creates an instance of PriorityQueue
|
||||
*
|
||||
* @param {(T[] | T)} [initialValues] The initial values to add to the queue
|
||||
* @param {PriorityQueueOptions<T>} [options] Queue configuration
|
||||
*/
|
||||
constructor(initialValues?: T[] | T, options?: PriorityQueueOptions<T>) {
|
||||
this.maxSize = options?.maxSize ?? Infinity;
|
||||
this.heap = new BinaryHeap(initialValues, { comparator: options?.comparator });
|
||||
|
||||
if (this.heap.length > this.maxSize) {
|
||||
throw new RangeError('Initial values exceed maxSize');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of elements in the queue
|
||||
* @returns {number} The number of elements in the queue
|
||||
*/
|
||||
public get length(): number {
|
||||
return this.heap.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the queue is empty
|
||||
* @returns {boolean} `true` if the queue is empty, `false` otherwise
|
||||
*/
|
||||
public get isEmpty(): boolean {
|
||||
return this.heap.isEmpty;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the queue is full
|
||||
* @returns {boolean} `true` if the queue has reached maxSize, `false` otherwise
|
||||
*/
|
||||
public get isFull(): boolean {
|
||||
return this.heap.length >= this.maxSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueues an element by priority
|
||||
* @param {T} element The element to enqueue
|
||||
* @throws {RangeError} If the queue is full
|
||||
*/
|
||||
public enqueue(element: T): void {
|
||||
if (this.isFull)
|
||||
throw new RangeError('PriorityQueue is full');
|
||||
|
||||
this.heap.push(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dequeues the highest-priority element
|
||||
* @returns {T | undefined} The highest-priority element, or `undefined` if empty
|
||||
*/
|
||||
public dequeue(): T | undefined {
|
||||
return this.heap.pop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the highest-priority element without removing it
|
||||
* @returns {T | undefined} The highest-priority element, or `undefined` if empty
|
||||
*/
|
||||
public peek(): T | undefined {
|
||||
return this.heap.peek();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all elements from the queue
|
||||
* @returns {this} The queue instance for chaining
|
||||
*/
|
||||
public clear(): this {
|
||||
this.heap.clear();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a shallow copy of elements in heap order
|
||||
* @returns {T[]} Array of elements
|
||||
*/
|
||||
public toArray(): T[] {
|
||||
return this.heap.toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of the queue
|
||||
* @returns {string} String representation
|
||||
*/
|
||||
public toString(): string {
|
||||
return `PriorityQueue(${this.heap.length})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterator over queue elements in heap order
|
||||
*/
|
||||
public *[Symbol.iterator](): Iterator<T> {
|
||||
yield* this.heap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Async iterator over queue elements in heap order
|
||||
*/
|
||||
public async *[Symbol.asyncIterator](): AsyncIterator<T> {
|
||||
for (const element of this.heap)
|
||||
yield element;
|
||||
}
|
||||
}
|
||||
16
core/stdlib/src/structs/PriorityQueue/types.ts
Normal file
16
core/stdlib/src/structs/PriorityQueue/types.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { Comparator } from '../BinaryHeap';
|
||||
|
||||
export interface PriorityQueueLike<T> extends Iterable<T>, AsyncIterable<T> {
|
||||
readonly length: number;
|
||||
readonly isEmpty: boolean;
|
||||
readonly isFull: boolean;
|
||||
|
||||
enqueue(element: T): void;
|
||||
dequeue(): T | undefined;
|
||||
peek(): T | undefined;
|
||||
clear(): this;
|
||||
toArray(): T[];
|
||||
toString(): string;
|
||||
}
|
||||
|
||||
export type { Comparator };
|
||||
207
core/stdlib/src/structs/Queue/index.test.ts
Normal file
207
core/stdlib/src/structs/Queue/index.test.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { Queue } from '.';
|
||||
|
||||
describe('queue', () => {
|
||||
describe('constructor', () => {
|
||||
it('create an empty queue if no initial values are provided', () => {
|
||||
const queue = new Queue<number>();
|
||||
|
||||
expect(queue.length).toBe(0);
|
||||
expect(queue.isEmpty).toBe(true);
|
||||
});
|
||||
|
||||
it('create a queue with the provided initial values', () => {
|
||||
const queue = new Queue([1, 2, 3]);
|
||||
|
||||
expect(queue.length).toBe(3);
|
||||
expect(queue.peek()).toBe(1);
|
||||
});
|
||||
|
||||
it('create a queue with a single initial value', () => {
|
||||
const queue = new Queue(42);
|
||||
|
||||
expect(queue.length).toBe(1);
|
||||
expect(queue.peek()).toBe(42);
|
||||
});
|
||||
|
||||
it('create a queue with the provided options', () => {
|
||||
const queue = new Queue<number>(undefined, { maxSize: 5 });
|
||||
|
||||
expect(queue.length).toBe(0);
|
||||
expect(queue.isFull).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('enqueue', () => {
|
||||
it('add an element to the back of the queue', () => {
|
||||
const queue = new Queue<number>();
|
||||
queue.enqueue(1);
|
||||
|
||||
expect(queue.length).toBe(1);
|
||||
expect(queue.peek()).toBe(1);
|
||||
});
|
||||
|
||||
it('maintain FIFO order', () => {
|
||||
const queue = new Queue<number>();
|
||||
queue.enqueue(1).enqueue(2).enqueue(3);
|
||||
|
||||
expect(queue.peek()).toBe(1);
|
||||
});
|
||||
|
||||
it('throw an error if the queue is full', () => {
|
||||
const queue = new Queue<number>(undefined, { maxSize: 1 });
|
||||
queue.enqueue(1);
|
||||
|
||||
expect(() => queue.enqueue(2)).toThrow(new RangeError('Queue is full'));
|
||||
});
|
||||
|
||||
it('return this for chaining', () => {
|
||||
const queue = new Queue<number>();
|
||||
const result = queue.enqueue(1);
|
||||
|
||||
expect(result).toBe(queue);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dequeue', () => {
|
||||
it('remove and return the front element', () => {
|
||||
const queue = new Queue([1, 2, 3]);
|
||||
const element = queue.dequeue();
|
||||
|
||||
expect(element).toBe(1);
|
||||
expect(queue.length).toBe(2);
|
||||
});
|
||||
|
||||
it('return undefined if the queue is empty', () => {
|
||||
const queue = new Queue<number>();
|
||||
|
||||
expect(queue.dequeue()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('maintain FIFO order across multiple dequeues', () => {
|
||||
const queue = new Queue([1, 2, 3]);
|
||||
|
||||
expect(queue.dequeue()).toBe(1);
|
||||
expect(queue.dequeue()).toBe(2);
|
||||
expect(queue.dequeue()).toBe(3);
|
||||
expect(queue.dequeue()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('compact internal storage after many dequeues', () => {
|
||||
const queue = new Queue<number>();
|
||||
|
||||
for (let i = 0; i < 100; i++)
|
||||
queue.enqueue(i);
|
||||
|
||||
for (let i = 0; i < 80; i++)
|
||||
queue.dequeue();
|
||||
|
||||
expect(queue.length).toBe(20);
|
||||
expect(queue.peek()).toBe(80);
|
||||
});
|
||||
});
|
||||
|
||||
describe('peek', () => {
|
||||
it('return the front element without removing it', () => {
|
||||
const queue = new Queue([1, 2, 3]);
|
||||
|
||||
expect(queue.peek()).toBe(1);
|
||||
expect(queue.length).toBe(3);
|
||||
});
|
||||
|
||||
it('return undefined if the queue is empty', () => {
|
||||
const queue = new Queue<number>();
|
||||
|
||||
expect(queue.peek()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('clear', () => {
|
||||
it('clear the queue', () => {
|
||||
const queue = new Queue([1, 2, 3]);
|
||||
queue.clear();
|
||||
|
||||
expect(queue.length).toBe(0);
|
||||
expect(queue.isEmpty).toBe(true);
|
||||
});
|
||||
|
||||
it('return this for chaining', () => {
|
||||
const queue = new Queue([1, 2, 3]);
|
||||
|
||||
expect(queue.clear()).toBe(queue);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toArray', () => {
|
||||
it('return elements in FIFO order', () => {
|
||||
const queue = new Queue([1, 2, 3]);
|
||||
|
||||
expect(queue.toArray()).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
it('return correct array after dequeues', () => {
|
||||
const queue = new Queue([1, 2, 3, 4, 5]);
|
||||
queue.dequeue();
|
||||
queue.dequeue();
|
||||
|
||||
expect(queue.toArray()).toEqual([3, 4, 5]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toString', () => {
|
||||
it('return comma-separated string', () => {
|
||||
const queue = new Queue([1, 2, 3]);
|
||||
|
||||
expect(queue.toString()).toBe('1,2,3');
|
||||
});
|
||||
});
|
||||
|
||||
describe('iteration', () => {
|
||||
it('iterate over the queue in FIFO order', () => {
|
||||
const queue = new Queue([1, 2, 3]);
|
||||
|
||||
expect([...queue]).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
it('iterate correctly after dequeues', () => {
|
||||
const queue = new Queue([1, 2, 3, 4]);
|
||||
queue.dequeue();
|
||||
|
||||
expect([...queue]).toEqual([2, 3, 4]);
|
||||
});
|
||||
|
||||
it('iterate over the queue asynchronously in FIFO order', async () => {
|
||||
const queue = new Queue([1, 2, 3]);
|
||||
const elements: number[] = [];
|
||||
|
||||
for await (const element of queue)
|
||||
elements.push(element);
|
||||
|
||||
expect(elements).toEqual([1, 2, 3]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mixed operations', () => {
|
||||
it('interleave enqueue and dequeue', () => {
|
||||
const queue = new Queue<number>();
|
||||
|
||||
queue.enqueue(1);
|
||||
queue.enqueue(2);
|
||||
expect(queue.dequeue()).toBe(1);
|
||||
|
||||
queue.enqueue(3);
|
||||
expect(queue.dequeue()).toBe(2);
|
||||
expect(queue.dequeue()).toBe(3);
|
||||
expect(queue.isEmpty).toBe(true);
|
||||
});
|
||||
|
||||
it('reuse queue after clear', () => {
|
||||
const queue = new Queue([1, 2, 3]);
|
||||
queue.clear();
|
||||
queue.enqueue(4);
|
||||
|
||||
expect(queue.length).toBe(1);
|
||||
expect(queue.peek()).toBe(4);
|
||||
});
|
||||
});
|
||||
});
|
||||
140
core/stdlib/src/structs/Queue/index.ts
Normal file
140
core/stdlib/src/structs/Queue/index.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { Deque } from '../Deque';
|
||||
import type { QueueLike } from './types';
|
||||
|
||||
export type { QueueLike } from './types';
|
||||
|
||||
export interface QueueOptions {
|
||||
maxSize?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @name Queue
|
||||
* @category Data Structures
|
||||
* @description Represents a queue data structure (FIFO) backed by a Deque
|
||||
*
|
||||
* @since 0.0.8
|
||||
*
|
||||
* @template T The type of elements stored in the queue
|
||||
*/
|
||||
export class Queue<T> implements QueueLike<T> {
|
||||
/**
|
||||
* The underlying deque
|
||||
*
|
||||
* @private
|
||||
* @type {Deque<T>}
|
||||
*/
|
||||
private readonly deque: Deque<T>;
|
||||
|
||||
/**
|
||||
* Creates an instance of Queue
|
||||
*
|
||||
* @param {(T[] | T)} [initialValues] The initial values to add to the queue
|
||||
* @param {QueueOptions} [options] The options for the queue
|
||||
*/
|
||||
constructor(initialValues?: T[] | T, options?: QueueOptions) {
|
||||
this.deque = new Deque(initialValues, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of elements in the queue
|
||||
* @returns {number} The number of elements in the queue
|
||||
*/
|
||||
get length() {
|
||||
return this.deque.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the queue is empty
|
||||
* @returns {boolean} `true` if the queue is empty, `false` otherwise
|
||||
*/
|
||||
get isEmpty() {
|
||||
return this.deque.isEmpty;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the queue is full
|
||||
* @returns {boolean} `true` if the queue is full, `false` otherwise
|
||||
*/
|
||||
get isFull() {
|
||||
return this.deque.isFull;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an element to the back of the queue
|
||||
* @param {T} element The element to enqueue
|
||||
* @returns {this}
|
||||
* @throws {RangeError} If the queue is full
|
||||
*/
|
||||
enqueue(element: T) {
|
||||
if (this.deque.isFull)
|
||||
throw new RangeError('Queue is full');
|
||||
|
||||
this.deque.pushBack(element);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes and returns the front element of the queue
|
||||
* @returns {T | undefined} The front element, or undefined if the queue is empty
|
||||
*/
|
||||
dequeue() {
|
||||
return this.deque.popFront();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the front element without removing it
|
||||
* @returns {T | undefined} The front element, or undefined if the queue is empty
|
||||
*/
|
||||
peek() {
|
||||
return this.deque.peekFront();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the queue
|
||||
*
|
||||
* @returns {this}
|
||||
*/
|
||||
clear() {
|
||||
this.deque.clear();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the queue to an array in FIFO order
|
||||
*
|
||||
* @returns {T[]}
|
||||
*/
|
||||
toArray() {
|
||||
return this.deque.toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of the queue
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
toString() {
|
||||
return this.deque.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an iterator for the queue
|
||||
*
|
||||
* @returns {IterableIterator<T>}
|
||||
*/
|
||||
[Symbol.iterator]() {
|
||||
return this.deque[Symbol.iterator]();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an async iterator for the queue
|
||||
*
|
||||
* @returns {AsyncIterableIterator<T>}
|
||||
*/
|
||||
async *[Symbol.asyncIterator]() {
|
||||
for (const element of this.deque)
|
||||
yield element;
|
||||
}
|
||||
}
|
||||
12
core/stdlib/src/structs/Queue/types.ts
Normal file
12
core/stdlib/src/structs/Queue/types.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export interface QueueLike<T> extends Iterable<T>, AsyncIterable<T> {
|
||||
readonly length: number;
|
||||
readonly isEmpty: boolean;
|
||||
readonly isFull: boolean;
|
||||
|
||||
enqueue(element: T): this;
|
||||
dequeue(): T | undefined;
|
||||
peek(): T | undefined;
|
||||
clear(): this;
|
||||
toArray(): T[];
|
||||
toString(): string;
|
||||
}
|
||||
@@ -1 +1,7 @@
|
||||
export * from './stack';
|
||||
export * from './BinaryHeap';
|
||||
export * from './CircularBuffer';
|
||||
export * from './Deque';
|
||||
export * from './LinkedList';
|
||||
export * from './PriorityQueue';
|
||||
export * from './Queue';
|
||||
export * from './Stack';
|
||||
@@ -1,5 +1,8 @@
|
||||
import { last } from '../../arrays';
|
||||
import { isArray } from '../../types';
|
||||
import type { StackLike } from './types';
|
||||
|
||||
export type { StackLike } from './types';
|
||||
|
||||
export interface StackOptions {
|
||||
maxSize?: number;
|
||||
@@ -14,7 +17,7 @@ export interface StackOptions {
|
||||
*
|
||||
* @template T The type of elements stored in the stack
|
||||
*/
|
||||
export class Stack<T> implements Iterable<T>, AsyncIterable<T> {
|
||||
export class Stack<T> implements StackLike<T> {
|
||||
/**
|
||||
* The maximum number of elements that the stack can hold
|
||||
*
|
||||
|
||||
12
core/stdlib/src/structs/stack/types.ts
Normal file
12
core/stdlib/src/structs/stack/types.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export interface StackLike<T> extends Iterable<T>, AsyncIterable<T> {
|
||||
readonly length: number;
|
||||
readonly isEmpty: boolean;
|
||||
readonly isFull: boolean;
|
||||
|
||||
push(element: T): this;
|
||||
pop(): T | undefined;
|
||||
peek(): T | undefined;
|
||||
clear(): this;
|
||||
toArray(): T[];
|
||||
toString(): string;
|
||||
}
|
||||
Reference in New Issue
Block a user