diff --git a/docs/index.d.ts b/docs/index.d.ts index eb369b6..839a3e8 100644 --- a/docs/index.d.ts +++ b/docs/index.d.ts @@ -101,6 +101,8 @@ export const examples: { readonly BloomFilter: 'examples/bloomFilter.ts'; readonly SegmentTree: 'examples/segmentTree.ts'; readonly SkipList: 'examples/skipList.ts'; + readonly runLengthEncode: 'examples/rle.ts'; + readonly runLengthDecode: 'examples/rle.ts'; }; readonly performance: { readonly debounce: 'examples/requestDedup.ts'; @@ -2998,6 +3000,15 @@ export class SkipList { values(): IterableIterator; } +/** + * Run-length encoding for strings. + * Use for: simple compression of repetitive text. + * Import: data/rle.ts + */ +export interface RlePair { char: string; count: number } +export function runLengthEncode(input: string): RlePair[]; +export function runLengthDecode(pairs: ReadonlyArray): string; + /** * Disjoint Set Union (Union-Find) with path compression and union by size. * Use for: connectivity queries, Kruskal MST, clustering. diff --git a/examples/rle.ts b/examples/rle.ts new file mode 100644 index 0000000..bfc0d7c --- /dev/null +++ b/examples/rle.ts @@ -0,0 +1,7 @@ +import { runLengthEncode, runLengthDecode } from '../src/index.js'; + +const s = 'AAAABBBCCDAA'; +const pairs = runLengthEncode(s); +console.log('pairs', pairs); +console.log('decoded', runLengthDecode(pairs)); + diff --git a/src/data/rle.ts b/src/data/rle.ts new file mode 100644 index 0000000..5e7ce4f --- /dev/null +++ b/src/data/rle.ts @@ -0,0 +1,42 @@ +/** + * Run-length encoding for strings. + * Useful for: simple compression on repetitive text. + */ +export interface RlePair { + char: string; + count: number; +} + +/** + * Encodes a string into RLE pairs. + */ +export function runLengthEncode(input: string): RlePair[] { + if (input.length === 0) return []; + const out: RlePair[] = []; + let last = input[0]; + let cnt = 1; + for (let i = 1; i < input.length; i += 1) { + const c = input[i]; + if (c === last) cnt += 1; + else { + out.push({ char: last, count: cnt }); + last = c; + cnt = 1; + } + } + out.push({ char: last, count: cnt }); + return out; +} + +/** + * Decodes RLE pairs into a string. + */ +export function runLengthDecode(pairs: ReadonlyArray): string { + let out = ''; + for (const p of pairs) { + if (!p || typeof p.count !== 'number' || typeof p.char !== 'string') continue; + if (p.count <= 0 || p.char.length === 0) continue; + out += p.char.repeat(p.count); + } + return out; +} diff --git a/src/index.ts b/src/index.ts index 15f1618..4ca3fd9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -99,6 +99,8 @@ export const examples = { BloomFilter: 'examples/bloomFilter.ts', SegmentTree: 'examples/segmentTree.ts', SkipList: 'examples/skipList.ts', + runLengthEncode: 'examples/rle.ts', + runLengthDecode: 'examples/rle.ts', }, performance: { debounce: 'examples/requestDedup.ts', @@ -1044,6 +1046,12 @@ export { BinaryHeap } from './data/binaryHeap.js'; * Example file: examples/skipList.ts */ export { SkipList } from './data/skipList.js'; +/** + * Run-length encoding helpers for strings. + * + * Example file: examples/rle.ts + */ +export { runLengthEncode, runLengthDecode } from './data/rle.js'; export type { TreeNode, diff --git a/tests/index.test.ts b/tests/index.test.ts index 05e198e..6a6ebb5 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -128,6 +128,8 @@ describe('package entry point', () => { | 'BloomFilter' | 'SegmentTree' | 'SkipList' + | 'runLengthEncode' + | 'runLengthDecode' >(); expectTypeOf>().toEqualTypeOf< diff --git a/tests/rle.test.ts b/tests/rle.test.ts new file mode 100644 index 0000000..b007a27 --- /dev/null +++ b/tests/rle.test.ts @@ -0,0 +1,24 @@ +import { describe, it, expect } from 'vitest'; +import { runLengthEncode, runLengthDecode } from '../src/index.js'; + +describe('RLE', () => { + it('encodes and decodes repetitive strings', () => { + const s = 'AAAABBBCCDAA'; + const pairs = runLengthEncode(s); + expect(pairs).toEqual([ + { char: 'A', count: 4 }, + { char: 'B', count: 3 }, + { char: 'C', count: 2 }, + { char: 'D', count: 1 }, + { char: 'A', count: 2 }, + ]); + expect(runLengthDecode(pairs)).toBe(s); + }); + + it('handles empty and mixed content', () => { + expect(runLengthEncode('')).toEqual([]); + const s = 'ABAB'; + expect(runLengthDecode(runLengthEncode(s))).toBe(s); + }); +}); +