From bf6d18ba41da48029fed4fc5f66298c3d08e7cd4 Mon Sep 17 00:00:00 2001 From: Dinil Hansara Date: Fri, 31 Oct 2025 00:15:54 +0530 Subject: [PATCH] Add FNV-1a hash algorithm implementation - Implement FNV-1a (Fowler-Noll-Vo) non-cryptographic hash function - Support both 32-bit and 64-bit hash variants - Hash from String or byte array input - Include hex string output methods - All operations have O(n) time complexity where n is input length - Space complexity O(1) Features: - Generic implementation supporting String and byte arrays - Proper null handling with IllegalArgumentException - Helper methods: hash32Hex() and hash64Hex() for hex output - UTF-8 encoding support for Unicode characters - Full JavaDoc documentation FNV-1a properties: - Fast computation using simple XOR and multiply operations - Good hash distribution minimizing collisions - Widely used in hash tables and checksums - Not suitable for cryptographic purposes Tests: - 30+ comprehensive unit tests covering all methods - Edge cases: empty string, null input, Unicode - Consistency tests (same input -> same output) - Distribution tests (different inputs -> different outputs) - Both String and byte array input validation Time Complexity: O(n) where n is input length Space Complexity: O(1) --- .../com/thealgorithms/others/FNV1aHash.java | 140 ++++++++++++ .../thealgorithms/others/FNV1aHashTest.java | 216 ++++++++++++++++++ 2 files changed, 356 insertions(+) create mode 100644 src/main/java/com/thealgorithms/others/FNV1aHash.java create mode 100644 src/test/java/com/thealgorithms/others/FNV1aHashTest.java diff --git a/src/main/java/com/thealgorithms/others/FNV1aHash.java b/src/main/java/com/thealgorithms/others/FNV1aHash.java new file mode 100644 index 000000000000..f28040b2cb98 --- /dev/null +++ b/src/main/java/com/thealgorithms/others/FNV1aHash.java @@ -0,0 +1,140 @@ +package com.thealgorithms.others; + +/** + * FNV-1a (Fowler-Noll-Vo) is a non-cryptographic hash function created by Glenn Fowler, Landon + * Curt Noll, and Kiem-Phong Vo. + * + *

The FNV-1a variant provides better avalanche characteristics (bit changes distribute more + * uniformly) compared to the original FNV-1. + * + *

Key properties: + *

+ * + *

Algorithm: hash = FNV_offset_basis for each byte in data: hash = hash XOR byte hash = hash * + * FNV_prime + * + *

FNV-1a 32-bit: FNV_offset_basis = 2166136261 (0x811c9dc5) FNV_prime = 16777619 (0x01000193) + * + *

FNV-1a 64-bit: FNV_offset_basis = 14695981039346656037 (0xcbf29ce484222325) FNV_prime = + * 1099511628211 (0x100000001b3) + * + *

Time Complexity: O(n) where n is the length of the input + *

Space Complexity: O(1) + * + * @author dinilH + * @see FNV Hash + * @see FNV on Wikipedia + */ +public final class FNV1aHash { + + // FNV-1a 32-bit parameters + private static final int FNV_32_INIT = 0x811c9dc5; + private static final int FNV_32_PRIME = 0x01000193; + + // FNV-1a 64-bit parameters + private static final long FNV_64_INIT = 0xcbf29ce484222325L; + private static final long FNV_64_PRIME = 0x100000001b3L; + + private FNV1aHash() { + // Utility class - prevent instantiation + } + + /** + * Computes the 32-bit FNV-1a hash of the input string. + * + * @param data the input string to hash + * @return the 32-bit hash value as an integer + * @throws IllegalArgumentException if data is null + */ + public static int hash32(String data) { + if (data == null) { + throw new IllegalArgumentException("Input string cannot be null"); + } + return hash32(data.getBytes(java.nio.charset.StandardCharsets.UTF_8)); + } + + /** + * Computes the 32-bit FNV-1a hash of the input byte array. + * + * @param data the input byte array to hash + * @return the 32-bit hash value as an integer + * @throws IllegalArgumentException if data is null + */ + public static int hash32(byte[] data) { + if (data == null) { + throw new IllegalArgumentException("Input byte array cannot be null"); + } + + int hash = FNV_32_INIT; + + for (byte b : data) { + hash ^= (b & 0xff); // XOR with byte (ensure unsigned) + hash *= FNV_32_PRIME; // Multiply by FNV prime + } + + return hash; + } + + /** + * Computes the 64-bit FNV-1a hash of the input string. + * + * @param data the input string to hash + * @return the 64-bit hash value as a long + * @throws IllegalArgumentException if data is null + */ + public static long hash64(String data) { + if (data == null) { + throw new IllegalArgumentException("Input string cannot be null"); + } + return hash64(data.getBytes(java.nio.charset.StandardCharsets.UTF_8)); + } + + /** + * Computes the 64-bit FNV-1a hash of the input byte array. + * + * @param data the input byte array to hash + * @return the 64-bit hash value as a long + * @throws IllegalArgumentException if data is null + */ + public static long hash64(byte[] data) { + if (data == null) { + throw new IllegalArgumentException("Input byte array cannot be null"); + } + + long hash = FNV_64_INIT; + + for (byte b : data) { + hash ^= (b & 0xff); // XOR with byte (ensure unsigned) + hash *= FNV_64_PRIME; // Multiply by FNV prime + } + + return hash; + } + + /** + * Computes the 32-bit FNV-1a hash and returns it as a hex string. + * + * @param data the input string to hash + * @return the hash value as an 8-character hex string + * @throws IllegalArgumentException if data is null + */ + public static String hash32Hex(String data) { + return String.format("%08x", hash32(data)); + } + + /** + * Computes the 64-bit FNV-1a hash and returns it as a hex string. + * + * @param data the input string to hash + * @return the hash value as a 16-character hex string + * @throws IllegalArgumentException if data is null + */ + public static String hash64Hex(String data) { + return String.format("%016x", hash64(data)); + } +} diff --git a/src/test/java/com/thealgorithms/others/FNV1aHashTest.java b/src/test/java/com/thealgorithms/others/FNV1aHashTest.java new file mode 100644 index 000000000000..f6708ec02b10 --- /dev/null +++ b/src/test/java/com/thealgorithms/others/FNV1aHashTest.java @@ -0,0 +1,216 @@ +package com.thealgorithms.others; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class FNV1aHashTest { + + @Test + void testHash32EmptyString() { + // Empty string should return the FNV offset basis + assertEquals(0x811c9dc5, FNV1aHash.hash32("")); + } + + @Test + void testHash32SingleCharacter() { + assertEquals(0xe40c292c, FNV1aHash.hash32("a")); + assertEquals(0xe70c2de5, FNV1aHash.hash32("b")); + assertEquals(0xe60c2c52, FNV1aHash.hash32("c")); + } + + @Test + void testHash32SimpleStrings() { + assertEquals(0x4ed566da, FNV1aHash.hash32("Hello")); + assertEquals(0xdcfc127b, FNV1aHash.hash32("World")); + assertEquals(0xc0bb652b, FNV1aHash.hash32("Algorithms")); + } + + @Test + void testHash32LongString() { + assertEquals(0x048fff90, FNV1aHash.hash32("The quick brown fox jumps over the lazy dog")); + } + + @Test + void testHash32Consistency() { + // Same input should always produce same output + String input = "test"; + int hash1 = FNV1aHash.hash32(input); + int hash2 = FNV1aHash.hash32(input); + assertEquals(hash1, hash2); + } + + @Test + void testHash32Differentiation() { + // Different inputs should produce different outputs + int hash1 = FNV1aHash.hash32("test"); + int hash2 = FNV1aHash.hash32("Test"); + assertNotEquals(hash1, hash2); + } + + @Test + void testHash32WithByteArray() { + byte[] data = "Hello".getBytes(java.nio.charset.StandardCharsets.UTF_8); + assertEquals(0x4ed566da, FNV1aHash.hash32(data)); + } + + @Test + void testHash32WithEmptyByteArray() { + byte[] data = new byte[0]; + assertEquals(0x811c9dc5, FNV1aHash.hash32(data)); + } + + @Test + void testHash32NullStringThrowsException() { + assertThrows(IllegalArgumentException.class, () -> FNV1aHash.hash32((String) null)); + } + + @Test + void testHash32NullByteArrayThrowsException() { + assertThrows(IllegalArgumentException.class, () -> FNV1aHash.hash32((byte[]) null)); + } + + @Test + void testHash64EmptyString() { + // Empty string should return the FNV offset basis + assertEquals(0xcbf29ce484222325L, FNV1aHash.hash64("")); + } + + @Test + void testHash64SingleCharacter() { + assertEquals(0xaf63dc4c8601ec8cL, FNV1aHash.hash64("a")); + assertEquals(0xaf63df4c8601f1a5L, FNV1aHash.hash64("b")); + assertEquals(0xaf63de4c8601f012L, FNV1aHash.hash64("c")); + } + + @Test + void testHash64SimpleStrings() { + assertEquals(0x63f4e1f2c97e89ebL, FNV1aHash.hash64("Hello")); + assertEquals(0x0f18f77b44424a53L, FNV1aHash.hash64("World")); + assertEquals(0x289a4c6f7f076f3bL, FNV1aHash.hash64("Algorithms")); + } + + @Test + void testHash64LongString() { + assertEquals(0xf3f9b7f5e7e47110L, FNV1aHash.hash64("The quick brown fox jumps over the lazy dog")); + } + + @Test + void testHash64Consistency() { + // Same input should always produce same output + String input = "test"; + long hash1 = FNV1aHash.hash64(input); + long hash2 = FNV1aHash.hash64(input); + assertEquals(hash1, hash2); + } + + @Test + void testHash64Differentiation() { + // Different inputs should produce different outputs + long hash1 = FNV1aHash.hash64("test"); + long hash2 = FNV1aHash.hash64("Test"); + assertNotEquals(hash1, hash2); + } + + @Test + void testHash64WithByteArray() { + byte[] data = "Hello".getBytes(java.nio.charset.StandardCharsets.UTF_8); + assertEquals(0x63f4e1f2c97e89ebL, FNV1aHash.hash64(data)); + } + + @Test + void testHash64WithEmptyByteArray() { + byte[] data = new byte[0]; + assertEquals(0xcbf29ce484222325L, FNV1aHash.hash64(data)); + } + + @Test + void testHash64NullStringThrowsException() { + assertThrows(IllegalArgumentException.class, () -> FNV1aHash.hash64((String) null)); + } + + @Test + void testHash64NullByteArrayThrowsException() { + assertThrows(IllegalArgumentException.class, () -> FNV1aHash.hash64((byte[]) null)); + } + + @Test + void testHash32HexFormat() { + assertEquals("4ed566da", FNV1aHash.hash32Hex("Hello")); + assertEquals("dcfc127b", FNV1aHash.hash32Hex("World")); + assertEquals("811c9dc5", FNV1aHash.hash32Hex("")); // Empty string + } + + @Test + void testHash64HexFormat() { + assertEquals("63f4e1f2c97e89eb", FNV1aHash.hash64Hex("Hello")); + assertEquals("0f18f77b44424a53", FNV1aHash.hash64Hex("World")); + assertEquals("cbf29ce484222325", FNV1aHash.hash64Hex("")); // Empty string + } + + @Test + void testHash32HexNullThrowsException() { + assertThrows(IllegalArgumentException.class, () -> FNV1aHash.hash32Hex(null)); + } + + @Test + void testHash64HexNullThrowsException() { + assertThrows(IllegalArgumentException.class, () -> FNV1aHash.hash64Hex(null)); + } + + @Test + void testHash32WithUnicodeCharacters() { + // Test with Unicode characters + assertDoesNotThrow(() -> FNV1aHash.hash32("Hello δΈ–η•Œ")); + assertDoesNotThrow(() -> FNV1aHash.hash32("πŸš€ Rocket")); + + // Different Unicode strings should have different hashes + assertNotEquals(FNV1aHash.hash32("Hello"), FNV1aHash.hash32("Hello δΈ–η•Œ")); + } + + @Test + void testHash64WithUnicodeCharacters() { + // Test with Unicode characters + assertDoesNotThrow(() -> FNV1aHash.hash64("Hello δΈ–η•Œ")); + assertDoesNotThrow(() -> FNV1aHash.hash64("πŸš€ Rocket")); + + // Different Unicode strings should have different hashes + assertNotEquals(FNV1aHash.hash64("Hello"), FNV1aHash.hash64("Hello δΈ–η•Œ")); + } + + @Test + void testHash32Distribution() { + // Test that similar strings produce different hashes + int hash1 = FNV1aHash.hash32("algorithm"); + int hash2 = FNV1aHash.hash32("algorithms"); + int hash3 = FNV1aHash.hash32("algorithmz"); + + assertNotEquals(hash1, hash2); + assertNotEquals(hash2, hash3); + assertNotEquals(hash1, hash3); + } + + @Test + void testHash64Distribution() { + // Test that similar strings produce different hashes + long hash1 = FNV1aHash.hash64("algorithm"); + long hash2 = FNV1aHash.hash64("algorithms"); + long hash3 = FNV1aHash.hash64("algorithmz"); + + assertNotEquals(hash1, hash2); + assertNotEquals(hash2, hash3); + assertNotEquals(hash1, hash3); + } + + @Test + void testHash32WithNumericStrings() { + assertNotEquals(FNV1aHash.hash32("123"), FNV1aHash.hash32("1234")); + assertNotEquals(FNV1aHash.hash32("999"), FNV1aHash.hash32("1000")); + } + + @Test + void testHash64WithNumericStrings() { + assertNotEquals(FNV1aHash.hash64("123"), FNV1aHash.hash64("1234")); + assertNotEquals(FNV1aHash.hash64("999"), FNV1aHash.hash64("1000")); + } +}