@rimbu/hashed
Version:
Immutable HashMap and HashSet implementations for TypeScript
566 lines • 21 kB
JavaScript
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