Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
- [x] Boyer–Moore fast substring search
- [x] Suffix array construction utilities
- [x] Longest common subsequence (LCS) enhancements and diff helpers
- [x] Aho–Corasick multi-pattern automaton
**Data pipelines & utilities**
- [x] Flatten/unflatten helpers for nested structures
- [x] Pagination utilities for client-side paging
Expand Down
47 changes: 47 additions & 0 deletions docs/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ export const examples: {
readonly aabbIntersection: 'examples/sat.ts';
readonly satCollision: 'examples/sat.ts';
readonly circleRayIntersection: 'examples/sat.ts';
readonly circleCollision: 'examples/circle.ts';
readonly circleAabbCollision: 'examples/circle.ts';
readonly circleSegmentIntersection: 'examples/circle.ts';
readonly sweptAABB: 'examples/sweptAabb.ts';
};
readonly search: {
Expand All @@ -79,6 +82,7 @@ export const examples: {
readonly buildSuffixArray: 'examples/search.ts';
readonly longestCommonSubsequence: 'examples/search.ts';
readonly diffStrings: 'examples/search.ts';
readonly createAhoCorasick: 'examples/search.ts';
};
readonly data: {
readonly diff: 'examples/jsonDiff.ts';
Expand Down Expand Up @@ -188,6 +192,34 @@ export type ExampleName<C extends ExampleCategory = ExampleCategory> = keyof Exa
// 🎮 PATHFINDING & NAVIGATION
// ============================================================================

// ============================================================================
// 🎯 SPATIAL & COLLISION – Circle Helpers
// ============================================================================

/**
* Circle vs circle overlap test.
* Use for: simple collision checks, proximity triggers.
* Performance: O(1).
* Import: spatial/circleCollision.ts
*/
export function circleCollision(a: Circle, b: Circle): boolean;

/**
* Circle vs axis-aligned rectangle (AABB) intersection.
* Use for: tile collisions, UI hit-tests, broad-phase pruning.
* Performance: O(1).
* Import: spatial/circleCollision.ts
*/
export function circleAabbCollision(circle: Circle, rect: Rect): boolean;

/**
* Circle vs line segment intersection test.
* Use for: ray/segment hits, visibility checks, bullet tests.
* Performance: O(1).
* Import: spatial/circleCollision.ts
*/
export function circleSegmentIntersection(circle: Circle, a: Point, b: Point): boolean;

/**
* A* pathfinding algorithm for grid-based navigation.
* Use for: game movement, robotics, maze solving.
Expand Down Expand Up @@ -2664,6 +2696,21 @@ export interface DiffOp {
export function longestCommonSubsequence(options: LCSOptions): LCSResult;
export function diffStrings(options: LCSOptions): DiffOp[];

/**
* Aho–Corasick multi-pattern automaton.
* Use for: scanning texts for many patterns efficiently with overlaps.
* Performance: O(n + m + z) where n=text length, m=total pattern length, z=matches.
* Import: search/ahoCorasick.ts
*/
export interface AhoBuildOptions {
patterns: ReadonlyArray<string>;
caseSensitive?: boolean;
}
export interface AhoAutomaton {
search(text: string): Record<string, number[]>;
}
export function createAhoCorasick(options: AhoBuildOptions): AhoAutomaton;

// ============================================================================
// 📊 DATA TOOLS
// ============================================================================
Expand Down
1 change: 1 addition & 0 deletions docs/list.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ Maximum Flow (Ford-Fulkerson) - Network flow

Rabin-Karp - Multiple pattern matching
Boyer-Moore - Fast single pattern search
Aho–Corasick - Multi-pattern automaton
Longest Common Subsequence - Diff algorithms
Suffix Array - Advanced pattern matching

Expand Down
24 changes: 24 additions & 0 deletions examples/circle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {
circleCollision,
circleAabbCollision,
circleSegmentIntersection,
} from '../src/index.js';

// Circle vs circle
console.log(
'circleCollision',
circleCollision({ x: 0, y: 0, radius: 2 }, { x: 3, y: 0, radius: 2 })
);

// Circle vs AABB
console.log(
'circleAabbCollision',
circleAabbCollision({ x: 5, y: 5, radius: 2 }, { x: 6, y: 6, width: 4, height: 4 })
);

// Circle vs segment
console.log(
'circleSegmentIntersection',
circleSegmentIntersection({ x: 0, y: 0, radius: 1 }, { x: -2, y: 0 }, { x: 2, y: 0 })
);

4 changes: 4 additions & 0 deletions examples/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
buildSuffixArray,
longestCommonSubsequence,
diffStrings,
createAhoCorasick,
} from '../src/index.js';

const items = ['alpha', 'beta', 'delta', 'epsilon', 'gamma'];
Expand Down Expand Up @@ -43,3 +44,6 @@ console.log('LCS of dynamic/programming:', lcs);

const diff = diffStrings({ a: 'kitten', b: 'sitting' });
console.log('Diff between kitten and sitting:', diff);

const automaton = createAhoCorasick({ patterns: ['abra', 'cad'] });
console.log('Aho–Corasick matches in abracadabra:', automaton.search('abracadabra'));
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
{
"name": "bundle",
"path": "dist/index.js",
"limit": "40 KB"
"limit": "41 KB"
}
]
}
27 changes: 27 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ export const examples = {
aabbIntersection: 'examples/sat.ts',
satCollision: 'examples/sat.ts',
circleRayIntersection: 'examples/sat.ts',
circleCollision: 'examples/circle.ts',
circleAabbCollision: 'examples/circle.ts',
circleSegmentIntersection: 'examples/circle.ts',
sweptAABB: 'examples/sweptAabb.ts',
},
search: {
Expand All @@ -75,6 +78,7 @@ export const examples = {
buildSuffixArray: 'examples/search.ts',
longestCommonSubsequence: 'examples/search.ts',
diffStrings: 'examples/search.ts',
createAhoCorasick: 'examples/search.ts',
},
data: {
diff: 'examples/jsonDiff.ts',
Expand Down Expand Up @@ -405,6 +409,24 @@ export { satCollision } from './spatial/sat.js';
* Example file: examples/sat.ts
*/
export { circleRayIntersection } from './spatial/circleRay.js';
/**
* Fast circle-circle overlap test.
*
* Example file: examples/circle.ts
*/
export { circleCollision } from './spatial/circleCollision.js';
/**
* Circle vs. AABB intersection test.
*
* Example file: examples/circle.ts
*/
export { circleAabbCollision } from './spatial/circleCollision.js';
/**
* Circle vs. line segment intersection test.
*
* Example file: examples/circle.ts
*/
export { circleSegmentIntersection } from './spatial/circleCollision.js';

/**
* Continuous swept AABB collision detection for moving boxes.
Expand Down Expand Up @@ -924,6 +946,11 @@ export type { LCSOptions, LCSResult, DiffOp } from './search/lcs.js';
*/
export { levenshteinDistance } from './search/levenshtein.js';

/**
* Aho–Corasick multi-pattern automaton.
*/
export { createAhoCorasick } from './search/ahoCorasick.js';

// ============================================================================
// 📊 DATA PROCESSING
// ============================================================================
Expand Down
106 changes: 106 additions & 0 deletions src/search/ahoCorasick.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
export interface AhoBuildOptions {
patterns: ReadonlyArray<string>;
caseSensitive?: boolean;
}

export interface AhoAutomaton {
search(text: string): Record<string, number[]>;
}

interface Node {
next: Map<string, number>;
fail: number;
out: number[]; // indices into originalPatterns
}

export function createAhoCorasick(options: AhoBuildOptions): AhoAutomaton {
validateOptions(options);
const caseSensitive = options.caseSensitive ?? true;
const originalPatterns = options.patterns.slice();
const normalizedPatterns = caseSensitive
? originalPatterns
: originalPatterns.map((p) => p.toLowerCase());

const nodes: Node[] = [{ next: new Map(), fail: 0, out: [] }];

// Build trie
normalizedPatterns.forEach((pattern, idx) => {
if (pattern.length === 0) return;
let state = 0;
for (const ch of pattern) {
let to = nodes[state].next.get(ch);
if (to === undefined) {
to = nodes.length;
nodes[state].next.set(ch, to);
nodes.push({ next: new Map(), fail: 0, out: [] });
}
state = to;
}
nodes[state].out.push(idx);
});

// Build fail links via BFS
const queue: number[] = [];
for (const [, to] of nodes[0].next.entries()) {
nodes[to].fail = 0;
queue.push(to);
}
while (queue.length > 0) {
const v = queue.shift()!;
for (const [ch, to] of nodes[v].next.entries()) {
queue.push(to);
let f = nodes[v].fail;
while (f !== 0 && !nodes[f].next.has(ch)) {
f = nodes[f].fail;
}
if (nodes[f].next.has(ch)) {
f = nodes[f].next.get(ch)!;
}
nodes[to].fail = f;
nodes[to].out.push(...nodes[f].out);
}
}

function search(text: string): Record<string, number[]> {
const t = caseSensitive ? text : text.toLowerCase();
const results: Record<string, number[]> = {};
// Handle empty patterns returning all positions
for (let i = 0; i < originalPatterns.length; i += 1) {
if (normalizedPatterns[i].length === 0) {
results[originalPatterns[i]] = Array.from({ length: text.length + 1 }, (_, p) => p);
}
}

let state = 0;
for (let i = 0; i < t.length; i += 1) {
const ch = t[i];
while (state !== 0 && !nodes[state].next.has(ch)) {
state = nodes[state].fail;
}
if (nodes[state].next.has(ch)) {
state = nodes[state].next.get(ch)!;
}
if (nodes[state].out.length > 0) {
for (const patIdx of nodes[state].out) {
const pat = originalPatterns[patIdx];
const len = normalizedPatterns[patIdx].length;
const pos = i - len + 1;
if (!results[pat]) results[pat] = [];
results[pat].push(pos);
}
}
}
for (const pat of originalPatterns) {
if (!results[pat]) results[pat] = [];
}
return results;
}

return { search };
}

function validateOptions(options: AhoBuildOptions): void {
if (!Array.isArray(options.patterns) || options.patterns.length === 0) {
throw new Error('patterns must contain at least one pattern.');
}
}
55 changes: 55 additions & 0 deletions src/spatial/circleCollision.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { Circle, Point, Rect, Vector2D } from '../types.js';

/**
* Tests whether two circles overlap or touch.
* Useful for: simple collision tests, triggers, proximity checks.
*/
export function circleCollision(a: Circle, b: Circle): boolean {
const dx = a.x + 0 - b.x;
const dy = a.y + 0 - b.y;
const r = a.radius + b.radius;
return dx * dx + dy * dy <= r * r;
}

/**
* Tests whether a circle intersects an axis-aligned rectangle (AABB).
* Useful for: broad-phase checks against tiles, UI hit areas.
*/
export function circleAabbCollision(circle: Circle, rect: Rect): boolean {
const closestX = clamp(circle.x, rect.x, rect.x + rect.width);
const closestY = clamp(circle.y, rect.y, rect.y + rect.height);
const dx = circle.x - closestX;
const dy = circle.y - closestY;
return dx * dx + dy * dy <= circle.radius * circle.radius;
}

/**
* Tests whether a line segment intersects a circle.
* Useful for: hit scans, visibility checks, bullet vs. circle tests.
*/
export function circleSegmentIntersection(circle: Circle, a: Point, b: Point): boolean {
const ab: Vector2D = { x: b.x - a.x, y: b.y - a.y };
const ac: Vector2D = { x: circle.x - a.x, y: circle.y - a.y };

const abLenSq = ab.x * ab.x + ab.y * ab.y;
if (abLenSq === 0) {
// Segment degenerates to point
const dx = a.x - circle.x;
const dy = a.y - circle.y;
return dx * dx + dy * dy <= circle.radius * circle.radius;
}

// Project AC onto AB to find closest point on segment to circle center
const t = clamp((ac.x * ab.x + ac.y * ab.y) / abLenSq, 0, 1);
const closest: Point = { x: a.x + ab.x * t, y: a.y + ab.y * t };
const dx = closest.x - circle.x;
const dy = closest.y - circle.y;
return dx * dx + dy * dy <= circle.radius * circle.radius;
}

function clamp(value: number, min: number, max: number): number {
return Math.max(min, Math.min(max, value));
}

export const __internals = { clamp };

22 changes: 22 additions & 0 deletions tests/ahoCorasick.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { describe, expect, it } from 'vitest';

import { createAhoCorasick } from '../src/index.js';

describe('createAhoCorasick', () => {
it('finds overlapping multi-pattern matches', () => {
const ac = createAhoCorasick({ patterns: ['ab', 'bc', 'abc'] });
const res = ac.search('ababc');
expect(res['ab']).toEqual([0, 2]);
expect(res['bc']).toEqual([3]);
expect(res['abc']).toEqual([2]);
});

it('supports case-insensitive matching and empty patterns', () => {
const ac = createAhoCorasick({ patterns: ['He', 'eL', ''], caseSensitive: false });
const res = ac.search('HeLlo');
expect(res['He']).toEqual([0]);
expect(res['eL']).toEqual([1]);
expect(res['']).toEqual([0, 1, 2, 3, 4, 5]);
});
});

Loading