diff --git a/packages/core/tests/index.test.ts b/packages/core/tests/index.test.ts index 5a54f08..663db79 100644 --- a/packages/core/tests/index.test.ts +++ b/packages/core/tests/index.test.ts @@ -1,4 +1,27 @@ -import {encode, decode, getCharCodesFromSource} from '../src'; +import { + decode, + encode, + getCharCodesFromSource, + getCharsFrequency, + getEntropyOfText, + getRelativeFrequency, + getTree, +} from '../src'; + +const roundTrip = (text: string): string => { + const codes = getCharCodesFromSource(text); + const encoded = encode(text, codes); + return decode(encoded, codes); +}; + +const isPrefixFree = (codes: Map): boolean => { + const values = Array.from(codes.values()); + return values.every( + (code, index) => + code === '' || + values.every((other, otherIndex) => index === otherIndex || !other.startsWith(code)), + ); +}; describe('Check Encode/decode', () => { it('abra', () => { @@ -9,13 +32,9 @@ describe('Check Encode/decode', () => { expect(decoded).toEqual(text); }); - // it('aaaaaaaaaa', () => { - // const text = 'aaaaaaaaaa'; - // const codes = getCodesFromText(text); - // const encoded = encode(text, codes); - // const decoded = decode(encoded, codes); - // expect(decoded).toEqual(text); - // }); + it('round-trips a single repeated symbol', () => { + expect(roundTrip('aaaaaaaaaa')).toEqual('aaaaaaaaaa'); + }); it('1234', () => { const text = '1234123121'; @@ -25,11 +44,61 @@ describe('Check Encode/decode', () => { expect(decoded).toEqual(text); }); - // it('', () => { - // const text = ''; - // const codes = getCodesFromText(text); - // const encoded = encode(text, codes); - // const decoded = decode(encoded, codes); - // expect(decoded).toEqual(text); - // }); + it('round-trips empty text', () => { + const text = ''; + const codes = getCharCodesFromSource(text); + const encoded = encode(text, codes); + const decoded = decode(encoded, codes); + expect(encoded).toEqual([]); + expect(decoded).toEqual(text); + }); + + it('round-trips punctuation, whitespace, and unicode text', () => { + expect(roundTrip('hello, huffman!\nhello, compression!')).toEqual( + 'hello, huffman!\nhello, compression!', + ); + expect(roundTrip('banana bandana 123')).toEqual('banana bandana 123'); + }); +}); + +describe('Frequency helpers', () => { + it('counts character frequency in descending order', () => { + expect(getCharsFrequency('banana')).toEqual([ + ['a', 3], + ['n', 2], + ['b', 1], + ]); + }); + + it('calculates relative frequencies from absolute counts', () => { + expect(getRelativeFrequency([ + ['a', 3], + ['b', 1], + ])).toEqual([ + ['a', 0.75], + ['b', 0.25], + ]); + }); + + it('calculates entropy for common simple distributions', () => { + expect(getEntropyOfText('aaaa')).toBe(-0); + expect(getEntropyOfText('abab')).toBe(1); + }); +}); + +describe('Huffman tree and code generation', () => { + it('creates a root node weighted by the source length', () => { + const tree = getTree(getCharsFrequency('mississippi')); + expect(tree.weight).toBe('mississippi'.length); + }); + + it('creates one code per unique character', () => { + const codes = getCharCodesFromSource('mississippi'); + expect(Array.from(codes.keys()).sort()).toEqual(['i', 'm', 'p', 's']); + }); + + it('generates prefix-free codes for multi-character input', () => { + const codes = getCharCodesFromSource('mississippi river'); + expect(isPrefixFree(codes)).toBe(true); + }); });