UNPKG

@rimbu/hashed

Version:

Immutable HashMap and HashSet implementations for TypeScript

566 lines 21 kB
import { Eq } from '@rimbu/common'; import { Stream } from '@rimbu/stream'; export var Hasher; (function (Hasher) { const MAX_STEP_BITS = 6; const STRING_INIT = 21; const BOOL_TRUE = -37; const BOOL_FALSE = -73; const OBJ_INIT = 91; const UNDEF_VALUE = -31; const NULL_VALUE = -41; const _anyFlatHasher = createAnyHasher('FLAT'); const _anyShallowHasher = createAnyHasher('SHALLOW'); const _anyDeepHasher = createAnyHasher('DEEP'); function defaultHasher() { return _anyShallowHasher; } Hasher.defaultHasher = defaultHasher; function createStringHasher(maxStepBits) { const maxSteps = 1 << maxStepBits; return Object.freeze({ isValid(obj) { return typeof obj === 'string'; }, hash(value) { const length = Math.min(value.length, maxSteps); const stepSize = Math.max(1, value.length >>> maxStepBits); let result = STRING_INIT; // implement times 31 = 32 - 1 for (let i = 0; i < length; i += stepSize) { result = ((result << 5) - result + value.charCodeAt(i)) | 0; } return result; }, }); } const _stringHasher = createStringHasher(MAX_STEP_BITS); /** * Returns a Hasher instance for string values. * @example * ```ts * const h = Hasher.stringHasher() * h.hash('abc') * ``` */ function stringHasher() { return _stringHasher; } Hasher.stringHasher = stringHasher; const _anyToStringHasher = Object.freeze({ isValid(obj) { return true; }, hash(value) { return _stringHasher.hash(Eq.convertAnyToString(value)); }, }); /** * Returns a Hasher instance that hashes the string representation of any value * @param maxStepBits - the maximum amount of samples to take from the string * @example * ```ts * const h = Hasher.anyToStringHasher() * h.hash([1, 3, 'a']) * ``` */ function anyToStringHasher(maxStepBits) { if (undefined === maxStepBits) return _anyToStringHasher; return createStringHasher(maxStepBits); } Hasher.anyToStringHasher = anyToStringHasher; const _anyJsonStringHasher = Object.freeze({ isValid(obj) { return true; }, hash(value) { return _stringHasher.hash(JSON.stringify(value)); }, }); /** * Returns a Hasher instance that hashes any value by hashing the string resulting from * applying JSON.stringify to the value. * @example * ```ts * const h = Hasher.anyJsonStringHasher() * console.log(h.hash({ a: 1, b: 2 }) === h.hash({ b: 2, a: 1 })) * // => false * ``` */ function anyJsonStringHasher() { return _anyJsonStringHasher; } Hasher.anyJsonStringHasher = anyJsonStringHasher; function createStringCaseInsensitiveHasher(maxStepBits) { const maxSteps = 1 << maxStepBits; return Object.freeze({ isValid(obj) { return typeof obj === 'string'; }, hash(value) { const length = Math.min(value.length, maxSteps); const stepSize = Math.max(1, value.length >>> maxStepBits); let result = STRING_INIT; // implement times 31 = 32 - 1 for (let i = 0; i < length; i += stepSize) { const char = value.charAt(i).toUpperCase(); result = ((result << 5) - result + char.charCodeAt(0)) | 0; } return result; }, }); } const _stringCaseInsensitiveHasher = createStringCaseInsensitiveHasher(MAX_STEP_BITS); function stringCaseInsensitiveHasher() { return _stringCaseInsensitiveHasher; } Hasher.stringCaseInsensitiveHasher = stringCaseInsensitiveHasher; function createArrayHasher(itemHasher, maxStepBits) { const maxSteps = 1 << maxStepBits; return Object.freeze({ isValid(obj) { return Array.isArray(obj); }, hash(value) { const length = Math.min(value.length, maxSteps); const stepSize = Math.max(1, value.length >>> maxStepBits); let result = value.length | 0; // implement times 31 = 32 - 1 for (let i = 0; i < length; i += stepSize) { result = ((result << 5) - result + (i + 1) * itemHasher.hash(value[i])) | 0; } return result; }, }); } const _arrayAnyHasher = createArrayHasher(defaultHasher(), MAX_STEP_BITS); /** * Returns a Hasher that hashes arrays of elements by sampling the array and using * the given `itemHasher` to hash the sampled elements. * @typeparam T - the array element type * @param options - (optional) an object containing the following items:<br/> * - itemHasher: (optional) a Hasher instance used to hash elements in the array<br/> * - maxStepBits: (optional) the amount of bits to determine the maximum amount of array * elements to process * @example * ```ts * const h = Hasher.arrayHasher() * console.log(h.hash([1, 2, 3] === h.hash([1, 3, 2]))) * // => false * ``` */ function arrayHasher(options) { if (undefined === options) return _arrayAnyHasher; return createArrayHasher(options.itemHasher ?? anyFlatHasher(), options.maxStepBits ?? MAX_STEP_BITS); } Hasher.arrayHasher = arrayHasher; function createStreamSourceHasher(itemHasher = defaultHasher(), maxStepBits = MAX_STEP_BITS) { const maxSteps = 1 << maxStepBits; return Object.freeze({ isValid(obj) { return (typeof obj === 'object' && obj !== null && Symbol.iterator in obj); }, hash(source) { const iter = Stream.from(source)[Symbol.iterator](); let hashItems = []; const storeItems = []; let skipAmount = 1; let skipCount = 0; const done = Symbol('done'); let value; let length = 0; while (done !== (value = iter.fastNext(done))) { length++; skipCount++; if (skipCount >= skipAmount) { storeItems.push(value); skipCount = 0; } if (storeItems.length >= maxSteps) { hashItems = storeItems.slice(); // remove elements from store items const newLength = maxSteps >>> 1; for (let index = 0; index < newLength; index += 1) { storeItems[index] = storeItems[index * 2]; } storeItems.length = newLength; skipAmount *= 2; } } if (hashItems.length === 0) { hashItems = storeItems; } const itemHash = createArrayHasher(itemHasher, maxStepBits).hash(hashItems); // include length in hash return ((itemHash << 5) - itemHash + length) | 0; }, }); } const _streamSourceAnyHasher = createStreamSourceHasher(defaultHasher(), MAX_STEP_BITS); /** * Returns a Hasher instance that hashes any StreamSource limited to a certain amount * of elements to prevent haning on infinite streams. * @typeparam T - the StreamSource element type * @param options - (optional) an object containing the following items:<br/> * - itemHasher: (optional) a Hasher instance used to hash elements in the array<br/> * - maxStepBits: (optional) the amount of bits to determine the maximum amount of array * elements to process * @example * ```ts * const h = Hasher.streamSourceHasher() * h.hash(Stream.random()) * // infinite stream but will not hang due to the max step limit * ``` */ function streamSourceHasher(options) { if (undefined === options) return _streamSourceAnyHasher; return createStreamSourceHasher(options.itemHasher, options.maxStepBits); } Hasher.streamSourceHasher = streamSourceHasher; // bitwise operators only work on 32-bit integers const MIN_HASH = -Math.pow(2, 31); const MAX_HASH = Math.pow(2, 31) - 1; const _numberHasher = Object.freeze({ isValid(obj) { return typeof obj === 'number'; }, hash(value) { if (Number.isInteger(value)) { return value | 0; } if (Number.isNaN(value)) { return MAX_HASH - 1; } if (value === Number.POSITIVE_INFINITY) { return MAX_HASH; } if (value === Number.NEGATIVE_INFINITY) { return MIN_HASH; } // not sure what else it could be return _anyToStringHasher.hash(value); }, }); /** * Returns a Hasher instance that hashes numbers, including 'special' values like NaN and infinities. * @example * ```ts * const h = Hasher.numberHasher() * console.log(h.hash(Number.POSITIVE_INFINITY) === h.hash(Number.NEGATIVE_INFINITY)) * // => false * console.log(h.hash(Number.NaN) === h.hash(Number.NaN)) * // => true * ``` */ function numberHasher() { return _numberHasher; } Hasher.numberHasher = numberHasher; const _booleanHasher = Object.freeze({ isValid(obj) { return typeof obj === 'boolean'; }, hash(value) { return value ? BOOL_TRUE : BOOL_FALSE; }, }); /** * Returns a Hasher instance that hashes booleans. * @example * ```ts * const h = Hasher.booleanHasher() * console.log(h.hash(true) === h.hash(false)) * // => false * ``` */ function booleanHasher() { return _booleanHasher; } Hasher.booleanHasher = booleanHasher; const _bigintHasher = Object.freeze({ isValid(obj) { return typeof obj === 'bigint'; }, hash: _anyToStringHasher.hash, }); /** * Returns a Hasher instance that hashes bigints. * @example * ```ts * const h = Hasher.bigintHasher() * console.log(h.hash(BigInt(5)) === h.hash(BigInt(10))) * // => false * ``` */ function bigintHasher() { return _bigintHasher; } Hasher.bigintHasher = bigintHasher; /** * Returns a Hasher instance that hashes the `.valueOf` value of the given * object using the given `valueHasher` for instances of given `cls` class. * @typeparam T - the input object type * @typeparam V - the .valueOf property type * @param cls - the class containing the contructur to check for validity of a given object * @param valueHasher - the Hasher instance to use for the `.valueOf` values * @example * ```ts * const h = Hasher.createValueOfHasher(Date) * console.log(h.isValid(new Boolean(true))) * // => false * const d1 = new Date() * const d2 = new Date(d1) * console.log(h.hash(d1) === h.hash(d2)) * // => true * ``` */ function createValueOfHasher(cls, valueHasher = anyFlatHasher()) { return Object.freeze({ isValid(obj) { return obj instanceof cls; }, hash(value) { return valueHasher.hash(value.valueOf()); }, }); } Hasher.createValueOfHasher = createValueOfHasher; const _BooleanHasher = createValueOfHasher(Boolean, _booleanHasher); const _DateHasher = createValueOfHasher(Date, _numberHasher); /** * Returns a Hasher instance that hashes Dates. * @example * ```ts * const h = Hasher.dateHasher() * const d1 = new Date() * const d2 = new Date(d1) * console.log(h.hash(d1) === h.hash(d2)) * // => true * ``` */ function dateHasher() { return _DateHasher; } Hasher.dateHasher = dateHasher; const _NumberHasher = createValueOfHasher(Number, _numberHasher); const _StringHasher = createValueOfHasher(String, _stringHasher); const _wrappedHashers = [ _BooleanHasher, _DateHasher, _NumberHasher, _StringHasher, ]; function tryWrappedHasher(value) { let i = -1; const len = _wrappedHashers.length; while (++i < len) { const hasher = _wrappedHashers[i]; if (hasher.isValid(value)) { return hasher.hash(value); } } return undefined; } function createObjectHasher(keyHasher, valueHasher) { return Object.freeze({ isValid(obj) { return typeof obj === 'object'; }, hash(value) { if (value === null) return NULL_VALUE; let result = OBJ_INIT; // implement order independent hash for (const key in value) { const keyValue = value[key]; const keyHash = keyHasher.hash(key); const valueHash = valueHasher.hash(keyValue); result = result ^ (keyHash + valueHash); } return result; }, }); } const _objectShallowHasher = createObjectHasher(anyFlatHasher(), anyFlatHasher()); const _objectDeepHasher = createObjectHasher(anyFlatHasher(), anyDeepHasher()); /** * Returns a Hasher instance that hashes objects of key type K and value type V. * @typeparam K - the key type * @typeparam V - the value type * @param options - (optional) an object containing:<br/> * - keyHasher: (optional) a Hasher instance that is used to hash object keys<br/> * - valueHasher: (optional) a Hasher instance that is used to hash object values * @example * ```ts * const h = Hasher.objectHasher() * console.log(h.hash({ a: 1, b: 2 }) === h.hash({ b: 2, a: 1 })) * // => true * ``` */ function objectHasher(options) { if (undefined === options) return _objectShallowHasher; return createObjectHasher(options.keyHasher, options.valueHasher); } Hasher.objectHasher = objectHasher; /** * Returns a Hasher instance that hashes objects of key type K and value type V. * If a value if an object or array, it will convert those values to a string. * @typeparam K - the key type * @typeparam V - the value type * @example * ```ts * const h = Hasher.objectShallowHasher() * console.log(h.hash({ a: 1, b: 2 }) === h.hash({ b: 2, a: 1 })) * // => true * ``` */ function objectShallowHasher() { return _objectShallowHasher; } Hasher.objectShallowHasher = objectShallowHasher; /** * Returns a Hasher instance that hashes objects of key type K and value type V. * If a value if an object or array, it will recursively hash its values. * @note be careful with circular structures, they can cause an infinite loop * @typeparam K - the key type * @typeparam V - the value type * @example * ```ts * const h = Hasher.objectDeepHasher() * console.log(h.hash({ a: 1, b: 2 }) === h.hash({ b: 2, a: 1 })) * // => true * ``` */ function objectDeepHasher() { return _objectDeepHasher; } Hasher.objectDeepHasher = objectDeepHasher; function createAnyHasher(mode, maxStepBits = MAX_STEP_BITS) { return Object.freeze({ isValid(obj) { return true; }, hash(value) { const valueType = typeof value; switch (valueType) { case 'undefined': return UNDEF_VALUE; case 'bigint': return _bigintHasher.hash(value); case 'boolean': return _booleanHasher.hash(value); case 'number': return _numberHasher.hash(value); case 'string': return _stringHasher.hash(value); case 'function': case 'symbol': return _anyToStringHasher.hash(value); case 'object': { if (null === value) return NULL_VALUE; const result = tryWrappedHasher(value); if (undefined !== result) return result; if (mode !== 'FLAT') { if (Array.isArray(value)) { if (mode === 'SHALLOW') { return createArrayHasher(_anyFlatHasher, MAX_STEP_BITS).hash(value); } return createArrayHasher(this, maxStepBits).hash(value); } if (_streamSourceAnyHasher.isValid(value)) { if (mode === 'SHALLOW') { return createStreamSourceHasher(_anyFlatHasher, maxStepBits).hash(value); } return createStreamSourceHasher(this, maxStepBits).hash(value); } if (_objectShallowHasher.isValid(value)) { if (mode === 'SHALLOW') return _objectShallowHasher.hash(value); return createObjectHasher(_anyFlatHasher, this).hash(value); } } return _anyToStringHasher.hash(value); } } }, }); } /** * Returns a Hasher instance that hashes any value, but never traverses into an object * or array to hash its elements. In those cases it will use toString. * @example * ```ts * const h = Hasher.anyFlatHasher() * console.log(h.hash({ a: 1, b: 2 }) === h.hash({ b: 2, a: 1 })) * // => false * ``` */ function anyFlatHasher() { return _anyFlatHasher; } Hasher.anyFlatHasher = anyFlatHasher; /** * Returns a Hasher instance that hashes any value, but only traverses into an object * or array to hash its elements one level deep. After one level, it will use toString. * @example * ```ts * const h = Hasher.anyShallowHasher() * console.log(h.hash({ a: 1, b: 2 }) === h.hash({ b: 2, a: 1 })) * // => true * console.log(h.hash([{ a: 1, b: 2 }]) === h.hash([{ b: 2, a: 1 }])) * // => false * ``` */ function anyShallowHasher() { return _anyShallowHasher; } Hasher.anyShallowHasher = anyShallowHasher; /** * Returns a Hasher instance that hashes any value, and traverses into an object * or array to hash its elements. * @example * ```ts * const h = Hasher.anyDeepHasher() * console.log(h.hash({ a: 1, b: 2 }) === h.hash({ b: 2, a: 1 })) * // => true * console.log(h.hash([{ a: 1, b: 2 }]) === h.hash([{ b: 2, a: 1 }])) * // => true * ``` */ function anyDeepHasher() { return _anyDeepHasher; } Hasher.anyDeepHasher = anyDeepHasher; /** * Returns a Hasher that will return equal hash values for values in a tuple regardless * of their order, and uses the given `hasher` function to hash the tuple elements. * @param hasher - the Hasher instance to use for tuple elements * @example * ```ts * const h = Hasher.tupleSymmetric() * console.log(h.hash(['abc', 'def']) === h.hash(['def', 'abc'])) * ``` */ function tupleSymmetric(hasher = anyShallowHasher()) { return Object.freeze({ isValid(obj) { return (Array.isArray(obj) && obj.length === 2 && hasher.isValid(obj[0]) && hasher.isValid(obj[1])); }, hash(value) { return (hasher.hash(value[0]) + hasher.hash(value[1])) | 0; }, }); } Hasher.tupleSymmetric = tupleSymmetric; })(Hasher || (Hasher = {})); //# sourceMappingURL=hasher.mjs.map