nhb-toolbox
Version:
A versatile collection of smart, efficient, and reusable utility functions and classes for everyday development needs.
290 lines (289 loc) • 11.5 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Finder = void 0;
/**
* The `Finder` class performs optimized searching on arrays.
* It supports binary search, fuzzy search, and smart caching with TTL.
*/
class Finder {
static #DEFAULT_TTL = 1000 * 60 * 5;
#cachedResult = new Map();
#sortedCache = new Map();
#ttl;
#items;
/**
* * Creates a new `Finder` instance.
*
* @param data The initial array of items or a callback returning them.
* @param ttl Time-to-live (in milliseconds) for cached search results. Defaults to {@link Finder.#DEFAULT_TTL 5 Minutes}.
*/
constructor(data, ttl = Finder.#DEFAULT_TTL) {
this.#ttl = ttl;
this.#items = typeof data === 'function' ? data() : data;
}
/**
* @instance Clears cache globally or for a specific key.
* @param key Optional key to clear only a specific cache entry.
*/
clearCache(key) {
if (key) {
this.#cachedResult.delete(key);
}
else {
this.#cachedResult.clear();
}
}
/**
* @instance Finds all items that match the provided matcher using optional caching or fuzzy logic.
* @param matcher The value to match against.
* @param keySelector Property key or selector function.
* @param options Optional settings for search behavior and source list.
*/
findAll(matcher, keySelector, options) {
const { fuzzy = false, needSorting = true, cacheKey = 'finder-cache', forceBinary = false, caseInsensitive = true, data, } = options ?? {};
const source = typeof data === 'function' ? 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 && typeof matcher === 'string' ?
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 && typeof key === 'string' ?
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 && typeof baseKey === 'string' ?
baseKey.toLowerCase()
: baseKey;
results = sorted.filter((item) => {
const key = getKey(item);
const value = caseInsensitive && typeof key === 'string' ?
key.toLowerCase()
: key;
return value === base;
});
}
}
if (!results.length && fuzzy && typeof normalizedMatcher === 'string') {
results = source.filter((item) => {
const rawKey = getKey(item);
const key = caseInsensitive && typeof rawKey === 'string' ?
rawKey.toLowerCase()
: String(rawKey);
return this.#match(key, normalizedMatcher);
});
}
if (cacheKey) {
this.#cachedResult.set(cacheKey, {
result: results,
timestamp: Date.now(),
});
}
return results;
}
/**
* @instance Finds first matching item that matches the provided matcher using optional caching or fuzzy logic.
* @param matcher The value to match.
* @param keySelector Property key or selector function.
* @param options Optional behavior flags and item source.
*/
findOne(matcher, keySelector, options) {
const { fuzzy = false, needSorting = true, cacheKey = 'finder-cache', forceBinary = false, caseInsensitive = true, data, } = options ?? {};
const source = typeof data === 'function' ? 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 && typeof matcher === 'string' ?
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 && typeof key === 'string' ?
key.toLowerCase()
: key;
return value === normalizedMatcher;
});
}
else {
result = this.binarySearch(needSorting ?
this.#sortAndCache(source, getKey, cacheKey)
: source, normalizedMatcher, getKey, caseInsensitive);
}
if (!result && fuzzy && typeof normalizedMatcher === 'string') {
return this.fuzzySearch(source, normalizedMatcher, getKey, caseInsensitive);
}
if (cacheKey && result) {
this.#cachedResult.set(cacheKey, {
result: [result],
timestamp: Date.now(),
});
}
return result;
}
/**
* @instance Asynchronous variant of `findAll` that accepts a promise-based data supplier.
* @param supplier Async function resolving the items list.
* @param matcher The value to match.
* @param keySelector Property key or selector function.
* @param options Optional settings for search behavior and cache.
*/
async findAllAsync(supplier, matcher, keySelector, options) {
const items = await supplier();
return this.findAll(matcher, keySelector, { ...options, data: items });
}
/**
* @instance Asynchronous variant of `findOne`.
* @param supplier Async function resolving the items list.
* @param matcher The value to match.
* @param keySelector Property key or selector function.
* @param options Optional settings for behavior and cache.
*/
async findOneAsync(supplier, matcher, keySelector, options) {
const items = await supplier();
return this.findOne(matcher, keySelector, { ...options, data: items });
}
/**
* @instance Performs a binary search on a sorted array using a custom key selector.
*
* @param sorted - The sorted array of items to search.
* @param matcher - The value to search for.
* @param keySelector - A function that extracts the comparable key from each item.
* @param caseInsensitive - Whether to compare string keys ignoring case.
* @returns The first matching item if found; otherwise, undefined.
*/
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 && typeof midKey === 'string' ?
midKey.toLowerCase()
: midKey;
if (key === matcher)
return sorted[mid];
if (key < matcher)
min = mid + 1;
else
max = mid - 1;
}
return undefined;
}
/**
* @instance Performs a fuzzy search on an array by matching characters in sequence.
*
* @param array - The array of items to search.
* @param matcher - The fuzzy search string to match against.
* @param keySelector - A function that extracts the key to search from each item.
* @param caseInsensitive - Whether to compare ignoring case for string values.
* @returns The first fuzzy-matching item if found; otherwise, undefined.
*/
fuzzySearch(array, matcher, keySelector, caseInsensitive) {
for (const item of array) {
const rawKey = keySelector(item);
const key = caseInsensitive && typeof rawKey === 'string' ?
rawKey.toLowerCase()
: String(rawKey);
if (this.#match(key, matcher))
return item;
}
return undefined;
}
/**
* @private Checks if the characters in the target string appear in order within the source string.
* @param source Source string to search within.
* @param target Target string to match against the source string.
* @returns True if the target string is a fuzzy match within the source string; otherwise, false.
*/
#match(source, target) {
let i = 0;
for (const char of target) {
i = source?.indexOf(char, i);
if (i === -1)
return false;
i++;
}
return true;
}
/**
* @private Sorts an array and caches the result for a specified time-to-live (TTL).
* @param data Data to sort and cache.
* @param getKey Key extraction function.
* @param cacheKey Optional cache key for storing the result.
* @returns
*/
#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 @private Creates a memoized version of a key extractor.
* @param getKey Original key extraction function
*/
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;