1
0
mirror of https://github.com/robonen/tools.git synced 2026-03-20 02:44:45 +00:00

feat(monorepo): migrate vue packages and apply oxlint refactors

This commit is contained in:
2026-03-07 18:07:22 +07:00
parent abd6605db3
commit 41d5e18f6b
286 changed files with 10295 additions and 5028 deletions

6
core/encoding/jsr.json Normal file
View File

@@ -0,0 +1,6 @@
{
"$schema": "https://jsr.io/schema/config-file.v1.json",
"name": "@robonen/encoding",
"version": "0.0.1",
"exports": "./src/index.ts"
}

View File

@@ -0,0 +1,14 @@
import { defineConfig } from 'oxlint';
import { compose, base, typescript, imports, stylistic } from '@robonen/oxlint';
export default defineConfig(compose(base, typescript, imports, stylistic, {
overrides: [
{
files: ['src/qr/qr-code.ts'],
rules: {
'@stylistic/max-statements-per-line': 'off',
'@stylistic/no-mixed-operators': 'off',
},
},
],
}));

View File

@@ -0,0 +1,47 @@
{
"name": "@robonen/encoding",
"version": "0.0.1",
"license": "Apache-2.0",
"description": "Encoding utilities for TypeScript",
"keywords": [
"encoding",
"tools"
],
"author": "Robonen Andrew <robonenandrew@gmail.com>",
"repository": {
"type": "git",
"url": "git+https://github.com/robonen/tools.git",
"directory": "core/encoding"
},
"packageManager": "pnpm@10.30.3",
"engines": {
"node": ">=24.13.1"
},
"type": "module",
"files": [
"dist"
],
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
},
"scripts": {
"lint:check": "oxlint -c oxlint.config.ts",
"lint:fix": "oxlint -c oxlint.config.ts --fix",
"test": "vitest run",
"dev": "vitest dev",
"bench": "vitest bench",
"build": "tsdown"
},
"devDependencies": {
"@robonen/oxlint": "workspace:*",
"@robonen/tsconfig": "workspace:*",
"@robonen/tsdown": "workspace:*",
"@stylistic/eslint-plugin": "catalog:",
"oxlint": "catalog:",
"tsdown": "catalog:"
}
}

View File

@@ -0,0 +1,2 @@
export * from './reed-solomon';
export * from './qr';

View File

@@ -0,0 +1,67 @@
import type { QrCodeEcc, QrSegmentMode } from './types';
/* -- ECC Levels -- */
export const LOW: QrCodeEcc = [0, 1]; // ~7% recovery
export const MEDIUM: QrCodeEcc = [1, 0]; // ~15% recovery
export const QUARTILE: QrCodeEcc = [2, 3]; // ~25% recovery
export const HIGH: QrCodeEcc = [3, 2]; // ~30% recovery
export const EccMap = {
L: LOW,
M: MEDIUM,
Q: QUARTILE,
H: HIGH,
} as const;
/* -- Segment Modes -- */
export const MODE_NUMERIC: QrSegmentMode = [0x1, 10, 12, 14];
export const MODE_ALPHANUMERIC: QrSegmentMode = [0x2, 9, 11, 13];
export const MODE_BYTE: QrSegmentMode = [0x4, 8, 16, 16];
/* -- Version Limits -- */
export const MIN_VERSION = 1;
export const MAX_VERSION = 40;
/* -- Penalty Constants -- */
export const PENALTY_N1 = 3;
export const PENALTY_N2 = 3;
export const PENALTY_N3 = 40;
export const PENALTY_N4 = 10;
/* -- Character Sets & Patterns -- */
export const NUMERIC_REGEX = /^[0-9]*$/;
export const ALPHANUMERIC_REGEX = /^[A-Z0-9 $%*+./:_-]*$/;
export const ALPHANUMERIC_CHARSET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:' as const;
/** Pre-computed charCode → alphanumeric index lookup (0xFF = invalid). O(1) instead of O(45) indexOf. */
export const ALPHANUMERIC_MAP = /* @__PURE__ */ (() => {
const map = new Uint8Array(128).fill(0xFF);
for (let i = 0; i < ALPHANUMERIC_CHARSET.length; i++)
map[ALPHANUMERIC_CHARSET.charCodeAt(i)] = i;
return map;
})();
/* -- ECC Lookup Tables -- */
// prettier-ignore
export const ECC_CODEWORDS_PER_BLOCK: number[][] = [
// 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40
[-1, 7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, 28, 28, 28, 30, 30, 26, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], // Low
[-1, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26, 30, 22, 22, 24, 24, 28, 28, 26, 26, 26, 26, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28], // Medium
[-1, 13, 22, 18, 26, 18, 24, 18, 22, 20, 24, 28, 26, 24, 20, 30, 24, 28, 28, 26, 30, 28, 30, 30, 30, 30, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], // Quartile
[-1, 17, 28, 22, 16, 22, 28, 26, 26, 24, 28, 24, 28, 22, 24, 24, 30, 28, 28, 26, 28, 30, 24, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], // High
];
// prettier-ignore
export const NUM_ERROR_CORRECTION_BLOCKS: number[][] = [
// 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40
[-1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8, 8, 9, 9, 10, 12, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25], // Low
[-1, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49], // Medium
[-1, 1, 1, 2, 2, 4, 4, 6, 6, 8, 8, 8, 10, 12, 16, 12, 17, 16, 18, 21, 20, 23, 23, 25, 27, 29, 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68], // Quartile
[-1, 1, 1, 2, 4, 4, 4, 5, 6, 8, 8, 11, 11, 16, 16, 18, 16, 19, 21, 25, 25, 25, 34, 30, 32, 35, 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81], // High
];

View File

@@ -0,0 +1,94 @@
import type { QrCodeEcc } from './types';
import { HIGH, MAX_VERSION, MEDIUM, MIN_VERSION, QUARTILE } from './constants';
import { QrCode } from './qr-code';
import { makeBytes, makeSegments } from './segment';
import type { QrSegment } from './segment';
import { appendBits, assert, getNumDataCodewords, getTotalBits, numCharCountBits } from './utils';
/**
* Returns a QR Code representing the given Unicode text string at the given error correction level.
* As a conservative upper bound, this function is guaranteed to succeed for strings that have 738 or fewer
* Unicode code points (not UTF-16 code units) if the low error correction level is used.
* The smallest possible QR Code version is automatically chosen for the output.
*/
export function encodeText(text: string, ecl: QrCodeEcc): QrCode {
const segs = makeSegments(text);
return encodeSegments(segs, ecl);
}
/**
* Returns a QR Code representing the given binary data at the given error correction level.
* This function always encodes using the binary segment mode, not any text mode.
* The maximum number of bytes allowed is 2953.
*/
export function encodeBinary(data: Readonly<number[]>, ecl: QrCodeEcc): QrCode {
const seg = makeBytes(data);
return encodeSegments([seg], ecl);
}
/**
* Returns a QR Code representing the given segments with the given encoding parameters.
* The smallest possible QR Code version within the given range is automatically chosen for the output.
* This is a mid-level API; the high-level API is encodeText() and encodeBinary().
*/
export function encodeSegments(
segs: Readonly<QrSegment[]>,
ecl: QrCodeEcc,
minVersion = 1,
maxVersion = 40,
mask = -1,
boostEcl = true,
): QrCode {
if (!(MIN_VERSION <= minVersion && minVersion <= maxVersion && maxVersion <= MAX_VERSION)
|| mask < -1 || mask > 7)
throw new RangeError('Invalid value');
// Find the minimal version number to use
let version: number;
let dataUsedBits: number;
for (version = minVersion; ; version++) {
const dataCapacityBits = getNumDataCodewords(version, ecl) * 8;
const usedBits = getTotalBits(segs, version);
if (usedBits <= dataCapacityBits) {
dataUsedBits = usedBits;
break;
}
if (version >= maxVersion)
throw new RangeError('Data too long');
}
// Increase the error correction level while the data still fits in the current version number
for (const newEcl of [MEDIUM, QUARTILE, HIGH]) {
if (boostEcl && dataUsedBits! <= getNumDataCodewords(version, newEcl) * 8)
ecl = newEcl;
}
// Concatenate all segments to create the data bit string
const bb: number[] = [];
for (const seg of segs) {
appendBits(seg.mode[0], 4, bb);
appendBits(seg.numChars, numCharCountBits(seg.mode, version), bb);
for (const b of seg.bitData)
bb.push(b);
}
assert(bb.length === dataUsedBits!);
// Add terminator and pad up to a byte if applicable
const dataCapacityBits = getNumDataCodewords(version, ecl) * 8;
assert(bb.length <= dataCapacityBits);
appendBits(0, Math.min(4, dataCapacityBits - bb.length), bb);
appendBits(0, (8 - bb.length % 8) % 8, bb);
assert(bb.length % 8 === 0);
// Pad with alternating bytes until data capacity is reached
for (let padByte = 0xEC; bb.length < dataCapacityBits; padByte ^= 0xEC ^ 0x11)
appendBits(padByte, 8, bb);
// Pack bits into bytes in big endian
const dataCodewords = Array.from({ length: Math.ceil(bb.length / 8) }, () => 0);
for (let i = 0; i < bb.length; i++)
dataCodewords[i >>> 3]! |= bb[i]! << (7 - (i & 7));
// Create the QR Code object
return new QrCode(version, ecl, dataCodewords, mask);
}

View File

@@ -0,0 +1,96 @@
import { bench, describe } from 'vitest';
import { encodeBinary, encodeSegments, encodeText, makeSegments, LOW, EccMap } from '.';
/* -- Test data -- */
const SHORT_TEXT = 'Hello';
const MEDIUM_TEXT = 'https://example.com/path?query=value&foo=bar';
const LONG_TEXT = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit.';
const NUMERIC_TEXT = '314159265358979323846264338327950288419716939937510';
const ALPHANUMERIC_TEXT = 'HELLO WORLD 12345';
const SMALL_BINARY = Array.from({ length: 32 }, (_, i) => i);
const MEDIUM_BINARY = Array.from({ length: 256 }, (_, i) => i % 256);
/* -- Precomputed segments for isolated encodeSegments benchmark -- */
const precomputedSegs = makeSegments(MEDIUM_TEXT);
/* -- encodeText benchmarks -- */
describe('encodeText', () => {
bench('short text (5 chars)', () => {
encodeText(SHORT_TEXT, LOW);
});
bench('medium text (URL ~44 chars)', () => {
encodeText(MEDIUM_TEXT, LOW);
});
bench('long text (~270 chars)', () => {
encodeText(LONG_TEXT, LOW);
});
bench('numeric text (50 digits)', () => {
encodeText(NUMERIC_TEXT, LOW);
});
bench('alphanumeric text (17 chars)', () => {
encodeText(ALPHANUMERIC_TEXT, LOW);
});
});
/* -- ECC level impact -- */
describe('encodeText — ECC levels', () => {
bench('LOW (L)', () => {
encodeText(MEDIUM_TEXT, EccMap.L);
});
bench('MEDIUM (M)', () => {
encodeText(MEDIUM_TEXT, EccMap.M);
});
bench('QUARTILE (Q)', () => {
encodeText(MEDIUM_TEXT, EccMap.Q);
});
bench('HIGH (H)', () => {
encodeText(MEDIUM_TEXT, EccMap.H);
});
});
/* -- encodeBinary benchmarks -- */
describe('encodeBinary', () => {
bench('small binary (32 bytes)', () => {
encodeBinary(SMALL_BINARY, LOW);
});
bench('medium binary (256 bytes)', () => {
encodeBinary(MEDIUM_BINARY, LOW);
});
});
/* -- makeSegments benchmarks -- */
describe('makeSegments', () => {
bench('numeric classification', () => {
makeSegments(NUMERIC_TEXT);
});
bench('alphanumeric classification', () => {
makeSegments(ALPHANUMERIC_TEXT);
});
bench('byte mode classification', () => {
makeSegments(MEDIUM_TEXT);
});
});
/* -- encodeSegments (pre-built segments) -- */
describe('encodeSegments', () => {
bench('from pre-built segments', () => {
encodeSegments(precomputedSegs, LOW);
});
});

View File

@@ -0,0 +1,182 @@
import { describe, expect, it } from 'vitest';
import { encodeText, encodeBinary, makeSegments, isNumeric, isAlphanumeric, QrCode, EccMap, LOW, MEDIUM, HIGH } from '.';
describe('isNumeric', () => {
it('accepts pure digit strings', () => {
expect(isNumeric('0123456789')).toBe(true);
expect(isNumeric('0')).toBe(true);
expect(isNumeric('')).toBe(true);
});
it('rejects non-digit characters', () => {
expect(isNumeric('12a3')).toBe(false);
expect(isNumeric('HELLO')).toBe(false);
expect(isNumeric('12 34')).toBe(false);
});
});
describe('isAlphanumeric', () => {
it('accepts valid alphanumeric strings', () => {
expect(isAlphanumeric('HELLO WORLD')).toBe(true);
expect(isAlphanumeric('0123456789')).toBe(true);
expect(isAlphanumeric('ABC123')).toBe(true);
expect(isAlphanumeric('')).toBe(true);
});
it('rejects lowercase and special characters', () => {
expect(isAlphanumeric('hello')).toBe(false);
expect(isAlphanumeric('Hello')).toBe(false);
expect(isAlphanumeric('test@email')).toBe(false);
});
});
describe('makeSegments', () => {
it('returns empty array for empty string', () => {
expect(makeSegments('')).toEqual([]);
});
it('selects numeric mode for digit strings', () => {
const segs = makeSegments('12345');
expect(segs).toHaveLength(1);
expect(segs[0]!.mode[0]).toBe(0x1); // MODE_NUMERIC
});
it('selects alphanumeric mode for uppercase strings', () => {
const segs = makeSegments('HELLO WORLD');
expect(segs).toHaveLength(1);
expect(segs[0]!.mode[0]).toBe(0x2); // MODE_ALPHANUMERIC
});
it('selects byte mode for general text', () => {
const segs = makeSegments('Hello, World!');
expect(segs).toHaveLength(1);
expect(segs[0]!.mode[0]).toBe(0x4); // MODE_BYTE
});
});
describe('encodeText', () => {
it('encodes short text at LOW ECC', () => {
const qr = encodeText('Hello', LOW);
expect(qr).toBeInstanceOf(QrCode);
expect(qr.version).toBeGreaterThanOrEqual(1);
expect(qr.size).toBe(qr.version * 4 + 17);
expect(qr.mask).toBeGreaterThanOrEqual(0);
expect(qr.mask).toBeLessThanOrEqual(7);
});
it('encodes text at different ECC levels', () => {
const qrL = encodeText('Test', LOW);
const qrM = encodeText('Test', MEDIUM);
const qrH = encodeText('Test', HIGH);
// Higher ECC needs same or higher version
expect(qrH.version).toBeGreaterThanOrEqual(qrL.version);
// All produce valid sizes
for (const qr of [qrL, qrM, qrH]) {
expect(qr.size).toBe(qr.version * 4 + 17);
}
});
it('encodes numeric-only text', () => {
const qr = encodeText('123456789012345', LOW);
expect(qr.version).toBe(1); // Numeric mode is compact
});
it('encodes a URL', () => {
const qr = encodeText('https://example.com/path?query=value', LOW);
expect(qr).toBeInstanceOf(QrCode);
expect(qr.size).toBeGreaterThanOrEqual(21);
});
it('encodes long text', () => {
const longText = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam.';
const qr = encodeText(longText, LOW);
expect(qr).toBeInstanceOf(QrCode);
});
it('throws for data too long', () => {
const tooLong = 'A'.repeat(10000);
expect(() => encodeText(tooLong, HIGH)).toThrow(RangeError);
});
});
describe('encodeBinary', () => {
it('encodes binary data', () => {
const data = [0x00, 0xFF, 0x48, 0x65, 0x6C, 0x6C, 0x6F];
const qr = encodeBinary(data, LOW);
expect(qr).toBeInstanceOf(QrCode);
});
});
describe('QrCode', () => {
it('modules grid has correct dimensions', () => {
const qr = encodeText('Test', LOW);
// Flat Uint8Array grid, verify via getModule
expect(qr.size).toBeGreaterThanOrEqual(21);
for (let y = 0; y < qr.size; y++) {
for (let x = 0; x < qr.size; x++) {
const mod = qr.getModule(x, y);
expect(typeof mod).toBe('boolean');
}
}
});
it('types grid has correct dimensions', () => {
const qr = encodeText('Test', LOW);
// Flat Int8Array grid, verify via getType
for (let y = 0; y < qr.size; y++) {
for (let x = 0; x < qr.size; x++) {
const t = qr.getType(x, y);
expect(typeof t).toBe('number');
}
}
});
it('getModule returns false for out of bounds', () => {
const qr = encodeText('Test', LOW);
expect(qr.getModule(-1, 0)).toBe(false);
expect(qr.getModule(0, -1)).toBe(false);
expect(qr.getModule(qr.size, 0)).toBe(false);
expect(qr.getModule(0, qr.size)).toBe(false);
});
it('produces deterministic output', () => {
const qr1 = encodeText('Hello', LOW);
const qr2 = encodeText('Hello', LOW);
expect(qr1.version).toBe(qr2.version);
expect(qr1.mask).toBe(qr2.mask);
for (let y = 0; y < qr1.size; y++) {
for (let x = 0; x < qr1.size; x++) {
expect(qr1.getModule(x, y)).toBe(qr2.getModule(x, y));
}
}
});
it('different inputs produce different outputs', () => {
const qr1 = encodeText('Hello', LOW);
const qr2 = encodeText('World', LOW);
// They might have the same version/size but different modules
let hasDiff = false;
for (let y = 0; y < qr1.size && !hasDiff; y++) {
for (let x = 0; x < qr1.size && !hasDiff; x++) {
if (qr1.getModule(x, y) !== qr2.getModule(x, y))
hasDiff = true;
}
}
expect(hasDiff).toBe(true);
});
});
describe('EccMap', () => {
it('has all four levels', () => {
expect(EccMap.L).toBeDefined();
expect(EccMap.M).toBeDefined();
expect(EccMap.Q).toBeDefined();
expect(EccMap.H).toBeDefined();
});
it('works with encodeText', () => {
const qr = encodeText('Test', EccMap.L);
expect(qr).toBeInstanceOf(QrCode);
});
});

