mirror of
https://github.com/robonen/tools.git
synced 2026-03-20 19:04:46 +00:00
feat(core/stdlib): implement LinkedList, PriorityQueue, and Queue data structures
This commit is contained in:
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;
|
||||
}
|
||||
Reference in New Issue
Block a user