-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(utils): add getHashFromCollection()
- Loading branch information
1 parent
75c9a78
commit 1664a59
Showing
3 changed files
with
124 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import { expect } from '@jest/globals' | ||
|
||
import { getHashFromCollection } from '../getHashFromCollection' | ||
|
||
describe('utils/getHashFromCollection()', () => { | ||
it('should return consistent hashes for collections with the same content', () => { | ||
const firstCollection = [ | ||
{ id: 1, name: 'Item A' }, | ||
{ id: 2, name: 'Item B' } | ||
] | ||
const secondCollection = [ | ||
{ id: 1, name: 'Item A' }, | ||
{ id: 2, name: 'Item B' } | ||
] | ||
|
||
const firstResult = getHashFromCollection(firstCollection) | ||
const secondResult = getHashFromCollection(secondCollection) | ||
|
||
expect(firstResult).toBe(secondResult) | ||
}) | ||
|
||
it('should return different hashes for collections with different content', () => { | ||
const firstCollection = [{ id: 1, name: 'Item A' }] | ||
const secondCollection = [{ id: 2, name: 'Item B' }] | ||
|
||
const firstResult = getHashFromCollection(firstCollection) | ||
const secondResult = getHashFromCollection(secondCollection) | ||
|
||
expect(firstResult).not.toBe(secondResult) | ||
}) | ||
|
||
it('should return consistent hashes regardless of item order when using `sortingKey`', () => { | ||
const firstCollection = [ | ||
{ id: 2, name: 'Item B' }, | ||
{ id: 1, name: 'Item A' } | ||
] | ||
const secondCollection = [ | ||
{ id: 1, name: 'Item A' }, | ||
{ id: 2, name: 'Item B' } | ||
] | ||
|
||
const firstResult = getHashFromCollection(firstCollection, 'id') | ||
const secondResult = getHashFromCollection(secondCollection, 'id') | ||
|
||
expect(firstResult).toBe(secondResult) | ||
}) | ||
|
||
it('should return different hashes based on item order when no `sortingKey` is provided', () => { | ||
const firstCollection = [ | ||
{ id: 2, name: 'Item B' }, | ||
{ id: 1, name: 'Item A' } | ||
] | ||
const secondCollection = [ | ||
{ id: 1, name: 'Item A' }, | ||
{ id: 2, name: 'Item B' } | ||
] | ||
|
||
const firstResult = getHashFromCollection(firstCollection) | ||
const secondResult = getHashFromCollection(secondCollection) | ||
|
||
expect(firstResult).not.toBe(secondResult) | ||
}) | ||
|
||
it('should return "0" for an empty collection', () => { | ||
const collection = [] | ||
|
||
const result = getHashFromCollection(collection) | ||
|
||
expect(result).toBe('0') | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
/* eslint-disable no-bitwise */ | ||
|
||
import { sortBy } from 'lodash/fp' | ||
|
||
/** | ||
* Generates a simple hash from a collection. | ||
* | ||
* @description | ||
* The function converts the collection into a string representation (JSON) to generate a hash for that string. | ||
* | ||
* In order to keep the hash generation independant from the collection order, you should provide a sorting key. | ||
* | ||
* Note: This hash is designed for simplicity and speed, it can't be used for cryptographic security. | ||
* | ||
* @param collection An array of objects | ||
* @param sortingKey A sortable object key to keep the hash consistent regardless of the collection order | ||
* @returns A string representation of the hash | ||
*/ | ||
export function getHashFromCollection<T extends Record<string, any> = Record<string, any>>( | ||
collection: T[], | ||
sortingKey?: keyof T | ||
): string { | ||
// If the collection is empty, simply return "0" | ||
if (collection.length === 0) { | ||
return '0' | ||
} | ||
|
||
const sortedCollection = sortingKey ? sortBy([sortingKey], collection) : collection | ||
|
||
const collectionAsJson = JSON.stringify(sortedCollection) | ||
|
||
// Initialize hash value | ||
let hashAsNumber = 0 | ||
|
||
let characterAsUnicodeValue: number | ||
|
||
// Iterate over each character in the string representation of the collection | ||
for (let index = 0; index < collectionAsJson.length; index += 1) { | ||
characterAsUnicodeValue = collectionAsJson.charCodeAt(index) | ||
|
||
// Modify the hash value based on the current character's code. | ||
// The operation `(hashAsNumber << 5) - hashAsNumber` rapidly increases the hash value, | ||
// and adding the character code `+ characterAsUnicodeValue` further modifies it. | ||
// This produces a wide range of hash values for different inputs. | ||
hashAsNumber = (hashAsNumber << 5) - hashAsNumber + characterAsUnicodeValue | ||
|
||
// This operation ensures we get a 32-bit integer, effectively wrapping the hash value if it exceeds 32 bit | ||
hashAsNumber |= 0 | ||
} | ||
|
||
return hashAsNumber.toString() | ||
} |