View File

@@ -0,0 +1,8 @@
export { QrCodeDataType } from './types';
export type { QrCodeEcc, QrSegmentMode } from './types';
export { EccMap, LOW, MEDIUM, QUARTILE, HIGH } from './constants';
export { QrCode } from './qr-code';
export { QrSegment, makeBytes, makeSegments, isNumeric, isAlphanumeric } from './segment';
export { encodeText, encodeBinary, encodeSegments } from './encode';

View File

@@ -0,0 +1,436 @@
/*
* QR Code generator — core QrCode class
*
* Based on Project Nayuki's QR Code generator library (MIT License)
* https://www.nayuki.io/page/qr-code-generator-library
*/
import type { QrCodeEcc } from './types';
import { QrCodeDataType } from './types';
import { ECC_CODEWORDS_PER_BLOCK, MAX_VERSION, MIN_VERSION, NUM_ERROR_CORRECTION_BLOCKS } from './constants';
import { assert, getBit, getNumDataCodewords, getNumRawDataModules } from './utils';
import { computeDivisor, computeRemainder } from '../reed-solomon';
const PENALTY_N1 = 3;
const PENALTY_N2 = 3;
const PENALTY_N3 = 40;
const PENALTY_N4 = 10;
/**
* A QR Code symbol, which is a type of two-dimension barcode.
* Invented by Denso Wave and described in the ISO/IEC 18004 standard.
* Instances of this class represent an immutable square grid of dark and light cells.
*/
export class QrCode {
/** The width and height of this QR Code, measured in modules, between 21 and 177 (inclusive). */
public readonly size: number;
/** The index of the mask pattern used in this QR Code, which is between 0 and 7 (inclusive). */
public readonly mask: number;
/** The modules of this QR Code (0 = light, 1 = dark). Flat row-major Uint8Array. */
private readonly modules: Uint8Array;
/** Data type of each module. Flat row-major Int8Array. */
private readonly types: Int8Array;
/**
* Creates a new QR Code with the given version number, error correction level, data codeword bytes, and mask number.
* This is a low-level API that most users should not use directly.
*/
public constructor(
/** The version number of this QR Code, which is between 1 and 40 (inclusive). */
public readonly version: number,
/** The error correction level used in this QR Code. */
public readonly ecc: QrCodeEcc,
dataCodewords: Readonly<number[]>,
msk: number,
) {
if (version < MIN_VERSION || version > MAX_VERSION)
throw new RangeError('Version value out of range');
if (msk < -1 || msk > 7)
throw new RangeError('Mask value out of range');
this.size = version * 4 + 17;
const totalModules = this.size * this.size;
this.modules = new Uint8Array(totalModules);
this.types = new Int8Array(totalModules); // 0 = QrCodeDataType.Data
// Compute ECC, draw modules
this.drawFunctionPatterns();
const allCodewords = this.addEccAndInterleave(dataCodewords);
this.drawCodewords(allCodewords);
// Do masking
if (msk === -1) {
let minPenalty = 1_000_000_000;
for (let i = 0; i < 8; i++) {
this.applyMask(i);
this.drawFormatBits(i);
const penalty = this.getPenaltyScore();
if (penalty < minPenalty) {
msk = i;
minPenalty = penalty;
}
this.applyMask(i); // Undoes the mask due to XOR
}
}
assert(msk >= 0 && msk <= 7);
this.mask = msk;
this.applyMask(msk);
this.drawFormatBits(msk);
}
/**
* Returns the color of the module (pixel) at the given coordinates.
* false for light, true for dark. Out of bounds returns false (light).
*/
public getModule(x: number, y: number): boolean {
return x >= 0 && x < this.size && y >= 0 && y < this.size
&& this.modules[y * this.size + x] === 1;
}
/** Returns the data type of the module at the given coordinates. */
public getType(x: number, y: number): QrCodeDataType {
return this.types[y * this.size + x] as QrCodeDataType;
}
/* -- Private helper methods for constructor: Drawing function modules -- */
private drawFunctionPatterns(): void {
const size = this.size;
// Draw horizontal and vertical timing patterns
for (let i = 0; i < size; i++) {
const dark = i % 2 === 0 ? 1 : 0;
this.setFunctionModule(6, i, dark, QrCodeDataType.Timing);
this.setFunctionModule(i, 6, dark, QrCodeDataType.Timing);
}
// Draw 3 finder patterns (all corners except bottom right)
this.drawFinderPattern(3, 3);
this.drawFinderPattern(size - 4, 3);
this.drawFinderPattern(3, size - 4);
// Draw numerous alignment patterns
const alignPatPos = this.getAlignmentPatternPositions();
const numAlign = alignPatPos.length;
for (let i = 0; i < numAlign; i++) {
for (let j = 0; j < numAlign; j++) {
if (!(i === 0 && j === 0 || i === 0 && j === numAlign - 1 || i === numAlign - 1 && j === 0))
this.drawAlignmentPattern(alignPatPos[i]!, alignPatPos[j]!);
}
}
// Draw configuration data
this.drawFormatBits(0); // Dummy mask value; overwritten later in the constructor
this.drawVersion();
}
private drawFormatBits(mask: number): void {
const data = this.ecc[1] << 3 | mask;
let rem = data;
for (let i = 0; i < 10; i++)
rem = (rem << 1) ^ ((rem >>> 9) * 0x537);
const bits = (data << 10 | rem) ^ 0x5412;
assert(bits >>> 15 === 0);
const size = this.size;
// Draw first copy
for (let i = 0; i <= 5; i++)
this.setFunctionModule(8, i, getBit(bits, i) ? 1 : 0);
this.setFunctionModule(8, 7, getBit(bits, 6) ? 1 : 0);
this.setFunctionModule(8, 8, getBit(bits, 7) ? 1 : 0);
this.setFunctionModule(7, 8, getBit(bits, 8) ? 1 : 0);
for (let i = 9; i < 15; i++)
this.setFunctionModule(14 - i, 8, getBit(bits, i) ? 1 : 0);
// Draw second copy
for (let i = 0; i < 8; i++)
this.setFunctionModule(size - 1 - i, 8, getBit(bits, i) ? 1 : 0);
for (let i = 8; i < 15; i++)
this.setFunctionModule(8, size - 15 + i, getBit(bits, i) ? 1 : 0);
this.setFunctionModule(8, size - 8, 1);
}
private drawVersion(): void {
if (this.version < 7)
return;
let rem = this.version;
for (let i = 0; i < 12; i++)
rem = (rem << 1) ^ ((rem >>> 11) * 0x1F25);
const bits = this.version << 12 | rem;
assert(bits >>> 18 === 0);
const size = this.size;
for (let i = 0; i < 18; i++) {
const color = getBit(bits, i) ? 1 : 0;
const a = size - 11 + i % 3;
const b = (i / 3) | 0;
this.setFunctionModule(a, b, color);
this.setFunctionModule(b, a, color);
}
}
private drawFinderPattern(x: number, y: number): void {
const size = this.size;
for (let dy = -4; dy <= 4; dy++) {
for (let dx = -4; dx <= 4; dx++) {
const dist = Math.max(Math.abs(dx), Math.abs(dy));
const xx = x + dx;
const yy = y + dy;
if (xx >= 0 && xx < size && yy >= 0 && yy < size)
this.setFunctionModule(xx, yy, dist !== 2 && dist !== 4 ? 1 : 0, QrCodeDataType.Position);
}
}
}
private drawAlignmentPattern(x: number, y: number): void {
for (let dy = -2; dy <= 2; dy++) {
for (let dx = -2; dx <= 2; dx++) {
this.setFunctionModule(
x + dx,
y + dy,
Math.max(Math.abs(dx), Math.abs(dy)) !== 1 ? 1 : 0,
QrCodeDataType.Alignment,
);
}
}
}
private setFunctionModule(x: number, y: number, isDark: number, type: QrCodeDataType = QrCodeDataType.Function): void {
const idx = y * this.size + x;
this.modules[idx] = isDark;
this.types[idx] = type;
}
/* -- Private helper methods for constructor: Codewords and masking -- */
private addEccAndInterleave(data: Readonly<number[]>): number[] {
const ver = this.version;
const ecl = this.ecc;
if (data.length !== getNumDataCodewords(ver, ecl))
throw new RangeError('Invalid argument');
const numBlocks = NUM_ERROR_CORRECTION_BLOCKS[ecl[0]]![ver]!;
const blockEccLen = ECC_CODEWORDS_PER_BLOCK[ecl[0]]![ver]!;
const rawCodewords = (getNumRawDataModules(ver) / 8) | 0;
const numShortBlocks = numBlocks - rawCodewords % numBlocks;
const shortBlockLen = (rawCodewords / numBlocks) | 0;
// Split data into blocks and append ECC to each block
const blocks: number[][] = [];
const rsDiv = computeDivisor(blockEccLen);
for (let i = 0, k = 0; i < numBlocks; i++) {
const dat: number[] = data.slice(k, k + shortBlockLen - blockEccLen + (i < numShortBlocks ? 0 : 1)) as number[];
k += dat.length;
const ecc = computeRemainder(dat, rsDiv);
if (i < numShortBlocks)
dat.push(0);
blocks.push([...dat, ...ecc]);
}
// Interleave (not concatenate) the bytes from every block into a single sequence
const result: number[] = [];
const blockLen = blocks[0]!.length;
for (let i = 0; i < blockLen; i++) {
for (let j = 0; j < blocks.length; j++) {
if (i !== shortBlockLen - blockEccLen || j >= numShortBlocks)
result.push(blocks[j]![i]!);
}
}
assert(result.length === rawCodewords);
return result;
}
private drawCodewords(data: Readonly<number[]>): void {
if (data.length !== ((getNumRawDataModules(this.version) / 8) | 0))
throw new RangeError('Invalid argument');
const size = this.size;
const modules = this.modules;
const types = this.types;
let i = 0;
for (let right = size - 1; right >= 1; right -= 2) {
if (right === 6)
right = 5;
for (let vert = 0; vert < size; vert++) {
for (let j = 0; j < 2; j++) {
const x = right - j;
const upward = ((right + 1) & 2) === 0;
const y = upward ? size - 1 - vert : vert;
const idx = y * size + x;
if (types[idx] === QrCodeDataType.Data && i < data.length * 8) {
modules[idx] = (data[i >>> 3]! >>> (7 - (i & 7))) & 1;
i++;
}
}
}
}
assert(i === data.length * 8);
}
private applyMask(mask: number): void {
if (mask < 0 || mask > 7)
throw new RangeError('Mask value out of range');
const size = this.size;
const modules = this.modules;
const types = this.types;
for (let y = 0; y < size; y++) {
const yOffset = y * size;
for (let x = 0; x < size; x++) {
const idx = yOffset + x;
if (types[idx] !== QrCodeDataType.Data)
continue;
let invert: boolean;
switch (mask) {
case 0: invert = (x + y) % 2 === 0; break;
case 1: invert = y % 2 === 0; break;
case 2: invert = x % 3 === 0; break;
case 3: invert = (x + y) % 3 === 0; break;
case 4: invert = (((x / 3) | 0) + ((y / 2) | 0)) % 2 === 0; break;
case 5: invert = x * y % 2 + x * y % 3 === 0; break;
case 6: invert = (x * y % 2 + x * y % 3) % 2 === 0; break;
case 7: invert = ((x + y) % 2 + x * y % 3) % 2 === 0; break;
default: throw new Error('Unreachable');
}
if (invert)
modules[idx]! ^= 1;
}
}
}
private getPenaltyScore(): number {
const size = this.size;
const modules = this.modules;
let result = 0;
// Adjacent modules in row having same color, and finder-like patterns
for (let y = 0; y < size; y++) {
const yOffset = y * size;
let runColor = 0;
let runX = 0;
let h0 = 0, h1 = 0, h2 = 0, h3 = 0, h4 = 0, h5 = 0, h6 = 0;
for (let x = 0; x < size; x++) {
const mod = modules[yOffset + x]!;
if (mod === runColor) {
runX++;
if (runX === 5)
result += PENALTY_N1;
else if (runX > 5)
result++;
}
else {
// finderPenaltyAddHistory inlined
if (h0 === 0) runX += size;
h6 = h5; h5 = h4; h4 = h3; h3 = h2; h2 = h1; h1 = runX; h0 = runX;
// finderPenaltyCountPatterns inlined (only when runColor is light = 0)
if (runColor === 0) {
const core = h1 > 0 && h2 === h1 && h3 === h1 * 3 && h4 === h1 && h5 === h1;
if (core && h0 >= h1 * 4 && h6 >= h1) result += PENALTY_N3;
if (core && h6 >= h1 * 4 && h0 >= h1) result += PENALTY_N3;
}
runColor = mod;
runX = 1;
}
}
// finderPenaltyTerminateAndCount inlined
{
let currentRunLength = runX;
if (runColor === 1) {
if (h0 === 0) currentRunLength += size;
h6 = h5; h5 = h4; h4 = h3; h3 = h2; h2 = h1; h1 = currentRunLength; h0 = currentRunLength;
currentRunLength = 0;
}
currentRunLength += size;
if (h0 === 0) currentRunLength += size;
h6 = h5; h5 = h4; h4 = h3; h3 = h2; h2 = h1; h1 = currentRunLength; h0 = currentRunLength;
const core = h1 > 0 && h2 === h1 && h3 === h1 * 3 && h4 === h1 && h5 === h1;
if (core && h0 >= h1 * 4 && h6 >= h1) result += PENALTY_N3;
if (core && h6 >= h1 * 4 && h0 >= h1) result += PENALTY_N3;
}
}
// Adjacent modules in column having same color, and finder-like patterns
for (let x = 0; x < size; x++) {
let runColor = 0;
let runY = 0;
let h0 = 0, h1 = 0, h2 = 0, h3 = 0, h4 = 0, h5 = 0, h6 = 0;
for (let y = 0; y < size; y++) {
const mod = modules[y * size + x]!;
if (mod === runColor) {
runY++;
if (runY === 5)
result += PENALTY_N1;
else if (runY > 5)
result++;
}
else {
if (h0 === 0) runY += size;
h6 = h5; h5 = h4; h4 = h3; h3 = h2; h2 = h1; h1 = runY; h0 = runY;
if (runColor === 0) {
const core = h1 > 0 && h2 === h1 && h3 === h1 * 3 && h4 === h1 && h5 === h1;
if (core && h0 >= h1 * 4 && h6 >= h1) result += PENALTY_N3;
if (core && h6 >= h1 * 4 && h0 >= h1) result += PENALTY_N3;
}
runColor = mod;
runY = 1;
}
}
{
let currentRunLength = runY;
if (runColor === 1) {
if (h0 === 0) currentRunLength += size;
h6 = h5; h5 = h4; h4 = h3; h3 = h2; h2 = h1; h1 = currentRunLength; h0 = currentRunLength;
currentRunLength = 0;
}
currentRunLength += size;
if (h0 === 0) currentRunLength += size;
h6 = h5; h5 = h4; h4 = h3; h3 = h2; h2 = h1; h1 = currentRunLength; h0 = currentRunLength;
const core = h1 > 0 && h2 === h1 && h3 === h1 * 3 && h4 === h1 && h5 === h1;
if (core && h0 >= h1 * 4 && h6 >= h1) result += PENALTY_N3;
if (core && h6 >= h1 * 4 && h0 >= h1) result += PENALTY_N3;
}
}
// 2*2 blocks of modules having same color
for (let y = 0; y < size - 1; y++) {
const yOffset = y * size;
const nextYOffset = yOffset + size;
for (let x = 0; x < size - 1; x++) {
const color = modules[yOffset + x]!;
if (color === modules[yOffset + x + 1]
&& color === modules[nextYOffset + x]
&& color === modules[nextYOffset + x + 1])
result += PENALTY_N2;
}
}
// Balance of dark and light modules
let dark = 0;
const total = size * size;
for (let i = 0; i < total; i++)
dark += modules[i]!;
const k = Math.ceil(Math.abs(dark * 20 - total * 10) / total) - 1;
assert(k >= 0 && k <= 9);
result += k * PENALTY_N4;
assert(result >= 0 && result <= 2568888);
return result;
}
private getAlignmentPatternPositions(): number[] {
if (this.version === 1)
return [];
const numAlign = ((this.version / 7) | 0)
+ 2;
const step = (this.version === 32)
? 26
: Math.ceil((this.version * 4 + 4) / (numAlign * 2 - 2)) * 2;
const result = [6];
for (let pos = this.size - 7; result.length < numAlign; pos -= step)
result.splice(1, 0, pos);
return result;
}
}

