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:
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;
|
||||
}
|
||||
Reference in New Issue
Block a user