diff --git a/frontend/src/helpers/stringHash.test.ts b/frontend/src/helpers/stringHash.test.ts new file mode 100644 index 000000000..3626acf0c --- /dev/null +++ b/frontend/src/helpers/stringHash.test.ts @@ -0,0 +1,21 @@ +import {describe, it, expect} from 'vitest' +import {stringHash} from './stringHash' + +describe('stringHash', () => { + it('returns a non-negative integer', () => { + expect(stringHash('hello')).toBeGreaterThanOrEqual(0) + expect(Number.isInteger(stringHash('hello'))).toBe(true) + }) + + it('is deterministic for the same input', () => { + expect(stringHash('foo')).toBe(stringHash('foo')) + }) + + it('returns different values for different inputs', () => { + expect(stringHash('foo')).not.toBe(stringHash('bar')) + }) + + it('handles the empty string', () => { + expect(stringHash('')).toBeGreaterThanOrEqual(0) + }) +}) diff --git a/frontend/src/helpers/stringHash.ts b/frontend/src/helpers/stringHash.ts new file mode 100644 index 000000000..b843b1fbe --- /dev/null +++ b/frontend/src/helpers/stringHash.ts @@ -0,0 +1,11 @@ +// Deterministic non-cryptographic string hash (djb2 variant). +// Used for stable pseudo-random selection keyed on date + user + bucket. +export function stringHash(input: string): number { + let hash = 5381 + for (let i = 0; i < input.length; i++) { + // hash * 33 + char, kept in 32-bit range via `| 0`. + hash = ((hash << 5) + hash + input.charCodeAt(i)) | 0 + } + // Ensure non-negative. + return hash >>> 0 +}