View File

@@ -0,0 +1,82 @@
import type { QrSegmentMode } from './types';
import { ALPHANUMERIC_MAP, ALPHANUMERIC_REGEX, MODE_ALPHANUMERIC, MODE_BYTE, MODE_NUMERIC, NUMERIC_REGEX } from './constants';
import { appendBits, toUtf8ByteArray } from './utils';
/**
* A segment of character/binary/control data in a QR Code symbol.
* Instances of this class are immutable.
*/
export class QrSegment {
public constructor(
/** The mode indicator of this segment. */
public readonly mode: QrSegmentMode,
/** The length of this segment's unencoded data. */
public readonly numChars: number,
/** The data bits of this segment. */
public readonly bitData: readonly number[],
) {
if (numChars < 0)
throw new RangeError('Invalid argument');
}
}
/** Returns a segment representing the given binary data encoded in byte mode. */
export function makeBytes(data: ArrayLike<number>): QrSegment {
const bb: number[] = [];
for (let i = 0, len = data.length; i < len; i++)
appendBits(data[i]!, 8, bb);
return new QrSegment(MODE_BYTE, data.length, bb);
}
/** Returns a segment representing the given string of decimal digits encoded in numeric mode. */
export function makeNumeric(digits: string): QrSegment {
if (!isNumeric(digits))
throw new RangeError('String contains non-numeric characters');
const bb: number[] = [];
for (let i = 0; i < digits.length;) {
const n = Math.min(digits.length - i, 3);
appendBits(Number.parseInt(digits.slice(i, i + n), 10), n * 3 + 1, bb);
i += n;
}
return new QrSegment(MODE_NUMERIC, digits.length, bb);
}
/** Returns a segment representing the given text string encoded in alphanumeric mode. */
export function makeAlphanumeric(text: string): QrSegment {
if (!isAlphanumeric(text))
throw new RangeError('String contains unencodable characters in alphanumeric mode');
const bb: number[] = [];
let i: number;
for (i = 0; i + 2 <= text.length; i += 2) {
let temp = ALPHANUMERIC_MAP[text.charCodeAt(i)]! * 45;
temp += ALPHANUMERIC_MAP[text.charCodeAt(i + 1)]!;
appendBits(temp, 11, bb);
}
if (i < text.length)
appendBits(ALPHANUMERIC_MAP[text.charCodeAt(i)]!, 6, bb);
return new QrSegment(MODE_ALPHANUMERIC, text.length, bb);
}
/**
* Returns a new mutable list of zero or more segments to represent the given Unicode text string.
* The result may use various segment modes and switch modes to optimize the length of the bit stream.
*/
export function makeSegments(text: string): QrSegment[] {
if (text === '')
return [];
if (isNumeric(text))
return [makeNumeric(text)];
if (isAlphanumeric(text))
return [makeAlphanumeric(text)];
return [makeBytes(toUtf8ByteArray(text))];
}
/** Tests whether the given string can be encoded as a segment in numeric mode. */
export function isNumeric(text: string): boolean {
return NUMERIC_REGEX.test(text);
}
/** Tests whether the given string can be encoded as a segment in alphanumeric mode. */
export function isAlphanumeric(text: string): boolean {
return ALPHANUMERIC_REGEX.test(text);
}

View File

@@ -0,0 +1,17 @@
export type QrCodeEcc = readonly [ordinal: number, formatBits: number];
export type QrSegmentMode = [
modeBits: number,
numBitsCharCount1: number,
numBitsCharCount2: number,
numBitsCharCount3: number,
];
export enum QrCodeDataType {
Border = -1,
Data = 0,
Function = 1,
Position = 2,
Timing = 3,
Alignment = 4,
}

View File

@@ -0,0 +1,79 @@
import type { QrCodeEcc, QrSegmentMode } from './types';
import { ECC_CODEWORDS_PER_BLOCK, MAX_VERSION, MIN_VERSION, NUM_ERROR_CORRECTION_BLOCKS } from './constants';
import type { QrSegment } from './segment';
const utf8Encoder = new TextEncoder();
/** Appends the given number of low-order bits of the given value to the buffer. */
export function appendBits(val: number, len: number, bb: number[]): void {
if (len < 0 || len > 31 || val >>> len !== 0)
throw new RangeError('Value out of range');
for (let i = len - 1; i >= 0; i--)
bb.push((val >>> i) & 1);
}
/** Returns true iff the i'th bit of x is set to 1. */
export function getBit(x: number, i: number): boolean {
return ((x >>> i) & 1) !== 0;
}
/** Throws an exception if the given condition is false. */
export function assert(cond: boolean): asserts cond {
if (!cond)
throw new Error('Assertion error');
}
/** Returns a Uint8Array representing the given string encoded in UTF-8. */
export function toUtf8ByteArray(str: string): Uint8Array {
return utf8Encoder.encode(str);
}
/** Returns the bit width of the character count field for a segment in this mode at the given version number. */
export function numCharCountBits(mode: QrSegmentMode, ver: number): number {
return mode[((ver + 7) / 17 | 0) + 1]!;
}
/**
* Returns the number of data bits that can be stored in a QR Code of the given version number,
* after all function modules are excluded. This includes remainder bits, so it might not be a multiple of 8.
* The result is in the range [208, 29648].
*/
export function getNumRawDataModules(ver: number): number {
if (ver < MIN_VERSION || ver > MAX_VERSION)
throw new RangeError('Version number out of range');
let result = (16 * ver + 128) * ver + 64;
if (ver >= 2) {
const numAlign = (ver / 7 | 0) + 2;
result -= (25 * numAlign - 10) * numAlign - 55;
if (ver >= 7)
result -= 36;
}
assert(result >= 208 && result <= 29648);
return result;
}
/**
* Returns the number of 8-bit data (i.e. not error correction) codewords contained in any
* QR Code of the given version number and error correction level, with remainder bits discarded.
*/
export function getNumDataCodewords(ver: number, ecl: QrCodeEcc): number {
return (getNumRawDataModules(ver) / 8 | 0)
- ECC_CODEWORDS_PER_BLOCK[ecl[0]]![ver]!
* NUM_ERROR_CORRECTION_BLOCKS[ecl[0]]![ver]!;
}
/**
* Calculates and returns the number of bits needed to encode the given segments at the given version.
* The result is infinity if a segment has too many characters to fit its length field.
*/
export function getTotalBits(segs: Readonly<QrSegment[]>, version: number): number {
let result = 0;
for (const seg of segs) {
const ccbits = numCharCountBits(seg.mode, version);
if (seg.numChars >= (1 << ccbits))
return Number.POSITIVE_INFINITY;
result += 4 + ccbits + seg.bitData.length;
}
return result;
}

View File

@@ -0,0 +1,100 @@
import { describe, expect, it } from 'vitest';
import { computeDivisor, computeRemainder, multiply } from '.';
describe('multiply', () => {
it('multiplies zero by anything to get zero', () => {
expect(multiply(0, 0)).toBe(0);
expect(multiply(0, 1)).toBe(0);
expect(multiply(0, 255)).toBe(0);
expect(multiply(1, 0)).toBe(0);
});
it('multiplies by one (identity)', () => {
expect(multiply(1, 1)).toBe(1);
expect(multiply(1, 42)).toBe(42);
expect(multiply(42, 1)).toBe(42);
expect(multiply(1, 255)).toBe(255);
});
it('is commutative', () => {
expect(multiply(5, 7)).toBe(multiply(7, 5));
expect(multiply(0x53, 0xCA)).toBe(multiply(0xCA, 0x53));
expect(multiply(100, 200)).toBe(multiply(200, 100));
});
it('produces known GF(2^8) products', () => {
expect(multiply(2, 2)).toBe(4);
expect(multiply(2, 0x80)).toBe(0x1D);
});
it('throws on out of range inputs', () => {
expect(() => multiply(256, 0)).toThrow(RangeError);
expect(() => multiply(0, 256)).toThrow(RangeError);
expect(() => multiply(1000, 1000)).toThrow(RangeError);
});
});
describe('computeDivisor', () => {
it('computes a degree-1 divisor', () => {
expect(computeDivisor(1)).toEqual(Uint8Array.from([1]));
});
it('computes a degree-2 divisor', () => {
const result = computeDivisor(2);
expect(result).toHaveLength(2);
expect(result).toEqual(Uint8Array.from([3, 2]));
});
it('has correct length for arbitrary degrees', () => {
expect(computeDivisor(7)).toHaveLength(7);
expect(computeDivisor(10)).toHaveLength(10);
expect(computeDivisor(30)).toHaveLength(30);
});
it('returns Uint8Array', () => {
expect(computeDivisor(5)).toBeInstanceOf(Uint8Array);
});
it('throws on degree out of range', () => {
expect(() => computeDivisor(0)).toThrow(RangeError);
expect(() => computeDivisor(256)).toThrow(RangeError);
expect(() => computeDivisor(-1)).toThrow(RangeError);
});
});
describe('computeRemainder', () => {
it('returns zero remainder for empty data', () => {
const divisor = computeDivisor(4);
const result = computeRemainder([], divisor);
expect(result).toEqual(new Uint8Array(4));
});
it('produces non-zero remainder for non-empty data', () => {
const divisor = computeDivisor(7);
const data = [0x40, 0xD2, 0x75, 0x47, 0x76, 0x17, 0x32, 0x06, 0x27, 0x26, 0x96, 0xC6, 0xC6, 0x96, 0x70, 0xEC];
const result = computeRemainder(data, divisor);
expect(result).toHaveLength(7);
expect(result).toBeInstanceOf(Uint8Array);
for (const b of result) {
expect(b).toBeGreaterThanOrEqual(0);
expect(b).toBeLessThanOrEqual(255);
}
});
it('accepts Uint8Array as data input', () => {
const divisor = computeDivisor(7);
const data = Uint8Array.from([0x40, 0xD2, 0x75, 0x47]);
const result = computeRemainder(data, divisor);
expect(result).toHaveLength(7);
expect(result).toBeInstanceOf(Uint8Array);
});
it('remainder length matches divisor length', () => {
for (const degree of [1, 5, 10, 20]) {
const divisor = computeDivisor(degree);
const data = [1, 2, 3, 4, 5];
const result = computeRemainder(data, divisor);
expect(result).toHaveLength(degree);
}
});
});

View File

