import { describe, it, expect } from 'bun:test'; import { randInt, randBool, choice, weightedChoice, shuffle, mapNumber, zip, enumerate, range, clamp, lerp, prevent, intHash, sinHash, throttle, callUpdater, formatTime, formatNumber, } from '@common/utils'; describe('utils', () => { describe('randInt', () => { it('should return a number within the specified range', () => { for (let i = 0; i < 100; i++) { const result = randInt(1, 10); expect(result).toBeGreaterThanOrEqual(1); expect(result).toBeLessThanOrEqual(10); } }); it('should return integers only (no floats)', () => { for (let i = 0; i < 100; i++) { const result = randInt(0, 100); expect(Number.isInteger(result)).toBe(true); } }); it('should handle single value range', () => { expect(randInt(5, 6)).toBe(5); }); }); describe('randBool', () => { it('should return boolean values', () => { for (let i = 0; i < 100; i++) { const result = randBool(); expect(typeof result).toBe('boolean'); } }); it('should return both true and false over multiple calls', () => { const results = new Set(); for (let i = 0; i < 100; i++) { results.add(randBool()); } expect(results.has(true)).toBe(true); expect(results.has(false)).toBe(true); }); }); describe('choice', () => { it('should return an element from the array', () => { const array = [1, 2, 3, 4, 5]; const result = choice(array); expect(array).toContain(result); }); it('should work with string arrays', () => { const array = ['a', 'b', 'c']; const result = choice(array); expect(array).toContain(result); }); it('should work with object arrays', () => { const array = [{ id: 1 }, { id: 2 }, { id: 3 }]; const result = choice(array); expect(array).toContain(result); }); }); describe('weightedChoice', () => { it('should return an item based on weights (array input)', () => { const options: [number, number][] = [[1, 10], [2, 1], [3, 1]]; const results = new Set(); for (let i = 0; i < 100; i++) { const result = weightedChoice(options); if (result !== null) results.add(result); } // Item 1 has much higher weight, should appear most often expect(results.has(1)).toBe(true); }); it('should return an item based on weights (object input)', () => { const options = { a: 10, b: 1, c: 1 }; const results = new Set(); for (let i = 0; i < 100; i++) { const result = weightedChoice(options); if (result !== null) results.add(result); } expect(results.has('a')).toBe(true); }); it('should return null for empty options', () => { expect(weightedChoice([])).toBe(null); expect(weightedChoice({})).toBe(null); }); }); describe('shuffle', () => { it('should return an array of the same length', () => { const array = [1, 2, 3, 4, 5]; const shuffled = shuffle(array); expect(shuffled.length).toBe(array.length); }); it('should contain the same elements', () => { const array = [1, 2, 3, 4, 5]; const shuffled = shuffle(array); expect(shuffled.sort((a, b) => a - b)).toEqual(array); }); it('should not modify the original array', () => { const array = [1, 2, 3, 4, 5]; const original = [...array]; shuffle(array); expect(array).toEqual(original); }); it('should handle empty arrays', () => { expect(shuffle([])).toEqual([]); }); it('should handle single element arrays', () => { expect(shuffle([1])).toEqual([1]); }); }); describe('mapNumber', () => { it('should map a number from one range to another', () => { expect(mapNumber(5, 0, 10, 0, 100)).toBe(50); }); it('should handle reverse ranges', () => { expect(mapNumber(5, 0, 10, 100, 0)).toBe(50); }); it('should handle negative ranges', () => { expect(mapNumber(0, -10, 10, 0, 100)).toBe(50); }); }); describe('zip', () => { it('should zip two arrays together', () => { const result = [...zip([1, 2, 3], ['a', 'b', 'c'])]; expect(result).toEqual([[1, 'a'], [2, 'b'], [3, 'c']]); }); it('should stop at the shortest array', () => { const result = [...zip([1, 2], ['a', 'b', 'c', 'd'])]; expect(result).toEqual([[1, 'a'], [2, 'b']]); }); it('should zip three arrays', () => { const result = [...zip([1, 2], ['a', 'b'], [true, false])]; expect(result).toEqual([[1, 'a', true], [2, 'b', false]]); }); it('should handle empty arrays', () => { expect([...zip([], [])]).toEqual([]); }); }); describe('enumerate', () => { it('should enumerate an array with indices', () => { const result = [...enumerate(['a', 'b', 'c'])]; expect(result).toEqual([[0, 'a'], [1, 'b'], [2, 'c']]); }); it('should handle empty arrays', () => { expect([...enumerate([])]).toEqual([]); }); }); describe('range', () => { it('should create a range from a number', () => { expect(range(5)).toEqual([0, 1, 2, 3, 4]); }); it('should create a range from a string number', () => { expect(range('5')).toEqual([0, 1, 2, 3, 4]); }); it('should return empty array for 0', () => { expect(range(0)).toEqual([]); }); }); describe('clamp', () => { it('should clamp a value within range', () => { expect(clamp(5, 0, 10)).toBe(5); }); it('should clamp values below minimum', () => { expect(clamp(-5, 0, 10)).toBe(0); }); it('should clamp values above maximum', () => { expect(clamp(15, 0, 10)).toBe(10); }); }); describe('lerp', () => { it('should linearly interpolate between two values', () => { expect(lerp(0, 100, 0.5)).toBe(50); }); it('should return start value at t=0', () => { expect(lerp(0, 100, 0)).toBe(0); }); it('should return end value at t=1', () => { expect(lerp(0, 100, 1)).toBe(100); }); it('should handle values outside 0-1 range', () => { expect(lerp(0, 100, -0.5)).toBe(-50); expect(lerp(0, 100, 1.5)).toBe(150); }); }); describe('prevent', () => { it('should call preventDefault and return false', () => { let preventDefaultCalled = false; const mockEvent = { preventDefault: () => { preventDefaultCalled = true; } } as Event; const result = prevent(mockEvent); expect(preventDefaultCalled).toBe(true); expect(result).toBe(false); }); }); describe('intHash', () => { it('should return a number', () => { expect(typeof intHash(1, 2, 3)).toBe('number'); }); it('should be deterministic', () => { const hash1 = intHash(1, 2, 3); const hash2 = intHash(1, 2, 3); expect(hash1).toBe(hash2); }); it('should produce different hashes for different inputs', () => { const hash1 = intHash(1); const hash2 = intHash(2); expect(hash1).not.toBe(hash2); }); }); describe('sinHash', () => { it('should return a number between 0 and 1', () => { const result = sinHash(1, 2, 3); expect(result).toBeGreaterThanOrEqual(0); expect(result).toBeLessThanOrEqual(1); }); it('should be deterministic', () => { const hash1 = sinHash(1, 2, 3); const hash2 = sinHash(1, 2, 3); expect(hash1).toBeCloseTo(hash2); }); it('should produce different hashes for different inputs', () => { const hash1 = sinHash(1, 2, 3); const hash2 = sinHash(4, 5, 6); expect(hash1).not.toBeCloseTo(hash2); }); }); describe('throttle', () => { it('should call the function immediately', () => { let callCount = 0; const fn = throttle(() => { callCount++; }, 1000); fn(); expect(callCount).toBe(1); }); it('should throttle subsequent calls', () => { let callCount = 0; const fn = throttle(() => { callCount++; }, 1000); fn(); fn(); fn(); expect(callCount).toBe(1); }); }); describe('callUpdater', () => { it('should call a function updater', () => { const updater = (prev: number) => prev + 1; expect(callUpdater(updater, 5)).toBe(6); }); it('should return a direct value updater', () => { expect(callUpdater(10, 5)).toBe(10); }); }); describe('formatNumber', () => { it('should return small numbers as-is', () => { expect(formatNumber(0)).toBe('0'); expect(formatNumber(42)).toBe('42'); expect(formatNumber(999)).toBe('999'); }); it('should format thousands with k suffix', () => { expect(formatNumber(1_000)).toBe('1.00k'); expect(formatNumber(5_500)).toBe('5.50k'); expect(formatNumber(999_999)).toBe('1000.00k'); }); it('should format millions with M suffix', () => { expect(formatNumber(1_000_000)).toBe('1.00M'); expect(formatNumber(2_500_000)).toBe('2.50M'); expect(formatNumber(999_999_999)).toBe('1000.00M'); }); it('should format billions with B suffix', () => { expect(formatNumber(1_000_000_000)).toBe('1.00B'); expect(formatNumber(3_750_000_000)).toBe('3.75B'); expect(formatNumber(999_999_999_999)).toBe('1000.00B'); }); it('should format trillions with T suffix', () => { expect(formatNumber(1_000_000_000_000)).toBe('1.00T'); expect(formatNumber(5_250_000_000_000)).toBe('5.25T'); }); }); describe('formatTime', () => { it('should return 0:00 for zero seconds', () => { expect(formatTime(0)).toBe('0:00'); }); it('should format seconds correctly', () => { expect(formatTime(1)).toBe('0:01'); expect(formatTime(30)).toBe('0:30'); expect(formatTime(59)).toBe('0:59'); }); it('should format minutes correctly', () => { expect(formatTime(60)).toBe('1:00'); expect(formatTime(2 * 60 + 5)).toBe('2:05'); expect(formatTime(59 * 60 + 59)).toBe('59:59'); }); it('should format hours correctly', () => { expect(formatTime(60 * 60)).toBe('1:00:00'); expect(formatTime(2 * 60 * 60 + 60 + 5)).toBe('2:01:05'); }); it('should format days correctly', () => { expect(formatTime(24 * 60 * 60)).toBe('1d 0:00:00'); expect(formatTime(2 * 24 * 60 * 60)).toBe('2d 0:00:00'); }); it('should format weeks correctly', () => { expect(formatTime(7 * 24 * 60 * 60)).toBe('1w 0:00:00'); }); it('should format months correctly', () => { const month = 30 * 24 * 60 * 60; expect(formatTime(month)).toBe('1m 0:00:00'); }); it('should format years correctly', () => { const year = 365 * 24 * 60 * 60; expect(formatTime(year)).toBe('1y 0:00:00'); }); it('should format complex durations', () => { const total = 365 * 24 * 60 * 60 + // 1y 30 * 24 * 60 * 60 + // 1m 7 * 24 * 60 * 60 + // 1w 24 * 60 * 60 + // 1d 60 * 60 + // 1h 60 + // 1m 1; // 1s expect(formatTime(total)).toBe('1y 1m 1w 1d 1:01:01'); }); }); });