UNPKG

nhb-toolbox

Version:

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

197 lines (196 loc) 7.81 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Finder = void 0; const non_primitives_1 = require("../guards/non-primitives"); const primitives_1 = require("../guards/primitives"); 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 = (0, non_primitives_1.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 = (0, non_primitives_1.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 && (0, primitives_1.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 && (0, primitives_1.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 && (0, primitives_1.isString)(baseKey) ? baseKey.toLowerCase() : baseKey; results = sorted.filter((item) => { const key = getKey(item); const value = caseInsensitive && (0, primitives_1.isString)(key) ? key.toLowerCase() : key; return value === base; }); } } if (!results.length && fuzzy && (0, primitives_1.isString)(normalizedMatcher)) { results = source.filter((item) => { const rawKey = getKey(item); const key = caseInsensitive && (0, primitives_1.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 = (0, non_primitives_1.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 && (0, primitives_1.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 && (0, primitives_1.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 && (0, primitives_1.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 && (0, primitives_1.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 && (0, primitives_1.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; }; } } exports.Finder = Finder;