@@ -0,0 +1,92 @@
/*
* Reed-Solomon error correction over GF(2^8/0x11D)
*
* Based on Project Nayuki's QR Code generator library (MIT License)
* https://www.nayuki.io/page/qr-code-generator-library
*/
/* -- GF(2^8) exp/log lookup tables (generator α=0x02, primitive polynomial 0x11D) -- */
const GF_EXP = new Uint8Array(256);
const GF_LOG = new Uint8Array(256);
{
let x = 1;
for (let i = 0; i < 255; i++) {
GF_EXP[i] = x;
GF_LOG[x] = i;
x = (x << 1) ^ ((x >>> 7) * 0x11D);
}
GF_EXP[255] = GF_EXP[0]!;
}
/**
* Returns the product of the two given field elements modulo GF(2^8/0x11D).
* The arguments and result are unsigned 8-bit integers.
*/
export function multiply(x: number, y: number): number {
if (x >>> 8 !== 0 || y >>> 8 !== 0)
throw new RangeError('Byte out of range');
if (x === 0 || y === 0)
return 0;
return GF_EXP[(GF_LOG[x]! + GF_LOG[y]!) % 255]!;
}
/**
* Returns a Reed-Solomon ECC generator polynomial for the given degree.
*
* Polynomial coefficients are stored from highest to lowest power, excluding the leading term which is always 1.
* For example the polynomial x^3 + 255x^2 + 8x + 93 is stored as the uint8 array [255, 8, 93].
*/
export function computeDivisor(degree: number): Uint8Array {
if (degree < 1 || degree > 255)
throw new RangeError('Degree out of range');
const result = new Uint8Array(degree);
result[degree - 1] = 1;
// Compute the product polynomial (x - r^0) * (x - r^1) * ... * (x - r^{degree-1}),
// dropping the leading term which is always 1x^degree.
// r = 0x02, a generator element of GF(2^8/0x11D).
let root = 0; // GF_LOG[1] = 0, i.e. α^0 = 1
for (let i = 0; i < degree; i++) {
// Multiply the current product by (x - r^i)
for (let j = 0; j < degree; j++) {
// result[j] = multiply(result[j], α^root) — inlined for performance
if (result[j] !== 0)
result[j] = GF_EXP[(GF_LOG[result[j]!]! + root) % 255]!;
if (j + 1 < degree)
result[j]! ^= result[j + 1]!;
}
root = (root + 1) % 255; // root tracks log(α^i) = i mod 255
}
return result;
}
/**
* Returns the Reed-Solomon error correction codeword for the given data and divisor polynomials.
*/
export function computeRemainder(data: ArrayLike<number>, divisor: Uint8Array): Uint8Array {
const len = divisor.length;
const result = new Uint8Array(len);
for (let d = 0, dLen = data.length; d < dLen; d++) {
const factor = data[d]! ^ result[0]!;
// Shift left by 1 position (native memcpy)
result.copyWithin(0, 1);
result[len - 1] = 0;
// XOR with divisor scaled by factor — inlined GF multiply for performance
if (factor !== 0) {
const logFactor = GF_LOG[factor]!;
for (let i = 0; i < len; i++) {
if (divisor[i] !== 0)
result[i]! ^= GF_EXP[(GF_LOG[divisor[i]!]! + logFactor) % 255]!;
}
}
}
return result;
}

View File

@@ -0,0 +1,3 @@
{
"extends": "@robonen/tsconfig/tsconfig.json"
}

View File

@@ -0,0 +1,7 @@
import { defineConfig } from 'tsdown';
import { sharedConfig } from '@robonen/tsdown';
export default defineConfig({
...sharedConfig,
entry: ['src/index.ts'],
});

View File

@@ -0,0 +1,7 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'node',
},
});

View File

@@ -1,15 +1,15 @@
import { defineConfig } from 'oxlint';
import { compose, base, typescript, imports } from '@robonen/oxlint';
import { compose, base, typescript, imports, stylistic } from '@robonen/oxlint';
export default defineConfig(
compose(base, typescript, imports, {
overrides: [
{
files: ['src/multi/global/index.ts'],
rules: {
'unicorn/prefer-global-this': 'off',
},
},
],
}),
compose(base, typescript, imports, stylistic, {
overrides: [
{
files: ['src/multi/global/index.ts'],
rules: {
'unicorn/prefer-global-this': 'off',
},
},
],
}),
);

View File

@@ -18,7 +18,7 @@
"url": "git+https://github.com/robonen/tools.git",
"directory": "packages/platform"
},
"packageManager": "pnpm@10.29.3",
"packageManager": "pnpm@10.30.3",
"engines": {
"node": ">=24.13.1"
},
@@ -39,7 +39,8 @@
}
},
"scripts": {
"lint": "oxlint -c oxlint.config.ts",
"lint:check": "oxlint -c oxlint.config.ts",
"lint:fix": "oxlint -c oxlint.config.ts --fix",
"test": "vitest run",
"dev": "vitest dev",
"build": "tsdown"
@@ -48,6 +49,7 @@
"@robonen/oxlint": "workspace:*",
"@robonen/tsconfig": "workspace:*",
"@robonen/tsdown": "workspace:*",
"@stylistic/eslint-plugin": "catalog:",
"oxlint": "catalog:",
"tsdown": "catalog:"
}

View File

@@ -8,7 +8,7 @@ describe('focusGuard', () => {
it('initialize with the correct default namespace', () => {
const guard = focusGuard();
expect(guard.selector).toBe('data-focus-guard');
});
@@ -31,7 +31,7 @@ describe('focusGuard', () => {
guard.removeGuard();
const guards = document.querySelectorAll(`[${guard.selector}]`);
expect(guards.length).toBe(0);
});
@@ -66,4 +66,4 @@ describe('focusGuard', () => {
expect(element.getAttribute('tabindex')).toBe('0');
expect(element.getAttribute('style')).toBe('outline: none; opacity: 0; pointer-events: none; position: fixed;');
});
});
});

View File

@@ -2,20 +2,20 @@
* @name focusGuard
* @category Browsers
* @description Adds a pair of focus guards at the boundaries of the DOM tree to ensure consistent focus behavior
*
*
* @param {string} namespace - The namespace to use for the guard attributes
* @returns {Object} - An object containing the selector, createGuard, and removeGuard functions
*
*
* @example
* const guard = focusGuard();
* guard.createGuard();
* guard.removeGuard();
*
*
* @example
* const guard = focusGuard('focus-guard');
* guard.createGuard();
* guard.removeGuard();
*
*
* @since 0.0.3
*/
export function focusGuard(namespace = 'focus-guard') {
@@ -29,7 +29,7 @@ export function focusGuard(namespace = 'focus-guard') {
};
const removeGuard = () => {
document.querySelectorAll(`[${guardAttr}]`).forEach((element) => element.remove());
document.querySelectorAll(`[${guardAttr}]`).forEach(element => element.remove());
};
return {
@@ -47,4 +47,4 @@ export function createGuardAttrs(namespace = 'focus-guard') {
element.setAttribute('style', 'outline: none; opacity: 0; pointer-events: none; position: fixed;');
return element;
}
}

View File

@@ -1 +1 @@
export * from './focusGuard';
export * from './focusGuard';

View File

@@ -7,8 +7,8 @@
*
* @since 0.0.1
*/
export const _global =
typeof globalThis !== 'undefined'
export const _global
= typeof globalThis !== 'undefined'
? globalThis
: typeof window !== 'undefined'
? window
@@ -22,7 +22,7 @@ export const _global =
* @name isClient
* @category Multi
* @description Check if the current environment is the client
*
*
* @since 0.0.1
*/
export const isClient = typeof window !== 'undefined' && typeof document !== 'undefined';
export const isClient = typeof window !== 'undefined' && typeof document !== 'undefined';

View File

@@ -1,2 +1,2 @@
export * from './global';
// export * from './debounce';
// export * from './debounce';

View File

@@ -7,4 +7,4 @@ export default defineConfig({
browsers: 'src/browsers/index.ts',
multi: 'src/multi/index.ts',
},
});
});

View File

@@ -5,4 +5,3 @@ export default defineConfig({
environment: 'jsdom',
},
});

View File

@@ -1,4 +1,4 @@
import { defineConfig } from 'oxlint';
import { compose, base, typescript, imports } from '@robonen/oxlint';
import { compose, base, typescript, imports, stylistic } from '@robonen/oxlint';
export default defineConfig(compose(base, typescript, imports));
export default defineConfig(compose(base, typescript, imports, stylistic));

View File

@@ -18,7 +18,7 @@
"url": "git+https://github.com/robonen/tools.git",
"directory": "packages/stdlib"
},
"packageManager": "pnpm@10.29.3",
"packageManager": "pnpm@10.30.3",
"engines": {
"node": ">=24.13.1"
},
@@ -34,7 +34,8 @@
}
},
"scripts": {
"lint": "oxlint -c oxlint.config.ts",
"lint:check": "oxlint -c oxlint.config.ts",
"lint:fix": "oxlint -c oxlint.config.ts --fix",
"test": "vitest run",
"dev": "vitest dev",
"build": "tsdown"
@@ -43,6 +44,7 @@
"@robonen/oxlint": "workspace:*",
"@robonen/tsconfig": "workspace:*",
"@robonen/tsdown": "workspace:*",
"@stylistic/eslint-plugin": "catalog:",
"oxlint": "catalog:",
"tsdown": "catalog:"
}

View File

@@ -34,7 +34,7 @@ describe('cluster', () => {
it('return an empty array if the input array is empty', () => {
const result = cluster([], 3);
expect(result).toEqual([]);
});
});
});

View File

@@ -2,17 +2,17 @@
* @name cluster
* @category Arrays
* @description Cluster an array into subarrays of a specific size
*
*
* @param {Value[]} arr The array to cluster
* @param {number} size The size of each cluster
* @returns {Value[][]} The clustered array
*
*
* @example
* cluster([1, 2, 3, 4, 5, 6, 7, 8], 3) // => [[1, 2, 3], [4, 5, 6], [7, 8]]
*
*
* @example
* cluster([1, 2, 3, 4], -1) // => []
*
*
* @since 0.0.3
*/
export function cluster<Value>(arr: Value[], size: number): Value[][] {

View File

@@ -20,4 +20,4 @@ describe('first', () => {
expect(first([1, 2, 3], 42)).toBe(1);
expect(first(['a', 'b', 'c'], 'default')).toBe('a');
});
});
});

View File

@@ -2,19 +2,19 @@
* @name first
* @category Arrays
* @description Returns the first element of an array
*
*
* @param {Value[]} arr The array to get the first element from
* @param {Value} [defaultValue] The default value to return if the array is empty
* @returns {Value | undefined} The first element of the array, or the default value if the array is empty
*
*
* @example
* first([1, 2, 3]); // => 1
*
*
* @example
* first([]); // => undefined
*
*
* @since 0.0.3
*/
export function first<Value>(arr: Value[], defaultValue?: Value) {
return arr[0] ?? defaultValue;
}
return arr[0] ?? defaultValue;
}

View File

@@ -2,4 +2,4 @@ export * from './cluster';
export * from './first';
export * from './last';
export * from './sum';
export * from './unique';
export * from './unique';

View File

@@ -20,4 +20,4 @@ describe('last', () => {
expect(last([1, 2, 3], 42)).toBe(3);
expect(last(['a', 'b', 'c'], 'default')).toBe('c');
});
});
});

View File

@@ -2,17 +2,17 @@
* @name last
* @section Arrays
* @description Gets the last element of an array
*
*
* @param {Value[]} arr The array to get the last element of
* @param {Value} [defaultValue] The default value to return if the array is empty
* @returns {Value | undefined} The last element of the array, or the default value if the array is empty
*
*
* @example
* last([1, 2, 3, 4, 5]); // => 5
*
*
* @example
* last([], 3); // => 3
*
*
* @since 0.0.3
*/
export function last<Value>(arr: Value[], defaultValue?: Value) {

View File

@@ -15,7 +15,7 @@ describe('sum', () => {
});
it('return the sum of all elements using a getValue function', () => {
const result = sum([{ value: 1 }, { value: 2 }, { value: 3 }], (item) => item.value);
const result = sum([{ value: 1 }, { value: 2 }, { value: 3 }], item => item.value);
expect(result).toBe(6);
});
@@ -39,8 +39,8 @@ describe('sum', () => {
});
it('handle arrays with a getValue function returning floating point numbers', () => {
const result = sum([{ value: 1.5 }, { value: 2.5 }, { value: 3.5 }], (item) => item.value);
const result = sum([{ value: 1.5 }, { value: 2.5 }, { value: 3.5 }], item => item.value);
expect(result).toBe(7.5);
});
});
});

View File

@@ -2,16 +2,16 @@
* @name sum
* @category Arrays
* @description Returns the sum of all the elements in an array
*
*
* @param {Value[]} array - The array to sum
* @param {(item: Value) => number} [getValue] - A function that returns the value to sum from each element in the array
* @returns {number} The sum of all the elements in the array
*
*
* @example
* sum([1, 2, 3, 4, 5]) // => 15
*
*
* sum([{ value: 1 }, { value: 2 }, { value: 3 }], (item) => item.value) // => 6
*
*
* @since 0.0.3
*/
export function sum<Value extends number>(array: Value[]): number;

View File

@@ -11,7 +11,7 @@ describe('unique', () => {
it('return an array with unique objects based on id', () => {
const result = unique(
[{ id: 1 }, { id: 2 }, { id: 1 }],
(item) => item.id,
item => item.id,
);
expect(result).toEqual([{ id: 1 }, { id: 2 }]);
@@ -33,7 +33,7 @@ describe('unique', () => {
const sym1 = Symbol('a');
const sym2 = Symbol('b');
const result = unique([sym1, sym2, sym1]);
expect(result).toEqual([sym1, sym2]);
});
@@ -42,4 +42,4 @@ describe('unique', () => {
expect(result).toEqual([]);
});
});
});

View File

@@ -5,17 +5,17 @@ export type Extractor<Value, Key extends UniqueKey> = (value: Value) => Key;
* @name unique
* @category Arrays
* @description Returns a new array with unique values from the original array
*
*
* @param {Value[]} array - The array to filter
* @param {Function} [extractor] - The function to extract the value to compare
* @returns {Value[]} - The new array with unique values
*
*
* @example
* unique([1, 2, 3, 3, 4, 5, 5, 6]) //=> [1, 2, 3, 4, 5, 6]
*
*
* @example
* unique([{ id: 1 }, { id: 2 }, { id: 1 }], (a, b) => a.id === b.id) //=> [{ id: 1 }, { id: 2 }]
*
*
* @since 0.0.3
*/
export function unique<Value, Key extends UniqueKey>(

View File

@@ -1,3 +1,3 @@
export interface AsyncPoolOptions {
concurrency?: number;
}
}

View File

@@ -16,4 +16,4 @@ describe('sleep', () => {
expect(end - start).toBeGreaterThan(delay - 5);
});
});
});

View File

@@ -2,20 +2,20 @@
* @name sleep
* @category Async
* @description Delays the execution of the current function by the specified amount of time
*
*
* @param {number} ms - The amount of time to delay the execution of the current function
* @returns {Promise<void>} - A promise that resolves after the specified amount of time
*
*
* @example
* await sleep(1000);
*
*
* @example
* sleep(1000).then(() => {
* console.log('Hello, World!');
* });
*
*
* @since 0.0.3
*/
export function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
return new Promise(resolve => setTimeout(resolve, ms));
}

View File

@@ -13,7 +13,9 @@ describe('tryIt', () => {
});
it('handle synchronous functions with errors', () => {
const syncFn = (): void => { throw new Error('Test error') };
const syncFn = (): void => {
throw new Error('Test error');
};
const wrappedSyncFn = tryIt(syncFn);
const [error, result] = wrappedSyncFn();
@@ -34,7 +36,9 @@ describe('tryIt', () => {
});
it('handle asynchronous functions with errors', async () => {
const asyncFn = async () => { throw new Error('Test error') };
const asyncFn = async () => {
throw new Error('Test error');
};
const wrappedAsyncFn = tryIt(asyncFn);
const [error, result] = await wrappedAsyncFn();
@@ -64,4 +68,4 @@ describe('tryIt', () => {
expect(error?.message).toBe('Test error');
expect(result).toBeUndefined();
});
});
});

View File

@@ -8,17 +8,17 @@ export type TryItReturn<Return> = Return extends Promise<any>
* @name tryIt
* @category Async
* @description Wraps promise-based code in a try/catch block without forking the control flow
*
*
* @param {Function} fn - The function to try
* @returns {Function} - The function that will return a tuple with the error and the result
*
*
* @example
* const wrappedFetch = tryIt(fetch);
* const [error, result] = await wrappedFetch('https://jsonplaceholder.typicode.com/todos/1');
*
*
* @example
* const [error, result] = await tryIt(fetch)('https://jsonplaceholder.typicode.com/todos/1');
*
*
* @since 0.0.3
*/
export function tryIt<Args extends any[], Return>(
@@ -30,11 +30,12 @@ export function tryIt<Args extends any[], Return>(
if (isPromise(result))
return result
.then((value) => [undefined, value])
.catch((error) => [error, undefined]) as TryItReturn<Return>;
.then(value => [undefined, value])
.catch(error => [error, undefined]) as TryItReturn<Return>;
return [undefined, result] as TryItReturn<Return>;
} catch (error) {
}
catch (error) {
return [error, undefined] as TryItReturn<Return>;
}
};

