From cff1f71be0239b0cb9da58f46ab4a9c8339e501d Mon Sep 17 00:00:00 2001 From: Francois Date: Thu, 9 Oct 2025 18:30:34 +0900 Subject: [PATCH 1/2] feat(data): add run-length encode/decode utilities; docs + tests + example --- docs/index.d.ts | 11 +++++++++++ examples/rle.ts | 7 +++++++ src/data/rle.ts | 42 ++++++++++++++++++++++++++++++++++++++++++ src/index.ts | 8 ++++++++ tests/index.test.ts | 2 ++ tests/rle.test.ts | 24 ++++++++++++++++++++++++ 6 files changed, 94 insertions(+) create mode 100644 examples/rle.ts create mode 100644 src/data/rle.ts create mode 100644 tests/rle.test.ts 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); + }); +}); + From 3c14c672a9519def454ddead5008384ba325cd55 Mon Sep 17 00:00:00 2001 From: Francois Date: Thu, 9 Oct 2025 18:33:04 +0900 Subject: [PATCH 2/2] feat(data): add Base64 encode/decode utilities; docs + tests + example --- docs/index.d.ts | 10 ++++++++++ examples/base64.ts | 7 +++++++ src/data/base64.ts | 41 +++++++++++++++++++++++++++++++++++++++++ src/index.ts | 8 ++++++++ tests/base64.test.ts | 13 +++++++++++++ tests/index.test.ts | 2 ++ 6 files changed, 81 insertions(+) create mode 100644 examples/base64.ts create mode 100644 src/data/base64.ts create mode 100644 tests/base64.test.ts diff --git a/docs/index.d.ts b/docs/index.d.ts index 839a3e8..a86721a 100644 --- a/docs/index.d.ts +++ b/docs/index.d.ts @@ -103,6 +103,8 @@ export const examples: { readonly SkipList: 'examples/skipList.ts'; readonly runLengthEncode: 'examples/rle.ts'; readonly runLengthDecode: 'examples/rle.ts'; + readonly base64Encode: 'examples/base64.ts'; + readonly base64Decode: 'examples/base64.ts'; }; readonly performance: { readonly debounce: 'examples/requestDedup.ts'; @@ -3009,6 +3011,14 @@ export interface RlePair { char: string; count: number } export function runLengthEncode(input: string): RlePair[]; export function runLengthDecode(pairs: ReadonlyArray): string; +/** + * Base64 encode/decode utilities (UTF-8 strings and bytes). + * Use for: compact textual transport, data URIs, wire formats. + * Import: data/base64.ts + */ +export function base64Encode(input: string | Uint8Array): string; +export function base64Decode(b64: string): Uint8Array; + /** * Disjoint Set Union (Union-Find) with path compression and union by size. * Use for: connectivity queries, Kruskal MST, clustering. diff --git a/examples/base64.ts b/examples/base64.ts new file mode 100644 index 0000000..7f0a0f0 --- /dev/null +++ b/examples/base64.ts @@ -0,0 +1,7 @@ +import { base64Encode, base64Decode } from '../src/index.js'; + +const s = 'Hello, δΈ–η•Œ! πŸŽ‰'; +const b64 = base64Encode(s); +console.log('b64:', b64); +console.log('utf8:', new TextDecoder().decode(base64Decode(b64))); + diff --git a/src/data/base64.ts b/src/data/base64.ts new file mode 100644 index 0000000..49f9e44 --- /dev/null +++ b/src/data/base64.ts @@ -0,0 +1,41 @@ +/** + * Base64 encode/decode utilities (UTF-8 strings and bytes). + */ +export function base64Encode(input: string | Uint8Array): string { + if (typeof input === 'string') { + const bytes = new TextEncoder().encode(input); + return encodeBytes(bytes); + } + return encodeBytes(input); +} + +export function base64Decode(b64: string): Uint8Array { + return decodeToBytes(b64); +} + +function encodeBytes(bytes: Uint8Array): string { + if (typeof Buffer !== 'undefined') { + // Node + // eslint-disable-next-line no-undef + return Buffer.from(bytes).toString('base64'); + } + // Browser + let binary = ''; + for (let i = 0; i < bytes.length; i += 1) binary += String.fromCharCode(bytes[i]); + const g = globalThis as unknown as { btoa?: (s: string) => string }; + if (!g.btoa) throw new Error('Base64 encode not available in this environment'); + return g.btoa(binary); +} + +function decodeToBytes(b64: string): Uint8Array { + if (typeof Buffer !== 'undefined') { + // eslint-disable-next-line no-undef + return new Uint8Array(Buffer.from(b64, 'base64')); + } + const g = globalThis as unknown as { atob?: (s: string) => string }; + if (!g.atob) throw new Error('Base64 decode not available in this environment'); + const binary: string = g.atob(b64); + const out = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i += 1) out[i] = binary.charCodeAt(i); + return out; +} diff --git a/src/index.ts b/src/index.ts index 4ca3fd9..592918b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -101,6 +101,8 @@ export const examples = { SkipList: 'examples/skipList.ts', runLengthEncode: 'examples/rle.ts', runLengthDecode: 'examples/rle.ts', + base64Encode: 'examples/base64.ts', + base64Decode: 'examples/base64.ts', }, performance: { debounce: 'examples/requestDedup.ts', @@ -1052,6 +1054,12 @@ export { SkipList } from './data/skipList.js'; * Example file: examples/rle.ts */ export { runLengthEncode, runLengthDecode } from './data/rle.js'; +/** + * Base64 encode/decode helpers for strings and bytes. + * + * Example file: examples/base64.ts + */ +export { base64Encode, base64Decode } from './data/base64.js'; export type { TreeNode, diff --git a/tests/base64.test.ts b/tests/base64.test.ts new file mode 100644 index 0000000..7e40ebf --- /dev/null +++ b/tests/base64.test.ts @@ -0,0 +1,13 @@ +import { describe, it, expect } from 'vitest'; +import { base64Encode, base64Decode } from '../src/index.js'; + +describe('Base64', () => { + it('encodes and decodes UTF-8 strings', () => { + const s = 'Hello, δΈ–η•Œ! πŸŽ‰'; + const b64 = base64Encode(s); + const bytes = base64Decode(b64); + const back = new TextDecoder().decode(bytes); + expect(back).toBe(s); + }); +}); + diff --git a/tests/index.test.ts b/tests/index.test.ts index 6a6ebb5..7513636 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -130,6 +130,8 @@ describe('package entry point', () => { | 'SkipList' | 'runLengthEncode' | 'runLengthDecode' + | 'base64Encode' + | 'base64Decode' >(); expectTypeOf>().toEqualTypeOf<