diff --git a/cli.ts b/cli.ts index 4990b18..9b4a883 100644 --- a/cli.ts +++ b/cli.ts @@ -3,7 +3,7 @@ import { defineCommand, runMain } from 'citty'; import { resolve } from 'pathe'; import { splitByCase } from 'scule'; -const PACKAGE_MANAGER = 'pnpm@8.15.6'; +const PACKAGE_MANAGER = 'pnpm@9.0.1'; const NODE_VERSION = '>=18.0.0'; const VITE_VERSION = '^5.2.8'; const VITE_DTS_VERSION = '^3.8.1'; diff --git a/package.json b/package.json index c862379..a22ed47 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "type": "git", "url": "git+https://github.com/robonen/tools.git" }, - "packageManager": "pnpm@8.15.6", + "packageManager": "pnpm@9.0.1", "engines": { "node": ">=18.0.0" }, diff --git a/packages/stdlib/package.json b/packages/stdlib/package.json index 6014622..dc9442f 100644 --- a/packages/stdlib/package.json +++ b/packages/stdlib/package.json @@ -18,7 +18,7 @@ "url": "git+https://github.com/robonen/tools.git", "directory": "packages/stdlib" }, - "packageManager": "pnpm@8.15.6", + "packageManager": "pnpm@9.0.1", "engines": { "node": ">=18.0.0" }, @@ -40,7 +40,7 @@ "test": "vitest", "test:coverage": "vitest run --coverage", "test:bench": "vitest bench", - "dev": "vite", + "dev": "vite build --watch", "build": "vite build" }, "devDependencies": { diff --git a/packages/stdlib/src/index.ts b/packages/stdlib/src/index.ts index 0e72608..e076937 100644 --- a/packages/stdlib/src/index.ts +++ b/packages/stdlib/src/index.ts @@ -1,4 +1,5 @@ export * from './text'; export * from './math'; export * from './patterns'; -export * from './bits'; \ No newline at end of file +export * from './bits'; +export * from './structs'; \ No newline at end of file diff --git a/packages/stdlib/src/structs/index.ts b/packages/stdlib/src/structs/index.ts new file mode 100644 index 0000000..d34d808 --- /dev/null +++ b/packages/stdlib/src/structs/index.ts @@ -0,0 +1 @@ +export * from './stack'; \ No newline at end of file diff --git a/packages/stdlib/src/structs/stack/index.test.ts b/packages/stdlib/src/structs/stack/index.test.ts new file mode 100644 index 0000000..b66dab2 --- /dev/null +++ b/packages/stdlib/src/structs/stack/index.test.ts @@ -0,0 +1,117 @@ +import { describe, it, expect } from 'vitest'; +import { Stack } from './index'; + +describe('Stack', () => { + describe('constructor', () => { + it('create an empty stack if no initial values are provided', () => { + const stack = new Stack(); + + expect(stack.length).toBe(0); + expect(stack.isEmpty).toBe(true); + }); + + it('create a stack with the provided initial values', () => { + const initialValues = [1, 2, 3]; + const stack = new Stack(initialValues); + + expect(stack.length).toBe(initialValues.length); + expect(stack.peek()).toBe(initialValues.at(-1)); + }); + + it('create a stack with the provided initial value', () => { + const initialValue = 1; + const stack = new Stack(initialValue); + + expect(stack.length).toBe(1); + expect(stack.peek()).toBe(initialValue); + }); + + it('create a stack with the provided options', () => { + const options = { maxSize: 5 }; + const stack = new Stack(undefined, options); + + expect(stack.length).toBe(0); + expect(stack.isFull).toBe(false); + }); + }); + + describe('push', () => { + it('push an element onto the stack', () => { + const stack = new Stack(); + stack.push(1); + + expect(stack.length).toBe(1); + expect(stack.peek()).toBe(1); + }); + + it('throw an error if the stack is full', () => { + const options = { maxSize: 1 }; + const stack = new Stack(undefined, options); + stack.push(1); + + expect(() => stack.push(2)).toThrow(new RangeError('Stack is full')); + }); + }); + + describe('pop', () => { + it('pop an element from the stack', () => { + const stack = new Stack([1, 2, 3]); + const poppedElement = stack.pop(); + + expect(poppedElement).toBe(3); + expect(stack.length).toBe(2); + }); + + it('return undefined if the stack is empty', () => { + const stack = new Stack(); + const poppedElement = stack.pop(); + + expect(poppedElement).toBeUndefined(); + }); + }); + + describe('peek', () => { + it('return the top element of the stack', () => { + const stack = new Stack([1, 2, 3]); + const topElement = stack.peek(); + + expect(topElement).toBe(3); + expect(stack.length).toBe(3); + }); + + it('throw an error if the stack is empty', () => { + const stack = new Stack(); + expect(() => stack.peek()).toThrow(new RangeError('Stack is empty')); + }); + }); + + describe('clear', () => { + it('clear the stack', () => { + const stack = new Stack([1, 2, 3]); + stack.clear(); + + expect(stack.length).toBe(0); + expect(stack.isEmpty).toBe(true); + }); + }); + + describe('iteration', () => { + it('iterate over the stack in LIFO order', () => { + const stack = new Stack([1, 2, 3]); + const elements = [...stack]; + + expect(elements).toEqual([3, 2, 1]); + }); + + it('iterate over the stack asynchronously in LIFO order', async () => { + const stack = new Stack([1, 2, 3]); + const elements: number[] = []; + + for await (const element of stack) { + elements.push(element); + } + + expect(elements).toEqual([3, 2, 1]); + }); + }); +}); \ No newline at end of file diff --git a/packages/stdlib/src/structs/stack/index.ts b/packages/stdlib/src/structs/stack/index.ts new file mode 100644 index 0000000..39a30f6 --- /dev/null +++ b/packages/stdlib/src/structs/stack/index.ts @@ -0,0 +1,144 @@ +export type StackOptions = { + maxSize?: number; +}; + +/** + * Represents a stack data structure + * @template T The type of elements stored in the stack + */ +export class Stack implements Iterable, AsyncIterable { + /** + * The maximum number of elements that the stack can hold + * + * @private + * @type {number} + */ + private maxSize: number; + + /** + * The stack data structure + * + * @private + * @type {T[]} + */ + private stack: T[]; + + /** + * Creates an instance of Stack + * + * @param {(T[] | T)} [initialValues] The initial values to add to the stack + * @param {StackOptions} [options] The options for the stack + * @memberof Stack + */ + constructor(initialValues?: T[] | T, options?: StackOptions) { + this.maxSize = options?.maxSize ?? Infinity; + this.stack = Array.isArray(initialValues) ? initialValues : initialValues ? [initialValues] : []; + } + + /** + * Gets the number of elements in the stack + * @returns {number} The number of elements in the stack + */ + public get length(): number { + return this.stack.length; + } + + /** + * Checks if the stack is empty + * @returns {boolean} `true` if the stack is empty, `false` otherwise + */ + public get isEmpty(): boolean { + return this.stack.length === 0; + } + + /** + * Checks if the stack is full + * @returns {boolean} `true` if the stack is full, `false` otherwise + */ + public get isFull(): boolean { + return this.stack.length === this.maxSize; + } + + /** + * Pushes an element onto the stack + * @param {T} element The element to push onto the stack + * @returns {this} + * @throws {RangeError} If the stack is full + */ + public push(element: T) { + if (this.isFull) + throw new RangeError('Stack is full'); + + this.stack.push(element); + + return this; + } + + /** + * Pops an element from the stack + * @returns {T} The element popped from the stack + */ + public pop(): T | undefined { + return this.stack.pop(); + } + + /** + * Peeks at the top element of the stack + * @returns {T} The top element of the stack + */ + public peek(): T | undefined { + if (this.isEmpty) + throw new RangeError('Stack is empty'); + + return this.stack[this.stack.length - 1]; + } + + /** + * Clears the stack + * + * @returns {this} + */ + public clear() { + this.stack.length = 0; + + return this; + } + + /** + * Converts the stack to an array + * + * @returns {T[]} + */ + public toArray(): T[] { + return this.stack.toReversed(); + } + + /** + * Returns a string representation of the stack + * + * @returns {string} + */ + public toString() { + return this.toArray().toString(); + } + + /** + * Returns an iterator for the stack + * + * @returns {IterableIterator} + */ + public [Symbol.iterator]() { + return this.toArray()[Symbol.iterator](); + } + + /** + * Returns an async iterator for the stack + * + * @returns {AsyncIterableIterator} + */ + public async *[Symbol.asyncIterator]() { + for (const element of this.toArray()) { + yield element; + } + } +}