View File

@@ -2,21 +2,21 @@
* @name flagsGenerator
* @category Bits
* @description Create a function that generates unique flags
*
*
* @returns {Function} A function that generates unique flags
* @throws {RangeError} If more than 31 flags are created
*
* @since 0.0.2
*/
export function flagsGenerator() {
let lastFlag = 0;
let lastFlag = 0;
return () => {
// 31 flags is the maximum number of flags that can be created
// (without zero) because of the 32-bit integer limit in bitwise operations
if (lastFlag & 0x40000000)
throw new RangeError('Cannot create more than 31 flags');
return () => {
// 31 flags is the maximum number of flags that can be created
// (without zero) because of the 32-bit integer limit in bitwise operations
if (lastFlag & 0x40000000)
throw new RangeError('Cannot create more than 31 flags');
return (lastFlag = lastFlag === 0 ? 1 : lastFlag << 1);
};
return (lastFlag = lastFlag === 0 ? 1 : lastFlag << 1);
};
}

View File

@@ -1,7 +1,6 @@
import { describe, it, expect } from 'vitest';
import { and, or, not, has, is, unset, toggle } from '.';
describe('flagsAnd', () => {
it('no effect on zero flags', () => {
const result = and();
@@ -14,7 +13,7 @@ describe('flagsAnd', () => {
expect(result).toBe(0b1010);
});
it('perform bitwise AND operation on flags', () => {
const result = and(0b1111, 0b1010, 0b1100);
@@ -30,15 +29,15 @@ describe('flagsOr', () => {
});
it('source flag is returned if no flags are provided', () => {
const result = or(0b1010);
expect(result).toBe(0b1010);
const result = or(0b1010);
expect(result).toBe(0b1010);
});
it('perform bitwise OR operation on flags', () => {
const result = or(0b1111, 0b1010, 0b1100);
expect(result).toBe(0b1111);
const result = or(0b1111, 0b1010, 0b1100);
expect(result).toBe(0b1111);
});
});
@@ -58,9 +57,9 @@ describe('flagsHas', () => {
});
it('check if a flag has a specific bit unset', () => {
const result = has(0b1010, 0b0100);
expect(result).toBe(false);
const result = has(0b1010, 0b0100);
expect(result).toBe(false);
});
});
@@ -72,9 +71,9 @@ describe('flagsIs', () => {
});
it('check if a flag is unset', () => {
const result = is(0);
expect(result).toBe(false);
const result = is(0);
expect(result).toBe(false);
});
});
@@ -92,4 +91,4 @@ describe('flagsToggle', () => {
expect(result).toBe(0b0010);
});
});
});

View File

@@ -2,7 +2,7 @@
* @name and
* @category Bits
* @description Function to combine multiple flags using the AND operator
*
*
* @param {number[]} flags - The flags to combine
* @returns {number} The combined flags
*
@@ -16,7 +16,7 @@ export function and(...flags: number[]) {
* @name or
* @category Bits
* @description Function to combine multiple flags using the OR operator
*
*
* @param {number[]} flags - The flags to combine
* @returns {number} The combined flags
*
@@ -30,7 +30,7 @@ export function or(...flags: number[]) {
* @name not
* @category Bits
* @description Function to combine multiple flags using the XOR operator
*
*
* @param {number} flag - The flag to apply the NOT operator to
* @returns {number} The result of the NOT operator
*
@@ -44,7 +44,7 @@ export function not(flag: number) {
* @name has
* @category Bits
* @description Function to make sure a flag has a specific bit set
*
*
* @param {number} flag - The flag to check
* @param {number} other - Flag to check
* @returns {boolean} Whether the flag has the bit set
@@ -59,7 +59,7 @@ export function has(flag: number, other: number) {
* @name is
* @category Bits
* @description Function to check if a flag is set
*
*
* @param {number} flag - The flag to check
* @returns {boolean} Whether the flag is set
*
@@ -73,7 +73,7 @@ export function is(flag: number) {
* @name unset
* @category Bits
* @description Function to unset a flag
*
*
* @param {number} flag - Source flag
* @param {number} other - Flag to unset
* @returns {number} The new flag
@@ -88,7 +88,7 @@ export function unset(flag: number, other: number) {
* @name toggle
* @category Bits
* @description Function to toggle (xor) a flag
*
*
* @param {number} flag - Source flag
* @param {number} other - Flag to toggle
* @returns {number} The new flag

View File

@@ -1 +1 @@
export * from './flags';
export * from './flags';

View File

@@ -13,7 +13,7 @@ describe('BitVector', () => {
it('set and get bits correctly', () => {
const bitVector = new BitVector(16);
bitVector.setBit(5);
expect(bitVector.getBit(5)).toBe(true);
expect(bitVector.getBit(4)).toBe(false);
});
@@ -40,7 +40,7 @@ describe('BitVector', () => {
const indices = [99, 88, 66, 65, 64, 63, 15, 14, 1, 0];
const result = [];
indices.forEach(index => bitVector.setBit(index));
for (let i = bitVector.previousBit(100); i !== -1; i = bitVector.previousBit(i)) {
result.push(i);
}
@@ -60,4 +60,4 @@ describe('BitVector', () => {
expect(() => bitVector.previousBit(24)).toThrow(new RangeError('Unreachable value'));
});
});
});

View File

@@ -9,7 +9,7 @@ export interface BitVectorLike {
* @name BitVector
* @category Bits
* @description A bit vector is a vector of bits that can be used to store a collection of bits
*
*
* @since 0.0.3
*/
export class BitVector extends Uint8Array implements BitVectorLike {
@@ -58,4 +58,4 @@ export class BitVector extends Uint8Array implements BitVectorLike {
throw new RangeError('Unreachable value');
}
}
}

View File

@@ -1,14 +1,14 @@
import type { Collection, Path } from '../../types';
export type ExtractFromObject<O extends Record<PropertyKey, unknown>, K> =
K extends keyof O
export type ExtractFromObject<O extends Record<PropertyKey, unknown>, K>
= K extends keyof O
? O[K]
: K extends keyof NonNullable<O>
? NonNullable<O>[K]
: never;
export type ExtractFromArray<A extends readonly any[], K> =
any[] extends A
export type ExtractFromArray<A extends readonly any[], K>
= any[] extends A
? A extends ReadonlyArray<infer T>
? T | undefined
: undefined
@@ -16,19 +16,19 @@ export type ExtractFromArray<A extends readonly any[], K> =
? A[K]
: undefined;
export type ExtractFromCollection<O, K> =
K extends []
? O
: K extends [infer Key, ...infer Rest]
? O extends Record<PropertyKey, unknown>
? ExtractFromCollection<ExtractFromObject<O, Key>, Rest>
: O extends readonly any[]
? ExtractFromCollection<ExtractFromArray<O, Key>, Rest>
: never
: never;
export type ExtractFromCollection<O, K>
= K extends []
? O
: K extends [infer Key, ...infer Rest]
? O extends Record<PropertyKey, unknown>
? ExtractFromCollection<ExtractFromObject<O, Key>, Rest>
: O extends readonly any[]
? ExtractFromCollection<ExtractFromArray<O, Key>, Rest>
: never
: never;
type Get<O, K> = ExtractFromCollection<O, Path<K>>;
export function get<O extends Collection, K extends string>(obj: O, path: K) {
return path.split('.').reduce((acc, key) => (acc as any)?.[key], obj) as Get<O, K> | undefined;
}
}

View File

@@ -1 +1 @@
export * from './get';
export * from './get';

View File

@@ -8,4 +8,4 @@ export * from './structs';
export * from './sync';
export * from './text';
export * from './types';
export * from './utils'
export * from './utils';

View File

@@ -1,4 +1,4 @@
import { describe,it, expect } from 'vitest';
import { describe, it, expect } from 'vitest';
import { clamp } from '.';
describe('clamp', () => {
@@ -78,4 +78,4 @@ describe('clamp', () => {
// min and max are -Infinity
expect(clamp(50, -Infinity, -Infinity)).toBe(-Infinity);
});
});
});

View File

@@ -2,7 +2,7 @@
* @name clamp
* @category Math
* @description Clamps a number between a minimum and maximum value
*
*
* @param {number} value The number to clamp
* @param {number} min Minimum value
* @param {number} max Maximum value

View File

@@ -1,5 +1,5 @@
import {describe, it, expect} from 'vitest';
import {inverseLerp, lerp} from '.';
import { describe, it, expect } from 'vitest';
import { inverseLerp, lerp } from '.';
describe('lerp', () => {
it('interpolates between two values', () => {

View File

@@ -1,5 +1,5 @@
import {describe, expect, it} from 'vitest';
import {remap} from '.';
import { describe, expect, it } from 'vitest';
import { remap } from '.';
describe('remap', () => {
it('map values from one range to another', () => {
@@ -43,4 +43,4 @@ describe('remap', () => {
// input range is zero (should return output min)
expect(remap(5, 0, 0, 0, 100)).toBe(0);
});
});
});

View File

@@ -1,11 +1,11 @@
import { clamp } from '../clamp';
import {inverseLerp, lerp} from '../lerp';
import { inverseLerp, lerp } from '../lerp';
/**
* @name remap
* @category Math
* @description Map a value from one range to another
*
*
* @param {number} value The value to map
* @param {number} in_min The minimum value of the input range
* @param {number} in_max The maximum value of the input range
@@ -22,4 +22,4 @@ export function remap(value: number, in_min: number, in_max: number, out_min: nu
const clampedValue = clamp(value, in_min, in_max);
return lerp(out_min, out_max, inverseLerp(in_min, in_max, clampedValue));
}
}

View File

@@ -1,5 +1,5 @@
import {describe, it, expect} from 'vitest';
import {clampBigInt} from '.';
import { describe, it, expect } from 'vitest';
import { clampBigInt } from '.';
describe('clampBigInt', () => {
it('clamp a value within the given range', () => {
@@ -32,4 +32,4 @@ describe('clampBigInt', () => {
// negative range and value
expect(clampBigInt(-10n, -100n, -5n)).toBe(-10n);
});
});
});

View File

@@ -1,5 +1,5 @@
import {minBigInt} from '../minBigInt';
import {maxBigInt} from '../maxBigInt';
import { minBigInt } from '../minBigInt';
import { maxBigInt } from '../maxBigInt';
/**
* @name clampBigInt

View File

@@ -1,5 +1,5 @@
import {describe, it, expect} from 'vitest';
import {inverseLerpBigInt, lerpBigInt} from '.';
import { describe, it, expect } from 'vitest';
import { inverseLerpBigInt, lerpBigInt } from '.';
const MAX_SAFE_INTEGER = BigInt(Number.MAX_SAFE_INTEGER);

View File

@@ -35,4 +35,4 @@ export function lerpBigInt(start: bigint, end: bigint, t: number) {
*/
export function inverseLerpBigInt(start: bigint, end: bigint, value: bigint) {
return start === end ? 0 : Number((value - start) * SCALE_N / (end - start)) / SCALE;
}
}

View File

@@ -36,4 +36,4 @@ describe('maxBigInt', () => {
const result = maxBigInt(...values);
expect(result).toBe(999n);
});
});
});

View File

@@ -14,4 +14,4 @@ export function maxBigInt(...values: bigint[]) {
throw new TypeError('maxBigInt requires at least one argument');
return values.reduce((acc, val) => val > acc ? val : acc);
}
}

View File

@@ -1,5 +1,5 @@
import {describe, it, expect} from 'vitest';
import {minBigInt} from '.';
import { describe, it, expect } from 'vitest';
import { minBigInt } from '.';
describe('minBigInt', () => {
it('returns Infinity when no values are provided', () => {
@@ -32,8 +32,8 @@ describe('minBigInt', () => {
});
it('handles a large number of bigints', () => {
const values = Array.from({length: 1000}, (_, i) => BigInt(i));
const values = Array.from({ length: 1000 }, (_, i) => BigInt(i));
const result = minBigInt(...values);
expect(result).toBe(0n);
});
});
});

View File

@@ -1,5 +1,5 @@
import {describe, expect, it} from 'vitest';
import {remapBigInt} from '.';
import { describe, expect, it } from 'vitest';
import { remapBigInt } from '.';
describe('remapBigInt', () => {
it('map values from one range to another', () => {
@@ -29,4 +29,4 @@ describe('remapBigInt', () => {
// input range is zero (should return output min)
expect(remapBigInt(5n, 0n, 0n, 0n, 100n)).toBe(0n);
});
});
});

View File

@@ -1,11 +1,11 @@
import { clampBigInt } from '../clampBigInt';
import {inverseLerpBigInt, lerpBigInt} from '../lerpBigInt';
import { inverseLerpBigInt, lerpBigInt } from '../lerpBigInt';
/**
* @name remapBigInt
* @category Math
* @description Map a bigint value from one range to another
*
*
* @param {bigint} value The value to map
* @param {bigint} in_min The minimum value of the input range
* @param {bigint} in_max The maximum value of the input range
@@ -22,4 +22,4 @@ export function remapBigInt(value: bigint, in_min: bigint, in_max: bigint, out_m
const clampedValue = clampBigInt(value, in_min, in_max);
return lerpBigInt(out_min, out_max, inverseLerpBigInt(in_min, in_max, clampedValue));
}
}

View File

@@ -47,4 +47,4 @@ describe('omit', () => {
expect(emptyTarget).toEqual({});
expect(emptyKeys).toEqual({ a: 1 });
});
});
});

View File

@@ -5,22 +5,22 @@ import type { Arrayable } from '../../types';
* @name omit
* @category Objects
* @description Returns a new object with the specified keys omitted
*
*
* @param {object} target - The object to omit keys from
* @param {Arrayable<keyof Target>} keys - The keys to omit
* @returns {Omit<Target, Key>} The new object with the specified keys omitted
*
*
* @example
* omit({ a: 1, b: 2, c: 3 }, 'a') // => { b: 2, c: 3 }
*
*
* @example
* omit({ a: 1, b: 2, c: 3 }, ['a', 'b']) // => { c: 3 }
*
*
* @since 0.0.3
*/
export function omit<Target extends object, Key extends keyof Target>(
target: Target,
keys: Arrayable<Key>
keys: Arrayable<Key>,
): Omit<Target, Key> {
const result = { ...target };
@@ -31,7 +31,8 @@ export function omit<Target extends object, Key extends keyof Target>(
for (const key of keys) {
delete result[key];
}
} else {
}
else {
delete result[keys];
}

View File

@@ -33,4 +33,4 @@ describe('pick', () => {
expect(emptyTarget).toEqual({});
expect(emptyKeys).toEqual({});
});
});
});

View File

