UNPKG

nhb-toolbox

Version:

A versatile collection of smart, efficient, and reusable utility functions, classes and types for everyday development needs.

193 lines (192 loc) 7.42 kB
import { isFunction } from '../guards/non-primitives.js'; import { isString } from '../guards/primitives.js'; export class Finder { static #DEFAULT_TTL = 1000 * 60 * 5; #cachedResult = new Map(); #sortedCache = new Map(); #ttl; #items; constructor(data, ttl = Finder.#DEFAULT_TTL) { this.#ttl = ttl; this.#items = isFunction(data) ? data() : data; } clearCache(key) { if (key) { this.#cachedResult.delete(key); } else { this.#cachedResult.clear(); } } findAll(matcher, keySelector, options) { const { fuzzy = false, needSorting = true, cacheKey = 'finder-cache', forceBinary = false, caseInsensitive = true, data, } = options ?? {}; const source = isFunction(data) ? data() : (data ?? this.#items); if (!source?.length) return []; const rawGetKey = typeof keySelector === 'function' ? keySelector : (item) => item[keySelector]; const getKey = Finder.#createMemoizedKeyGetter(rawGetKey); const normalizedMatcher = caseInsensitive && isString(matcher) ? matcher.toLowerCase() : matcher; if (cacheKey) { const entry = this.#cachedResult.get(cacheKey); if (entry && Date.now() - entry.timestamp < this.#ttl) { return entry.result; } else { this.#cachedResult.delete(cacheKey); } } let results = []; if (source.length < 100 && !forceBinary) { results = source.filter((item) => { const key = getKey(item); const value = caseInsensitive && isString(key) ? key.toLowerCase() : key; return value === normalizedMatcher; }); } else { const sorted = needSorting ? this.#sortAndCache(source, getKey, cacheKey) : source; const firstMatch = this.binarySearch(sorted, normalizedMatcher, getKey, caseInsensitive); if (firstMatch) { const baseKey = getKey(firstMatch); const base = caseInsensitive && isString(baseKey) ? baseKey.toLowerCase() : baseKey; results = sorted.filter((item) => { const key = getKey(item); const value = caseInsensitive && isString(key) ? key.toLowerCase() : key; return value === base; }); } } if (!results.length && fuzzy && isString(normalizedMatcher)) { results = source.filter((item) => { const rawKey = getKey(item); const key = caseInsensitive && isString(rawKey) ? rawKey.toLowerCase() : String(rawKey); return this.#match(key, normalizedMatcher); }); } if (cacheKey) { this.#cachedResult.set(cacheKey, { result: results, timestamp: Date.now(), }); } return results; } findOne(matcher, keySelector, options) { const { fuzzy = false, needSorting = true, cacheKey = 'finder-cache', forceBinary = false, caseInsensitive = true, data, } = options ?? {}; const source = isFunction(data) ? data() : (data ?? this.#items); if (!source?.length) return undefined; const rawGetKey = typeof keySelector === 'function' ? keySelector : (item) => item[keySelector]; const getKey = Finder.#createMemoizedKeyGetter(rawGetKey); const normalizedMatcher = caseInsensitive && isString(matcher) ? matcher.toLowerCase() : matcher; if (cacheKey) { const entry = this.#cachedResult.get(cacheKey); if (entry && Date.now() - entry.timestamp < this.#ttl) { return entry.result[0]; } else { this.#cachedResult.delete(cacheKey); } } let result; if (source?.length < 100 && !forceBinary) { result = source?.find((item) => { const key = getKey(item); const value = caseInsensitive && isString(key) ? key.toLowerCase() : key; return value === normalizedMatcher; }); } else { result = this.binarySearch(needSorting ? this.#sortAndCache(source, getKey, cacheKey) : source, normalizedMatcher, getKey, caseInsensitive); } if (!result && fuzzy && isString(normalizedMatcher)) { return this.fuzzySearch(source, normalizedMatcher, getKey, caseInsensitive); } if (cacheKey && result) { this.#cachedResult.set(cacheKey, { result: [result], timestamp: Date.now(), }); } return result; } async findAllAsync(supplier, matcher, keySelector, options) { const items = await supplier(); return this.findAll(matcher, keySelector, { ...options, data: items }); } async findOneAsync(supplier, matcher, keySelector, options) { const items = await supplier(); return this.findOne(matcher, keySelector, { ...options, data: items }); } binarySearch(sorted, matcher, keySelector, caseInsensitive) { let min = 0, max = sorted?.length - 1; while (min <= max) { const mid = Math.floor((min + max) / 2); const midKey = keySelector(sorted[mid]); const key = caseInsensitive && isString(midKey) ? midKey.toLowerCase() : midKey; if (key === matcher) return sorted[mid]; if (key < matcher) min = mid + 1; else max = mid - 1; } return undefined; } fuzzySearch(array, matcher, keySelector, caseInsensitive) { for (const item of array) { const rawKey = keySelector(item); const key = caseInsensitive && isString(rawKey) ? rawKey.toLowerCase() : String(rawKey); if (this.#match(key, matcher)) return item; } return undefined; } #match(source, target) { let i = 0; for (const char of target) { i = source?.indexOf(char, i); if (i === -1) return false; i++; } return true; } #sortAndCache(data, getKey, cacheKey) { if (cacheKey) { const entry = this.#sortedCache.get(cacheKey); if (entry && Date.now() - entry.timestamp < this.#ttl) { return entry.result; } else { this.#sortedCache.delete(cacheKey); } } const sorted = [...data].sort((a, b) => { const keyA = getKey(a); const keyB = getKey(b); return keyA < keyB ? -1 : keyA > keyB ? 1 : 0; }); if (cacheKey) { this.#sortedCache.set(cacheKey, { result: sorted, timestamp: Date.now(), }); } return sorted; } static #createMemoizedKeyGetter(getKey) { const cache = new Map(); return (item) => { if (cache.has(item)) return cache.get(item); const key = getKey(item); cache.set(item, key); return key; }; } }