Skip to content

Commit

Permalink
feat(utils): add getHashFromCollection()
Browse files Browse the repository at this point in the history
  • Loading branch information
ivangabriele committed Sep 28, 2023
1 parent 75c9a78 commit 1664a59
Show file tree
Hide file tree
Showing 3 changed files with 124 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export { TableWithSelectableRows } from './tables/TableWithSelectableRows'
export { cleanString } from './utils/cleanString'
export { customDayjs } from './utils/customDayjs'
export { getCoordinates, coordinatesAreDistinct } from './utils/coordinates'
export { getHashFromCollection } from './utils/getHashFromCollection'
export { getLocalizedDayjs } from './utils/getLocalizedDayjs'
export { getOptionsFromIdAndName } from './utils/getOptionsFromIdAndName'
export { getOptionsFromLabelledEnum } from './utils/getOptionsFromLabelledEnum'
Expand Down
71 changes: 71 additions & 0 deletions src/utils/__tests__/getHashFromCollection.test.ts
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')
})
})
52 changes: 52 additions & 0 deletions src/utils/getHashFromCollection.ts
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()
}

0 comments on commit 1664a59

Please sign in to comment.