@@ -5,22 +5,22 @@ import type { Arrayable } from '../../types';
* @name pick
* @category Objects
* @description Returns a partial copy of an object containing only the keys specified
*
*
* @param {object} target - The object to pick keys from
* @param {Arrayable<keyof Target>} keys - The keys to pick
* @returns {Pick<Target, Key>} The new object with the specified keys picked
*
*
* @example
* pick({ a: 1, b: 2, c: 3 }, 'a') // => { a: 1 }
*
*
* @example
* pick({ a: 1, b: 2, c: 3 }, ['a', 'b']) // => { a: 1, b: 2 }
*
*
* @since 0.0.3
*/
export function pick<Target extends object, Key extends keyof Target>(
target: Target,
keys: Arrayable<Key>
keys: Arrayable<Key>,
): Pick<Target, Key> {
const result = {} as Pick<Target, Key>;
@@ -31,9 +31,10 @@ export function pick<Target extends object, Key extends keyof Target>(
for (const key of keys) {
result[key] = target[key];
}
} else {
}
else {
result[keys] = target[keys];
}
return result;
}
}

View File

@@ -205,11 +205,11 @@ describe('asyncCommandHistory', () => {
function addItemAsync(item: string): AsyncCommand {
return {
execute: async () => {
await new Promise((r) => setTimeout(r, 5));
await new Promise(r => setTimeout(r, 5));
items.push(item);
},
undo: async () => {
await new Promise((r) => setTimeout(r, 5));
await new Promise(r => setTimeout(r, 5));
items.pop();
},
};

View File

@@ -40,7 +40,7 @@ export class CommandHistory extends BaseCommandHistory<Command> {
batch(commands: Command[]): void {
const macro: Command = {
execute: () => commands.forEach((c) => c.execute()),
execute: () => commands.forEach(c => c.execute()),
undo: () => {
for (let i = commands.length - 1; i >= 0; i--)
commands[i]!.undo();

View File

@@ -115,4 +115,4 @@ describe('pubsub', () => {
expect(listener).toHaveBeenCalledTimes(1);
});
});
});

View File

@@ -4,13 +4,13 @@
* @description Simple PubSub implementation
*
* @since 0.0.2
*
*
* @template Events - Event map where keys are event names and values are listener signatures
*/
export class PubSub<Events extends Record<string, (...args: any[]) => any>> {
/**
* Events map
*
*
* @private
* @type {Map<keyof Events, Set<Events[keyof Events]>>}
*/
@@ -25,7 +25,7 @@ export class PubSub<Events extends Record<string, (...args: any[]) => any>> {
/**
* Subscribe to an event
*
*
* @template {keyof Events} K
* @param {K} event Name of the event
* @param {Events[K]} listener Listener function
@@ -44,7 +44,7 @@ export class PubSub<Events extends Record<string, (...args: any[]) => any>> {
/**
* Unsubscribe from an event
*
*
* @template {keyof Events} K
* @param {K} event Name of the event
* @param {Events[K]} listener Listener function
@@ -61,7 +61,7 @@ export class PubSub<Events extends Record<string, (...args: any[]) => any>> {
/**
* Subscribe to an event only once
*
*
* @template {keyof Events} K
* @param {K} event Name of the event
* @param {Events[K]} listener Listener function
@@ -69,8 +69,8 @@ export class PubSub<Events extends Record<string, (...args: any[]) => any>> {
*/
public once<K extends keyof Events>(event: K, listener: Events[K]) {
const onceListener = (...args: Parameters<Events[K]>) => {
this.off(event, onceListener as Events[K]);
listener(...args);
this.off(event, onceListener as Events[K]);
listener(...args);
};
this.on(event, onceListener as Events[K]);
@@ -80,7 +80,7 @@ export class PubSub<Events extends Record<string, (...args: any[]) => any>> {
/**
* Emit an event
*
*
* @template {keyof Events} K
* @param {K} event Name of the event
* @param {...Parameters<Events[K]>} args Arguments for the listener
@@ -92,14 +92,14 @@ export class PubSub<Events extends Record<string, (...args: any[]) => any>> {
if (!listeners)
return false;
listeners.forEach((listener) => listener(...args));
listeners.forEach(listener => listener(...args));
return true;
}
/**
* Clear all listeners for an event
*
*
* @template {keyof Events} K
* @param {K} event Name of the event
* @returns {this}

View File

@@ -39,7 +39,8 @@ export class AsyncStateMachine<
if (isString(transition)) {
target = transition;
} else {
}
else {
if (transition.guard && !(await transition.guard(this.context)))
return this.currentState;

View File

@@ -169,7 +169,7 @@ describe('stateMachine', () => {
on: {
FAIL: {
target: 'idle',
guard: (ctx) => ctx.retries < 3,
guard: ctx => ctx.retries < 3,
},
SUCCESS: 'done',
},
@@ -255,7 +255,7 @@ describe('stateMachine', () => {
on: {
UNLOCK: {
target: 'unlocked',
guard: (ctx) => ctx.unlocked,
guard: ctx => ctx.unlocked,
},
},
exit: exitHook,
@@ -374,7 +374,7 @@ describe('stateMachine', () => {
on: {
NEXT: {
target: 'c',
guard: (ctx) => ctx.step === 1,
guard: ctx => ctx.step === 1,
action: (ctx) => { ctx.step = 2; },
},
},
@@ -434,7 +434,7 @@ describe('asyncStateMachine', () => {
on: {
GO: {
target: 'active',
guard: async (ctx) => ctx.allowed,
guard: async ctx => ctx.allowed,
},
},
},
@@ -456,7 +456,7 @@ describe('asyncStateMachine', () => {
on: {
GO: {
target: 'active',
guard: async (ctx) => ctx.allowed,
guard: async ctx => ctx.allowed,
},
},
},
@@ -483,7 +483,7 @@ describe('asyncStateMachine', () => {
FETCH: {
target: 'done',
action: async (ctx) => {
await new Promise((r) => setTimeout(r, 10));
await new Promise(r => setTimeout(r, 10));
ctx.data = 'fetched';
order.push('action');
},
@@ -513,13 +513,13 @@ describe('asyncStateMachine', () => {
a: {
on: { GO: 'b' },
exit: async () => {
await new Promise((r) => setTimeout(r, 10));
await new Promise(r => setTimeout(r, 10));
order.push('exit-a');
},
},
b: {
entry: async () => {
await new Promise((r) => setTimeout(r, 10));
await new Promise(r => setTimeout(r, 10));
order.push('entry-b');
},
},
@@ -544,7 +544,7 @@ describe('asyncStateMachine', () => {
on: {
UNLOCK: {
target: 'unlocked',
guard: async (ctx) => ctx.unlocked,
guard: async ctx => ctx.unlocked,
},
},
exit: exitHook,
@@ -573,7 +573,7 @@ describe('asyncStateMachine', () => {
on: {
GO: {
target: 'active',
guard: async (ctx) => ctx.ready,
guard: async ctx => ctx.ready,
},
},
},
@@ -667,7 +667,7 @@ describe('asyncStateMachine', () => {
on: {
GO: {
target: 'active',
guard: (ctx) => ctx.count === 0,
guard: ctx => ctx.count === 0,
action: (ctx) => { ctx.count++; },
},
},

View File

@@ -39,7 +39,8 @@ export class StateMachine<
if (isString(transition)) {
target = transition;
} else {
}
else {
if (transition.guard && !transition.guard(this.context))
return this.currentState;

View File

@@ -1,3 +1,3 @@
export * from './behavioral/Command';
export * from './behavioral/PubSub';
export * from './behavioral/StateMachine';
export * from './behavioral/StateMachine';

View File

@@ -3,227 +3,227 @@ import { describe, expect, it } from 'vitest';
import { BinaryHeap } from '.';
describe('BinaryHeap', () => {
describe('constructor', () => {
it('should create an empty heap', () => {
const heap = new BinaryHeap<number>();
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);
});
expect(heap.length).toBe(0);
expect(heap.isEmpty).toBe(true);
});
describe('push', () => {
it('should insert elements maintaining heap property', () => {
const heap = new BinaryHeap<number>();
it('should create a heap from single value', () => {
const heap = new BinaryHeap(42);
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);
});
expect(heap.length).toBe(1);
expect(heap.peek()).toBe(42);
});
describe('pop', () => {
it('should return undefined for empty heap', () => {
const heap = new BinaryHeap<number>();
it('should create a heap from array (heapify)', () => {
const heap = new BinaryHeap([5, 3, 8, 1, 4]);
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);
});
expect(heap.length).toBe(5);
expect(heap.peek()).toBe(1);
});
describe('peek', () => {
it('should return undefined for empty heap', () => {
const heap = new BinaryHeap<number>();
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()).toBeUndefined();
});
expect(heap.peek()).toBe(8);
});
});
it('should return root without removing it', () => {
const heap = new BinaryHeap([5, 3, 1]);
describe('push', () => {
it('should insert elements maintaining heap property', () => {
const heap = new BinaryHeap<number>();
expect(heap.peek()).toBe(1);
expect(heap.length).toBe(3);
});
heap.push(5);
heap.push(3);
heap.push(8);
heap.push(1);
expect(heap.peek()).toBe(1);
expect(heap.length).toBe(4);
});
describe('clear', () => {
it('should remove all elements', () => {
const heap = new BinaryHeap([1, 2, 3]);
it('should handle duplicate values', () => {
const heap = new BinaryHeap<number>();
const result = heap.clear();
heap.push(3);
heap.push(3);
heap.push(3);
expect(heap.length).toBe(0);
expect(heap.isEmpty).toBe(true);
expect(result).toBe(heap);
});
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();
});
describe('toArray', () => {
it('should return empty array for empty heap', () => {
const heap = new BinaryHeap<number>();
it('should extract elements in min-heap order', () => {
const heap = new BinaryHeap([5, 3, 8, 1, 4, 2, 7, 6]);
const sorted: number[] = [];
expect(heap.toArray()).toEqual([]);
});
while (!heap.isEmpty) {
sorted.push(heap.pop()!);
}
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);
});
expect(sorted).toEqual([1, 2, 3, 4, 5, 6, 7, 8]);
});
describe('toString', () => {
it('should return formatted string', () => {
const heap = new BinaryHeap([1, 2, 3]);
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[] = [];
expect(heap.toString()).toBe('BinaryHeap(3)');
});
while (!heap.isEmpty) {
sorted.push(heap.pop()!);
}
expect(sorted).toEqual([8, 5, 4, 3, 1]);
});
describe('iterator', () => {
it('should iterate over heap elements', () => {
const heap = new BinaryHeap([5, 3, 8, 1]);
const elements = [...heap];
it('should handle single element', () => {
const heap = new BinaryHeap(42);
expect(elements.length).toBe(4);
expect(elements[0]).toBe(1);
});
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();
});
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,
});
it('should return root without removing it', () => {
const heap = new BinaryHeap([5, 3, 1]);
expect(heap.pop()).toBe('fig');
expect(heap.pop()).toBe('kiwi');
});
expect(heap.peek()).toBe(1);
expect(heap.length).toBe(3);
});
});
it('should work with object comparator', () => {
interface Task {
priority: number;
name: string;
}
describe('clear', () => {
it('should remove all elements', () => {
const heap = new BinaryHeap([1, 2, 3]);
const heap = new BinaryHeap<Task>(
[
{ priority: 3, name: 'low' },
{ priority: 1, name: 'high' },
{ priority: 2, name: 'medium' },
],
{ comparator: (a, b) => a.priority - b.priority },
);
const result = heap.clear();
expect(heap.pop()?.name).toBe('high');
expect(heap.pop()?.name).toBe('medium');
expect(heap.pop()?.name).toBe('low');
});
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([]);
});
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[] = [];
it('should return a shallow copy', () => {
const heap = new BinaryHeap([3, 1, 2]);
const arr = heap.toArray();
while (!heap.isEmpty) {
sorted.push(heap.pop()!);
}
arr.push(99);
const expected = [...values].sort((a, b) => a - b);
expect(heap.length).toBe(3);
});
});
expect(sorted).toEqual(expected);
});
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');
});
describe('interleaved operations', () => {
it('should maintain heap property with mixed push and pop', () => {
const heap = new BinaryHeap<number>();
it('should work with object comparator', () => {
interface Task {
priority: number;
name: string;
}
heap.push(10);
heap.push(5);
expect(heap.pop()).toBe(5);
const heap = new BinaryHeap<Task>(
[
{ priority: 3, name: 'low' },
{ priority: 1, name: 'high' },
{ priority: 2, name: 'medium' },
],
{ comparator: (a, b) => a.priority - b.priority },
);
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();
});
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();
});
});
});

View File

@@ -5,7 +5,7 @@ import type { BinaryHeapLike, Comparator } from './types';
export type { BinaryHeapLike, Comparator } from './types';
export interface BinaryHeapOptions<T> {
comparator?: Comparator<T>;
comparator?: Comparator<T>;
}
/**
@@ -27,194 +27,194 @@ const defaultComparator: Comparator<any> = (a: number, b: number) => a - b;
* @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>;
private readonly comparator: Comparator<T>;
/**
/**
* Internal flat array backing the heap
*
* @private
* @type {T[]}
*/
private readonly heap: 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;
constructor(initialValues?: T[] | T, options?: BinaryHeapOptions<T>) {
this.comparator = options?.comparator ?? defaultComparator;
if (initialValues !== null && initialValues !== undefined) {
const items = isArray(initialValues) ? initialValues : [initialValues];
this.heap.push(...items);
this.heapify();
}
if (initialValues !== null && initialValues !== undefined) {
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;
}
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;
}
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);
}
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;
public pop(): T | undefined {
if (this.heap.length === 0) return undefined;
const root = first(this.heap)!;
const last = this.heap.pop()!;
const root = first(this.heap)!;
const last = this.heap.pop()!;
if (this.heap.length > 0) {
this.heap[0] = last;
this.siftDown(0);
}
return root;
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);
}
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;
}
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();
}
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})`;
}
public toString(): string {
return `BinaryHeap(${this.heap.length})`;
}
/**
/**
* Iterator over heap elements in heap order
*/
public *[Symbol.iterator](): Iterator<T> {
yield* this.heap;
}
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;
}
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;
private siftUp(index: number): void {
const heap = this.heap;
const cmp = this.comparator;
while (index > 0) {
const parent = (index - 1) >> 1;
while (index > 0) {
const parent = (index - 1) >> 1;
if (cmp(heap[index]!, heap[parent]!) >= 0) break;
if (cmp(heap[index]!, heap[parent]!) >= 0) break;
const temp = heap[index]!;
heap[index] = heap[parent]!;
heap[parent] = temp;
const temp = heap[index]!;
heap[index] = heap[parent]!;
heap[parent] = temp;
index = parent;
}
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;
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;
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 (left < length && cmp(heap[left]!, heap[smallest]!) < 0) {
smallest = left;
}
if (right < length && cmp(heap[right]!, heap[smallest]!) < 0) {
smallest = right;
}
if (right < length && cmp(heap[right]!, heap[smallest]!) < 0) {
smallest = right;
}
if (smallest === index) break;
if (smallest === index) break;
const temp = heap[index]!;
heap[index] = heap[smallest]!;
heap[smallest] = temp;
const temp = heap[index]!;
heap[index] = heap[smallest]!;
heap[smallest] = temp;
index = smallest;
}
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);
}
private heapify(): void {
for (let i = (this.heap.length >> 1) - 1; i >= 0; i--) {
this.siftDown(i);
}
}
}

View File

@@ -234,7 +234,7 @@ export class CircularBuffer<T> implements CircularBufferLike<T> {
*
* @returns {AsyncIterableIterator<T>}
*/
async *[Symbol.asyncIterator]() {
async* [Symbol.asyncIterator]() {
for (const element of this)
yield element;
}

View File

@@ -173,7 +173,7 @@ export class Deque<T> implements DequeLike<T> {
*
* @returns {AsyncIterableIterator<T>}
*/
async *[Symbol.asyncIterator]() {
async* [Symbol.asyncIterator]() {
for (const element of this.buffer)
yield element;
}

View File

@@ -3,404 +3,404 @@ import { describe, expect, it } from 'vitest';
import { LinkedList } from '.';
describe('LinkedList', () => {
describe('constructor', () => {
it('should create an empty list', () => {
const list = new LinkedList<number>();
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);
});
expect(list.length).toBe(0);
expect(list.isEmpty).toBe(true);
expect(list.head).toBeUndefined();
expect(list.tail).toBeUndefined();
});
describe('pushBack', () => {
it('should append to empty list', () => {
const list = new LinkedList<number>();
it('should create a list from single value', () => {
const list = new LinkedList(42);
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();
});
expect(list.length).toBe(1);
expect(list.peekFront()).toBe(42);
expect(list.peekBack()).toBe(42);
});
describe('pushFront', () => {
it('should prepend to empty list', () => {
const list = new LinkedList<number>();
it('should create a list from array', () => {
const list = new LinkedList([1, 2, 3]);
const node = list.pushFront(1);
expect(list.length).toBe(3);
expect(list.peekFront()).toBe(1);
expect(list.peekBack()).toBe(3);
});
});
expect(list.length).toBe(1);
expect(list.head).toBe(node);
expect(list.tail).toBe(node);
});
describe('pushBack', () => {
it('should append to empty list', () => {
const list = new LinkedList<number>();
it('should prepend to non-empty list', () => {
const list = new LinkedList([2, 3]);
const node = list.pushBack(1);
list.pushFront(1);
expect(list.length).toBe(3);
expect(list.peekFront()).toBe(1);
expect(list.peekBack()).toBe(3);
});
expect(list.length).toBe(1);
expect(node.value).toBe(1);
expect(list.head).toBe(node);
expect(list.tail).toBe(node);
});
describe('popBack', () => {
it('should return undefined for empty list', () => {
const list = new LinkedList<number>();
it('should append to non-empty list', () => {
const list = new LinkedList([1, 2]);
expect(list.popBack()).toBeUndefined();
});
list.pushBack(3);
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();
});
expect(list.length).toBe(3);
expect(list.peekBack()).toBe(3);
expect(list.peekFront()).toBe(1);
});
describe('popFront', () => {
it('should return undefined for empty list', () => {
const list = new LinkedList<number>();
it('should return the created node', () => {
const list = new LinkedList<number>();
expect(list.popFront()).toBeUndefined();
});
const node = list.pushBack(5);
it('should remove and return first value', () => {
const list = new LinkedList([1, 2, 3]);
expect(node.value).toBe(5);
expect(node.prev).toBeUndefined();
expect(node.next).toBeUndefined();
});
});
expect(list.popFront()).toBe(1);
expect(list.length).toBe(2);
expect(list.peekFront()).toBe(2);
});
describe('pushFront', () => {
it('should prepend to empty list', () => {
const list = new LinkedList<number>();
it('should handle single element', () => {
const list = new LinkedList(1);
const node = list.pushFront(1);
expect(list.popFront()).toBe(1);
expect(list.isEmpty).toBe(true);
expect(list.head).toBeUndefined();
expect(list.tail).toBeUndefined();
});
expect(list.length).toBe(1);
expect(list.head).toBe(node);
expect(list.tail).toBe(node);
});
describe('peekBack', () => {
it('should return undefined for empty list', () => {
const list = new LinkedList<number>();
it('should prepend to non-empty list', () => {
const list = new LinkedList([2, 3]);
expect(list.peekBack()).toBeUndefined();
});
list.pushFront(1);
it('should return last value without removing', () => {
const list = new LinkedList([1, 2, 3]);
expect(list.length).toBe(3);
expect(list.peekFront()).toBe(1);
expect(list.peekBack()).toBe(3);
});
});
expect(list.peekBack()).toBe(3);
expect(list.length).toBe(3);
});
describe('popBack', () => {
it('should return undefined for empty list', () => {
const list = new LinkedList<number>();
expect(list.popBack()).toBeUndefined();
});
describe('peekFront', () => {
it('should return undefined for empty list', () => {
const list = new LinkedList<number>();
it('should remove and return last value', () => {
const list = new LinkedList([1, 2, 3]);
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);
});
expect(list.popBack()).toBe(3);
expect(list.length).toBe(2);
expect(list.peekBack()).toBe(2);
});
describe('insertBefore', () => {
it('should insert before head', () => {
const list = new LinkedList<number>();
const node = list.pushBack(2);
it('should handle single element', () => {
const list = new LinkedList(1);
list.insertBefore(node, 1);
expect(list.popBack()).toBe(1);
expect(list.isEmpty).toBe(true);
expect(list.head).toBeUndefined();
expect(list.tail).toBeUndefined();
});
});
expect(list.peekFront()).toBe(1);
expect(list.peekBack()).toBe(2);
expect(list.length).toBe(2);
});
describe('popFront', () => {
it('should return undefined for empty list', () => {
const list = new LinkedList<number>();
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);
});
expect(list.popFront()).toBeUndefined();
});
describe('insertAfter', () => {
it('should insert after tail', () => {
const list = new LinkedList<number>();
const node = list.pushBack(1);
it('should remove and return first value', () => {
const list = new LinkedList([1, 2, 3]);
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);
});
expect(list.popFront()).toBe(1);
expect(list.length).toBe(2);
expect(list.peekFront()).toBe(2);
});
describe('remove', () => {
it('should remove head node', () => {
const list = new LinkedList([1, 2, 3]);
const head = list.head!;
it('should handle single element', () => {
const list = new LinkedList(1);
const value = list.remove(head);
expect(list.popFront()).toBe(1);
expect(list.isEmpty).toBe(true);
expect(list.head).toBeUndefined();
expect(list.tail).toBeUndefined();
});
});
expect(value).toBe(1);
expect(list.length).toBe(2);
expect(list.peekFront()).toBe(2);
});
describe('peekBack', () => {
it('should return undefined for empty list', () => {
const list = new LinkedList<number>();
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();
});
expect(list.peekBack()).toBeUndefined();
});
describe('clear', () => {
it('should remove all elements', () => {
const list = new LinkedList([1, 2, 3]);
it('should return last value without removing', () => {
const list = new LinkedList([1, 2, 3]);
const result = list.clear();
expect(list.peekBack()).toBe(3);
expect(list.length).toBe(3);
});
});
expect(list.length).toBe(0);
expect(list.isEmpty).toBe(true);
expect(list.head).toBeUndefined();
expect(list.tail).toBeUndefined();
expect(result).toBe(list);
});
describe('peekFront', () => {
it('should return undefined for empty list', () => {
const list = new LinkedList<number>();
expect(list.peekFront()).toBeUndefined();
});
describe('toArray', () => {
it('should return empty array for empty list', () => {
const list = new LinkedList<number>();
it('should return first value without removing', () => {
const list = new LinkedList([1, 2, 3]);
expect(list.toArray()).toEqual([]);
});
expect(list.peekFront()).toBe(1);
expect(list.length).toBe(3);
});
});
it('should return values from head to tail', () => {
const list = new LinkedList([1, 2, 3]);
describe('insertBefore', () => {
it('should insert before head', () => {
const list = new LinkedList<number>();
const node = list.pushBack(2);
expect(list.toArray()).toEqual([1, 2, 3]);
});
list.insertBefore(node, 1);
expect(list.peekFront()).toBe(1);
expect(list.peekBack()).toBe(2);
expect(list.length).toBe(2);
});
describe('toString', () => {
it('should return comma-separated values', () => {
const list = new LinkedList([1, 2, 3]);
it('should insert before middle node', () => {
const list = new LinkedList([1, 3]);
const tail = list.tail!;
expect(list.toString()).toBe('1,2,3');
});
list.insertBefore(tail, 2);
expect(list.toArray()).toEqual([1, 2, 3]);
});
describe('iterator', () => {
it('should iterate from head to tail', () => {
const list = new LinkedList([1, 2, 3]);
it('should return the created node', () => {
const list = new LinkedList<number>();
const existing = list.pushBack(2);
expect([...list]).toEqual([1, 2, 3]);
});
const newNode = list.insertBefore(existing, 1);
it('should yield nothing for empty list', () => {
const list = new LinkedList<number>();
expect(newNode.value).toBe(1);
expect(newNode.next).toBe(existing);
});
});
expect([...list]).toEqual([]);
});
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);
});
describe('async iterator', () => {
it('should async iterate from head to tail', async () => {
const list = new LinkedList([1, 2, 3]);
const result: number[] = [];
it('should insert after middle node', () => {
const list = new LinkedList([1, 3]);
const head = list.head!;
for await (const value of list)
result.push(value);
list.insertAfter(head, 2);
expect(result).toEqual([1, 2, 3]);
});
expect(list.toArray()).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);
it('should return the created node', () => {
const list = new LinkedList<number>();
const existing = list.pushBack(1);
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();
});
const newNode = list.insertAfter(existing, 2);
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);
expect(newNode.value).toBe(2);
expect(newNode.prev).toBe(existing);
});
});
list.remove(b);
describe('remove', () => {
it('should remove head node', () => {
const list = new LinkedList([1, 2, 3]);
const head = list.head!;
expect(a.next).toBe(c);
expect(c.prev).toBe(a);
});
const value = list.remove(head);
expect(value).toBe(1);
expect(list.length).toBe(2);
expect(list.peekFront()).toBe(2);
});
describe('interleaved operations', () => {
it('should handle mixed push/pop from both ends', () => {
const list = new LinkedList<number>();
it('should remove tail node', () => {
const list = new LinkedList([1, 2, 3]);
const tail = list.tail!;
list.pushBack(1);
list.pushBack(2);
list.pushFront(0);
const value = list.remove(tail);
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]);
});
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]);
});
});
});

View File

@@ -11,7 +11,7 @@ export type { LinkedListLike, LinkedListNode } from './types';
* @returns {LinkedListNode<T>} The created node
*/
function createNode<T>(value: T): LinkedListNode<T> {
return { value, prev: undefined, next: undefined };
return { value, prev: undefined, next: undefined };
}
/**
@@ -24,301 +24,307 @@ function createNode<T>(value: T): LinkedListNode<T> {
* @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 = 0;
private count = 0;
/**
/**
* The first node in the list
*
* @private
* @type {LinkedListNode<T> | undefined}
*/
private first: LinkedListNode<T> | undefined;
private first: LinkedListNode<T> | undefined;
/**
/**
* The last node in the list
*
* @private
* @type {LinkedListNode<T> | undefined}
*/
private last: 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 && initialValues !== undefined) {
const items = isArray(initialValues) ? initialValues : [initialValues];
constructor(initialValues?: T[] | T) {
if (initialValues !== null && initialValues !== undefined) {
const items = isArray(initialValues) ? initialValues : [initialValues];
for (const item of items)
this.pushBack(item);
}
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;
}
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;
}
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;
}
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;
}
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);
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;
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);
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;
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;
public popBack(): T | undefined {
if (!this.last) return undefined;
const node = this.last;
const node = this.last;
this.detach(node);
this.detach(node);
return node.value;
}
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;
public popFront(): T | undefined {
if (!this.first) return undefined;
const node = this.first;
const node = this.first;
this.detach(node);
this.detach(node);
return node.value;
}
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;
}
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;
}
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);
public insertBefore(node: LinkedListNode<T>, value: T): LinkedListNode<T> {
const newNode = createNode(value);
newNode.next = node;
newNode.prev = node.prev;
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;
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);
public insertAfter(node: LinkedListNode<T>, value: T): LinkedListNode<T> {
const newNode = createNode(value);
newNode.prev = node;
newNode.next = node.next;
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;
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);
public remove(node: LinkedListNode<T>): T {
this.detach(node);
return node.value;
}
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;
public clear(): this {
this.first = undefined;
this.last = undefined;
this.count = 0;
return this;
}
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 = Array.from<T>({ length: this.count });
let current = this.first;
let i = 0;
public toArray(): T[] {
const result = Array.from<T>({ length: this.count });
let current = this.first;
let i = 0;
while (current) {
result[i++] = current.value;
current = current.next;
}
return result;
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();
}
public toString(): string {
return this.toArray().toString();
}
/**
/**
* Iterator over list values from head to tail
*/
public *[Symbol.iterator](): Iterator<T> {
let current = this.first;
public* [Symbol.iterator](): Iterator<T> {
let current = this.first;
while (current) {
yield current.value;
current = current.next;
}
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;
}
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--;
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--;
}
}

View File

@@ -1,28 +1,28 @@
export interface LinkedListNode<T> {
value: T;
prev: LinkedListNode<T> | undefined;
next: LinkedListNode<T> | undefined;
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 length: number;
readonly isEmpty: boolean;
readonly head: LinkedListNode<T> | undefined;
readonly tail: LinkedListNode<T> | undefined;
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;
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;
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;
clear(): this;
toArray(): T[];
toString(): string;
}

View File

@@ -3,211 +3,211 @@ import { describe, expect, it } from 'vitest';
import { PriorityQueue } from '.';
describe('PriorityQueue', () => {
describe('constructor', () => {
it('should create an empty queue', () => {
const pq = new PriorityQueue<number>();
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');
});
expect(pq.length).toBe(0);
expect(pq.isEmpty).toBe(true);
expect(pq.isFull).toBe(false);
});
describe('enqueue', () => {
it('should enqueue elements by priority', () => {
const pq = new PriorityQueue<number>();
it('should create a queue from single value', () => {
const pq = new PriorityQueue(42);
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');
});
expect(pq.length).toBe(1);
expect(pq.peek()).toBe(42);
});
describe('dequeue', () => {
it('should return undefined for empty queue', () => {
const pq = new PriorityQueue<number>();
it('should create a queue from array', () => {
const pq = new PriorityQueue([5, 3, 8, 1, 4]);
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]);
});
expect(pq.length).toBe(5);
expect(pq.peek()).toBe(1);
});
describe('peek', () => {
it('should return undefined for empty queue', () => {
const pq = new PriorityQueue<number>();
it('should throw if initial values exceed maxSize', () => {
expect(() => new PriorityQueue([1, 2, 3], { maxSize: 2 }))
.toThrow('Initial values exceed maxSize');
});
});
expect(pq.peek()).toBeUndefined();
});
describe('enqueue', () => {
it('should enqueue elements by priority', () => {
const pq = new PriorityQueue<number>();
it('should return highest-priority element without removing', () => {
const pq = new PriorityQueue([5, 1, 3]);
pq.enqueue(5);
pq.enqueue(1);
pq.enqueue(3);
expect(pq.peek()).toBe(1);
expect(pq.length).toBe(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]);
it('should throw when queue is full', () => {
const pq = new PriorityQueue<number>(undefined, { maxSize: 2 });
expect(pq.isFull).toBe(false);
});
pq.enqueue(1);
pq.enqueue(2);
it('should be true when at maxSize', () => {
const pq = new PriorityQueue([1, 2], { maxSize: 2 });
expect(() => pq.enqueue(3)).toThrow('PriorityQueue is full');
});
});
expect(pq.isFull).toBe(true);
});
describe('dequeue', () => {
it('should return undefined for empty queue', () => {
const pq = new PriorityQueue<number>();
it('should become false after dequeue', () => {
const pq = new PriorityQueue([1, 2], { maxSize: 2 });
pq.dequeue();
expect(pq.isFull).toBe(false);
});
expect(pq.dequeue()).toBeUndefined();
});
describe('clear', () => {
it('should remove all elements', () => {
const pq = new PriorityQueue([1, 2, 3]);
it('should dequeue elements in priority order (min-heap)', () => {
const pq = new PriorityQueue([5, 3, 8, 1, 4]);
const result: number[] = [];
const result = pq.clear();
while (!pq.isEmpty) {
result.push(pq.dequeue()!);
}
expect(pq.length).toBe(0);
expect(pq.isEmpty).toBe(true);
expect(result).toBe(pq);
});
expect(result).toEqual([1, 3, 4, 5, 8]);
});
describe('toArray', () => {
it('should return empty array for empty queue', () => {
const pq = new PriorityQueue<number>();
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[] = [];
expect(pq.toArray()).toEqual([]);
});
while (!pq.isEmpty) {
result.push(pq.dequeue()!);
}
it('should return a shallow copy', () => {
const pq = new PriorityQueue([3, 1, 2]);
const arr = pq.toArray();
expect(result).toEqual([8, 5, 4, 3, 1]);
});
});
arr.push(99);
describe('peek', () => {
it('should return undefined for empty queue', () => {
const pq = new PriorityQueue<number>();
expect(pq.length).toBe(3);
});
expect(pq.peek()).toBeUndefined();
});
describe('toString', () => {
it('should return formatted string', () => {
const pq = new PriorityQueue([1, 2, 3]);
it('should return highest-priority element without removing', () => {
const pq = new PriorityQueue([5, 1, 3]);
expect(pq.toString()).toBe('PriorityQueue(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);
});
describe('iterator', () => {
it('should iterate over elements', () => {
const pq = new PriorityQueue([5, 3, 1]);
const elements = [...pq];
it('should be true when at maxSize', () => {
const pq = new PriorityQueue([1, 2], { maxSize: 2 });
expect(elements.length).toBe(3);
});
expect(pq.isFull).toBe(true);
});
describe('custom comparator', () => {
it('should work with object priority', () => {
interface Job {
priority: number;
name: string;
}
it('should become false after dequeue', () => {
const pq = new PriorityQueue([1, 2], { maxSize: 2 });
const pq = new PriorityQueue<Job>(
[
{ priority: 3, name: 'low' },
{ priority: 1, name: 'critical' },
{ priority: 2, name: 'normal' },
],
{ comparator: (a, b) => a.priority - b.priority },
);
pq.dequeue();
expect(pq.dequeue()?.name).toBe('critical');
expect(pq.dequeue()?.name).toBe('normal');
expect(pq.dequeue()?.name).toBe('low');
});
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([]);
});
describe('interleaved operations', () => {
it('should maintain priority with mixed enqueue and dequeue', () => {
const pq = new PriorityQueue<number>();
it('should return a shallow copy', () => {
const pq = new PriorityQueue([3, 1, 2]);
const arr = pq.toArray();
pq.enqueue(10);
pq.enqueue(5);
expect(pq.dequeue()).toBe(5);
arr.push(99);
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();
});
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();
});
});
});

View File

@@ -5,8 +5,8 @@ export type { PriorityQueueLike } from './types';
export type { Comparator } from './types';
export interface PriorityQueueOptions<T> {
comparator?: Comparator<T>;
maxSize?: number;
comparator?: Comparator<T>;
maxSize?: number;
}
/**
@@ -19,126 +19,126 @@ export interface PriorityQueueOptions<T> {
* @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;
private readonly maxSize: number;
/**
/**
* Internal binary heap backing the queue
*
* @private
* @type {BinaryHeap<T>}
*/
private readonly heap: 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 });
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');
}
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;
}
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;
}
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;
}
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');
public enqueue(element: T): void {
if (this.isFull)
throw new RangeError('PriorityQueue is full');
this.heap.push(element);
}
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();
}
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();
}
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;
}
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();
}
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})`;
}
public toString(): string {
return `PriorityQueue(${this.heap.length})`;
}
/**
/**
* Iterator over queue elements in heap order
*/
public *[Symbol.iterator](): Iterator<T> {
yield* this.heap;
}
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;
}
public async* [Symbol.asyncIterator](): AsyncIterator<T> {
for (const element of this.heap)
yield element;
}
}

View File

@@ -1,16 +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;
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;
enqueue(element: T): void;
dequeue(): T | undefined;
peek(): T | undefined;
clear(): this;
toArray(): T[];
toString(): string;
}
export type { Comparator };

View File

@@ -133,7 +133,7 @@ export class Queue<T> implements QueueLike<T> {
*
* @returns {AsyncIterableIterator<T>}
*/
async *[Symbol.asyncIterator]() {
async* [Symbol.asyncIterator]() {
for (const element of this.deque)
yield element;
}

View File

@@ -112,8 +112,8 @@ describe('stack', () => {
for await (const element of stack) {
elements.push(element);
}
expect(elements).toEqual([3, 2, 1]);
});
});
});
});

View File

@@ -5,7 +5,7 @@ import type { StackLike } from './types';
export type { StackLike } from './types';
export interface StackOptions {
maxSize?: number;
maxSize?: number;
}
/**
@@ -18,138 +18,138 @@ export interface StackOptions {
* @template T The type of elements stored in the stack
*/
export class Stack<T> implements StackLike<T> {
/**
/**
* The maximum number of elements that the stack can hold
*
*
* @private
* @type {number}
*/
private readonly maxSize: number;
private readonly maxSize: number;
/**
/**
* The stack data structure
*
*
* @private
* @type {T[]}
*/
private readonly stack: T[];
private readonly 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 = isArray(initialValues) ? initialValues : initialValues ? [initialValues] : [];
}
/**
constructor(initialValues?: T[] | T, options?: StackOptions) {
this.maxSize = options?.maxSize ?? Infinity;
this.stack = 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() {
return this.stack.length;
}
public get length() {
return this.stack.length;
}
/**
/**
* Checks if the stack is empty
* @returns {boolean} `true` if the stack is empty, `false` otherwise
*/
public get isEmpty() {
return this.stack.length === 0;
}
public get isEmpty() {
return this.stack.length === 0;
}
/**
/**
* Checks if the stack is full
* @returns {boolean} `true` if the stack is full, `false` otherwise
*/
public get isFull() {
return this.stack.length === this.maxSize;
}
public get isFull() {
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);
public push(element: T) {
if (this.isFull)
throw new RangeError('Stack is full');
return this;
}
this.stack.push(element);
/**
return this;
}
/**
* Pops an element from the stack
* @returns {T | undefined} The element popped from the stack
*/
public pop() {
return this.stack.pop();
}
public pop() {
return this.stack.pop();
}
/**
/**
* Peeks at the top element of the stack
* @returns {T | undefined} The top element of the stack
*/
public peek() {
if (this.isEmpty)
return undefined;
return last(this.stack);
}
public peek() {
if (this.isEmpty)
return undefined;
/**
return last(this.stack);
}
/**
* Clears the stack
*
*
* @returns {this}
*/
public clear() {
this.stack.length = 0;
public clear() {
this.stack.length = 0;
return this;
}
return this;
}
/**
/**
* Converts the stack to an array
*
*
* @returns {T[]}
*/
public toArray() {
return this.stack.toReversed();
}
public toArray() {
return this.stack.toReversed();
}
/**
/**
* Returns a string representation of the stack
*
*
* @returns {string}
*/
public toString() {
return this.toArray().toString();
}
public toString() {
return this.toArray().toString();
}
/**
/**
* Returns an iterator for the stack
*
*
* @returns {IterableIterator<T>}
*/
public [Symbol.iterator]() {
return this.toArray()[Symbol.iterator]();
}
public [Symbol.iterator]() {
return this.toArray()[Symbol.iterator]();
}
/**
/**
* Returns an async iterator for the stack
*
*
* @returns {AsyncIterableIterator<T>}
*/
public async *[Symbol.asyncIterator]() {
for (const element of this.toArray()) {
yield element;
}
public async* [Symbol.asyncIterator]() {
for (const element of this.toArray()) {
yield element;
}
}
}

View File

@@ -4,4 +4,4 @@ export * from './Deque';
export * from './LinkedList';
export * from './PriorityQueue';
export * from './Queue';
export * from './Stack';
export * from './Stack';

View File

@@ -14,21 +14,21 @@ describe('SyncMutex', () => {
it('lock the mutex', () => {
mutex.lock();
expect(mutex.isLocked).toBe(true);
});
it('remain locked when locked multiple times', () => {
mutex.lock();
mutex.lock();
expect(mutex.isLocked).toBe(true);
});
it('unlock a locked mutex', () => {
mutex.lock();
mutex.unlock();
expect(mutex.isLocked).toBe(false);
});
@@ -50,7 +50,7 @@ describe('SyncMutex', () => {
it('execute a callback when unlocked', async () => {
const callback = vi.fn(() => 'done');
const result = await mutex.execute(callback);
expect(result).toBe('done');
expect(callback).toHaveBeenCalled();
});
@@ -58,7 +58,7 @@ describe('SyncMutex', () => {
it('execute a promise callback when unlocked', async () => {
const callback = vi.fn(() => Promise.resolve('done'));
const result = await mutex.execute(callback);
expect(result).toBe('done');
expect(callback).toHaveBeenCalled();
});
@@ -71,7 +71,7 @@ describe('SyncMutex', () => {
mutex.execute(callback),
mutex.execute(callback),
]);
expect(result).toEqual(['done', undefined, undefined]);
expect(callback).toHaveBeenCalledTimes(1);
});
@@ -88,7 +88,7 @@ describe('SyncMutex', () => {
it('unlocks after executing a callback', async () => {
const callback = vi.fn(() => 'done');
await mutex.execute(callback);
expect(mutex.isLocked).toBe(false);
});
});

View File

@@ -2,19 +2,19 @@
* @name SyncMutex
* @category Utils
* @description A simple synchronous mutex to provide more readable locking and unlocking of code blocks
*
*
* @example
* const mutex = new SyncMutex();
*
*
* mutex.lock();
*
* mutex.unlock();
*
*
* const result = await mutex.execute(() => {
* // do something
* return Promise.resolve('done');
* return Promise.resolve('done');
* });
*
*
* @since 0.0.5
*/
export class SyncMutex {

View File

@@ -1,4 +1,4 @@
export * from './levenshtein-distance';
export * from './trigram-distance';
// TODO: Template is not implemented yet
// export * from './template';
// export * from './template';

View File

@@ -1,32 +1,32 @@
import { describe, expect, it } from 'vitest';
import {levenshteinDistance} from '.';
describe('levenshteinDistance', () => {
it('calculate edit distance between two strings', () => {
// just one substitution I at the beginning
expect(levenshteinDistance('islander', 'slander')).toBe(1);
// substitution M->K, T->M and add an A to the end
expect(levenshteinDistance('mart', 'karma')).toBe(3);
// substitution K->S, E->I and insert G at the end
expect(levenshteinDistance('kitten', 'sitting')).toBe(3);
// should add 4 letters FOOT at the beginning
expect(levenshteinDistance('ball', 'football')).toBe(4);
// should delete 4 letters FOOT at the beginning
expect(levenshteinDistance('football', 'foot')).toBe(4);
// needs to substitute the first 5 chars INTEN->EXECU
expect(levenshteinDistance('intention', 'execution')).toBe(5);
});
import { levenshteinDistance } from '.';
it('handle empty strings', () => {
expect(levenshteinDistance('', '')).toBe(0);
expect(levenshteinDistance('a', '')).toBe(1);
expect(levenshteinDistance('', 'a')).toBe(1);
expect(levenshteinDistance('abc', '')).toBe(3);
expect(levenshteinDistance('', 'abc')).toBe(3);
});
});
describe('levenshteinDistance', () => {
it('calculate edit distance between two strings', () => {
// just one substitution I at the beginning
expect(levenshteinDistance('islander', 'slander')).toBe(1);
// substitution M->K, T->M and add an A to the end
expect(levenshteinDistance('mart', 'karma')).toBe(3);
// substitution K->S, E->I and insert G at the end
expect(levenshteinDistance('kitten', 'sitting')).toBe(3);
// should add 4 letters FOOT at the beginning
expect(levenshteinDistance('ball', 'football')).toBe(4);
// should delete 4 letters FOOT at the beginning
expect(levenshteinDistance('football', 'foot')).toBe(4);
// needs to substitute the first 5 chars INTEN->EXECU
expect(levenshteinDistance('intention', 'execution')).toBe(5);
});
it('handle empty strings', () => {
expect(levenshteinDistance('', '')).toBe(0);
expect(levenshteinDistance('a', '')).toBe(1);
expect(levenshteinDistance('', 'a')).toBe(1);
expect(levenshteinDistance('abc', '')).toBe(3);
expect(levenshteinDistance('', 'abc')).toBe(3);
});
});

View File

@@ -2,7 +2,7 @@
* @name levenshteinDistance
* @category Text
* @description Calculate the Levenshtein distance between two strings
*
*
* @param {string} left First string
* @param {string} right Second string
* @returns {number} The Levenshtein distance between the two strings
@@ -10,37 +10,37 @@
* @since 0.0.1
*/
export function levenshteinDistance(left: string, right: string): number {
if (left === right) return 0;
if (left === right) return 0;
if (left.length === 0) return right.length;
if (right.length === 0) return left.length;
if (left.length === 0) return right.length;
if (right.length === 0) return left.length;
// Create empty edit distance matrix for all possible modifications of
// substrings of left to substrings of right
const distanceMatrix = Array(right.length + 1).fill(null).map(() => Array(left.length + 1).fill(null));
// Create empty edit distance matrix for all possible modifications of
// substrings of left to substrings of right
const distanceMatrix = Array(right.length + 1).fill(null).map(() => Array(left.length + 1).fill(null));
// Fill the first row of the matrix
// If this is the first row, we're transforming from an empty string to left
// In this case, the number of operations equals the length of left substring
for (let i = 0; i <= left.length; i++)
distanceMatrix[0]![i]! = i;
// Fill the first row of the matrix
// If this is the first row, we're transforming from an empty string to left
// In this case, the number of operations equals the length of left substring
for (let i = 0; i <= left.length; i++)
distanceMatrix[0]![i]! = i;
// Fill the first column of the matrix
// If this is the first column, we're transforming empty string to right
// In this case, the number of operations equals the length of right substring
for (let j = 0; j <= right.length; j++)
distanceMatrix[j]![0]! = j;
// Fill the first column of the matrix
// If this is the first column, we're transforming empty string to right
// In this case, the number of operations equals the length of right substring
for (let j = 0; j <= right.length; j++)
distanceMatrix[j]![0]! = j;
for (let j = 1; j <= right.length; j++) {
for (let i = 1; i <= left.length; i++) {
const indicator = left[i - 1] === right[j - 1] ? 0 : 1;
distanceMatrix[j]![i]! = Math.min(
distanceMatrix[j]![i - 1]! + 1, // deletion
distanceMatrix[j - 1]![i]! + 1, // insertion
distanceMatrix[j - 1]![i - 1]! + indicator // substitution
);
}
for (let j = 1; j <= right.length; j++) {
for (let i = 1; i <= left.length; i++) {
const indicator = left[i - 1] === right[j - 1] ? 0 : 1;
distanceMatrix[j]![i]! = Math.min(
distanceMatrix[j]![i - 1]! + 1, // deletion
distanceMatrix[j - 1]![i]! + 1, // insertion
distanceMatrix[j - 1]![i - 1]! + indicator, // substitution
);
}
}
return distanceMatrix[right.length]![left.length]!;
return distanceMatrix[right.length]![left.length]!;
}

View File

@@ -102,4 +102,4 @@ describe.skip('template', () => {
expectTypeOf<actual>().toEqualTypeOf<expected>();
});
});
});
});

Some files were not shown because too many files have changed in this diff Show More