@oscarpalmer/atoms 0.178.0 → 0.179.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +108 -1
- package/dist/index.mjs +310 -150
- package/dist/internal/array/index-of.mjs +1 -1
- package/dist/string/fuzzy.d.mts +110 -0
- package/dist/string/fuzzy.mjs +165 -0
- package/package.json +6 -2
- package/src/index.ts +1 -0
- package/src/internal/array/index-of.ts +1 -1
- package/src/string/fuzzy.ts +399 -0
package/dist/index.d.mts
CHANGED
|
@@ -2669,6 +2669,113 @@ declare function titleCase(value: string): string;
|
|
|
2669
2669
|
*/
|
|
2670
2670
|
declare function upperCase(value: string): string;
|
|
2671
2671
|
//#endregion
|
|
2672
|
+
//#region src/string/fuzzy.d.ts
|
|
2673
|
+
/**
|
|
2674
|
+
* Fuzzy searcher for an array of items
|
|
2675
|
+
*/
|
|
2676
|
+
declare class Fuzzy<Item> {
|
|
2677
|
+
#private;
|
|
2678
|
+
/**
|
|
2679
|
+
* Get items currently being searched through
|
|
2680
|
+
*/
|
|
2681
|
+
get items(): Item[];
|
|
2682
|
+
/**
|
|
2683
|
+
* Set new items to search through
|
|
2684
|
+
*/
|
|
2685
|
+
set items(items: Item[]);
|
|
2686
|
+
/**
|
|
2687
|
+
* Get strings currently being searched through _(the stringified version of `items`)_
|
|
2688
|
+
*/
|
|
2689
|
+
get strings(): string[];
|
|
2690
|
+
constructor(state: FuzzyState<Item>);
|
|
2691
|
+
/**
|
|
2692
|
+
* Search for items matching the given value
|
|
2693
|
+
* @param value Value to search for
|
|
2694
|
+
* @param options Search options
|
|
2695
|
+
* @returns Search results, with exact matches _(ordered)_ and similar matches _(ordered by relevance)_
|
|
2696
|
+
*/
|
|
2697
|
+
search(value: string, options?: FuzzyOptions): FuzzyResult<Item>;
|
|
2698
|
+
/**
|
|
2699
|
+
* Search for items matching the given value
|
|
2700
|
+
* @param value Value to search for
|
|
2701
|
+
* @param limit Maximum number of combined items to return in `exact` and `similar`
|
|
2702
|
+
* @returns Search results, with exact matches _(ordered)_ and similar matches _(ordered by relevance)_
|
|
2703
|
+
*/
|
|
2704
|
+
search(value: string, limit: number): FuzzyResult<Item>;
|
|
2705
|
+
}
|
|
2706
|
+
type FuzzyConfiguration<Item> = {
|
|
2707
|
+
/**
|
|
2708
|
+
* Handler to stringify items
|
|
2709
|
+
*
|
|
2710
|
+
* May be a function that takes an item and returns a string, or if items are plain objects, a key of the item to use to grab a string value from
|
|
2711
|
+
*
|
|
2712
|
+
* Defaults to `getString`
|
|
2713
|
+
*/
|
|
2714
|
+
handler?: (item: Item) => string;
|
|
2715
|
+
} & (Item extends PlainObject ? {
|
|
2716
|
+
/**
|
|
2717
|
+
* Key to use to stringify items
|
|
2718
|
+
*
|
|
2719
|
+
* Prioritized over `handler`
|
|
2720
|
+
*/
|
|
2721
|
+
key?: keyof Item;
|
|
2722
|
+
} : {}) & FuzzyOptions;
|
|
2723
|
+
type FuzzyOptions = {
|
|
2724
|
+
/**
|
|
2725
|
+
* Maximum number of combined items to return in `exact` and `similar` _(defaults to all matches)_
|
|
2726
|
+
*/
|
|
2727
|
+
limit?: number;
|
|
2728
|
+
/**
|
|
2729
|
+
* Maximum score difference between the best and worst similar matches included in results; higher values cast a wider net _(defaults to 5)_
|
|
2730
|
+
*/
|
|
2731
|
+
tolerance?: number;
|
|
2732
|
+
};
|
|
2733
|
+
/**
|
|
2734
|
+
* Search results from a fuzzy search, with exact and similar matches
|
|
2735
|
+
*/
|
|
2736
|
+
type FuzzyResult<Item> = {
|
|
2737
|
+
/**
|
|
2738
|
+
* Ordered items that exactly match the search value
|
|
2739
|
+
*/
|
|
2740
|
+
exact: Item[];
|
|
2741
|
+
/**
|
|
2742
|
+
* Ordered items that are similar to the search value, ranked by relevance
|
|
2743
|
+
*/
|
|
2744
|
+
similar: Item[];
|
|
2745
|
+
};
|
|
2746
|
+
/**
|
|
2747
|
+
* Options for fuzzy searching
|
|
2748
|
+
*/
|
|
2749
|
+
type FuzzySearchOptions = FuzzyOptions;
|
|
2750
|
+
type FuzzyState<Item> = {
|
|
2751
|
+
handler: (item: Item) => string;
|
|
2752
|
+
items: Item[];
|
|
2753
|
+
limit?: number;
|
|
2754
|
+
strings: string[];
|
|
2755
|
+
tolerance: number;
|
|
2756
|
+
};
|
|
2757
|
+
/**
|
|
2758
|
+
* Create a fuzzy searcher for an array of items
|
|
2759
|
+
* @param items Items to search through
|
|
2760
|
+
* @param key Key to use to stringify items
|
|
2761
|
+
* @returns Fuzzy searcher
|
|
2762
|
+
*/
|
|
2763
|
+
declare function fuzzy<Item extends PlainObject, ItemKey extends keyof Item>(items: Item[], key?: ItemKey): Fuzzy<Item>;
|
|
2764
|
+
/**
|
|
2765
|
+
* Create a fuzzy searcher for an array of items
|
|
2766
|
+
* @param items Items to search through
|
|
2767
|
+
* @param handler Handler to stringify items
|
|
2768
|
+
* @returns Fuzzy searcher
|
|
2769
|
+
*/
|
|
2770
|
+
declare function fuzzy<Item>(items: Item[], handler?: (item: Item) => string): Fuzzy<Item>;
|
|
2771
|
+
/**
|
|
2772
|
+
* Create a fuzzy searcher for an array of items
|
|
2773
|
+
* @param items Items to search through
|
|
2774
|
+
* @param configuration Fuzzy configuration
|
|
2775
|
+
* @returns Fuzzy searcher
|
|
2776
|
+
*/
|
|
2777
|
+
declare function fuzzy<Item>(items: Item[], configuration?: FuzzyConfiguration<Item>): Fuzzy<Item>;
|
|
2778
|
+
//#endregion
|
|
2672
2779
|
//#region src/string/index.d.ts
|
|
2673
2780
|
declare function dedent(strings: TemplateStringsArray, ...values: unknown[]): string;
|
|
2674
2781
|
declare function dedent(value: string): string;
|
|
@@ -4826,4 +4933,4 @@ declare class SizedSet<Value = unknown> extends Set<Value> {
|
|
|
4826
4933
|
get(value: Value, update?: boolean): Value | undefined;
|
|
4827
4934
|
}
|
|
4828
4935
|
//#endregion
|
|
4829
|
-
export { AnyResult, ArrayComparisonSorter, ArrayKeySorter, ArrayOrPlainObject, ArrayPosition, ArrayValueSorter, Asserter, AsyncCancelableCallback, AttemptFlow, AttemptFlowPromise, type Beacon, type BeaconOptions, BuiltIns, CancelableCallback, CancelablePromise, type Color, Constructor, DiffOptions, DiffResult, DiffValue, EqualOptions, Err, EventPosition, ExtendedErr, ExtendedResult, Flow, FlowPromise, FulfilledPromise, GenericAsyncCallback, GenericCallback, type HSLAColor, type HSLColor, HasValue, Key, KeyedValue, type Logger, type Memoized, type MemoizedOptions, MergeOptions, Merger, NestedArray, NestedKeys, NestedPartial, NestedValue, NestedValues, NumericalKeys, NumericalValues, type Observable, type Observer, Ok, OnceAsyncCallback, OnceCallback, PROMISE_ABORT_EVENT, PROMISE_ABORT_OPTIONS, PROMISE_ERROR_NAME, PROMISE_MESSAGE_EXPECTATION_ATTEMPT, PROMISE_MESSAGE_EXPECTATION_RESULT, PROMISE_MESSAGE_EXPECTATION_TIMED, PROMISE_MESSAGE_TIMEOUT, PROMISE_STRATEGY_ALL, PROMISE_STRATEGY_DEFAULT, PROMISE_TYPE_FULFILLED, PROMISE_TYPE_REJECTED, PlainObject, Primitive, PromiseData, PromiseHandlers, PromiseOptions, PromiseParameters, PromiseStrategy, PromiseTimeoutError, PromisesItems, PromisesOptions, PromisesResult, PromisesUnwrapped, PromisesValue, PromisesValues, type Queue, QueueError, type QueueOptions, type Queued, type QueuedResult, type RGBAColor, type RGBColor, RejectedPromise, RequiredKeys, Result, ResultMatch, RetryError, RetryOptions, SORT_DIRECTION_ASCENDING, SORT_DIRECTION_DESCENDING, Simplify, SizedMap, SizedSet, Smushed, SortDirection, Sorter, type Subscription, TemplateOptions, type Time, ToString, TypedArray, Unsmushed, UnwrapValue, assert, attempt, attemptFlow, attemptPipe, attemptPromise, average, beacon, between, camelCase, cancelable, capitalize, ceil, chunk, clamp, clone, compact, compare, count, debounce, dedent, delay, diff, difference, drop, endsWith, endsWithArray, equal, error, exists, filter, find, first, flatten, floor, flow, fromQuery, toPromise as fromResult, toPromise, getArray, getArrayPosition, getColor, getError, getForegroundColor, getHexColor, getHexaColor, getHslColor, getHslaColor, getNormalizedHex, getNumber, getRandomBoolean, getRandomCharacters, getRandomColor, getRandomFloat, getRandomHex, getRandomInteger, getRandomItem, getRandomItems, getRgbColor, getRgbaColor, getString, getTimedPromise, getUuid, getValue, groupBy, handleResult, hasValue, hexToHsl, hexToHsla, hexToRgb, hexToRgba, hslToHex, hslToRgb, hslToRgba, ignoreKey, inMap, inSet, includes, includesArray, indexOf, indexOfArray, insert, intersection, isArrayOrPlainObject, isColor, isConstructor, isEmpty, isError, isFulfilled, isHexColor, isHslColor, isHslLike, isHslaColor, isInstanceOf, isKey, isNonArrayOrPlainObject, isNonConstructor, isNonEmpty, isNonInstanceOf, isNonKey, isNonNullable, isNonNullableOrEmpty, isNonNullableOrWhitespace, isNonNumber, isNonNumerical, isNonObject, isNonPlainObject, isNonPrimitive, isNonTypedArray, isNullable, isNullableOrEmpty, isNullableOrWhitespace, isNumber, isNumerical, isObject, isOk, isPlainObject, isPrimitive, isRejected, isResult, isRgbColor, isRgbLike, isRgbaColor, isTypedArray, join, kebabCase, last, logger, lowerCase, matchResult, max, median, memoize, merge, min, move, noop, ok, omit, once, parse, partition, pascalCase, pick, pipe, promises, push, queue, range, retry, reverse, rgbToHex, rgbToHsl, rgbToHsla, round, select, setValue, settlePromise, shuffle, single, slice, smush, snakeCase, sort, splice, startsWith, startsWithArray, sum, swap, take, template, throttle, timed, times, titleCase, toMap, toQuery, toRecord, toResult, toSet, toggle, trim, truncate, tryDecode, tryEncode, union, unique, unsmush, unwrap, update, upperCase, words };
|
|
4936
|
+
export { AnyResult, ArrayComparisonSorter, ArrayKeySorter, ArrayOrPlainObject, ArrayPosition, ArrayValueSorter, Asserter, AsyncCancelableCallback, AttemptFlow, AttemptFlowPromise, type Beacon, type BeaconOptions, BuiltIns, CancelableCallback, CancelablePromise, type Color, Constructor, DiffOptions, DiffResult, DiffValue, EqualOptions, Err, EventPosition, ExtendedErr, ExtendedResult, Flow, FlowPromise, FulfilledPromise, FuzzyConfiguration, FuzzyOptions, FuzzyResult, FuzzySearchOptions, GenericAsyncCallback, GenericCallback, type HSLAColor, type HSLColor, HasValue, Key, KeyedValue, type Logger, type Memoized, type MemoizedOptions, MergeOptions, Merger, NestedArray, NestedKeys, NestedPartial, NestedValue, NestedValues, NumericalKeys, NumericalValues, type Observable, type Observer, Ok, OnceAsyncCallback, OnceCallback, PROMISE_ABORT_EVENT, PROMISE_ABORT_OPTIONS, PROMISE_ERROR_NAME, PROMISE_MESSAGE_EXPECTATION_ATTEMPT, PROMISE_MESSAGE_EXPECTATION_RESULT, PROMISE_MESSAGE_EXPECTATION_TIMED, PROMISE_MESSAGE_TIMEOUT, PROMISE_STRATEGY_ALL, PROMISE_STRATEGY_DEFAULT, PROMISE_TYPE_FULFILLED, PROMISE_TYPE_REJECTED, PlainObject, Primitive, PromiseData, PromiseHandlers, PromiseOptions, PromiseParameters, PromiseStrategy, PromiseTimeoutError, PromisesItems, PromisesOptions, PromisesResult, PromisesUnwrapped, PromisesValue, PromisesValues, type Queue, QueueError, type QueueOptions, type Queued, type QueuedResult, type RGBAColor, type RGBColor, RejectedPromise, RequiredKeys, Result, ResultMatch, RetryError, RetryOptions, SORT_DIRECTION_ASCENDING, SORT_DIRECTION_DESCENDING, Simplify, SizedMap, SizedSet, Smushed, SortDirection, Sorter, type Subscription, TemplateOptions, type Time, ToString, TypedArray, Unsmushed, UnwrapValue, assert, attempt, attemptFlow, attemptPipe, attemptPromise, average, beacon, between, camelCase, cancelable, capitalize, ceil, chunk, clamp, clone, compact, compare, count, debounce, dedent, delay, diff, difference, drop, endsWith, endsWithArray, equal, error, exists, filter, find, first, flatten, floor, flow, fromQuery, toPromise as fromResult, toPromise, fuzzy, getArray, getArrayPosition, getColor, getError, getForegroundColor, getHexColor, getHexaColor, getHslColor, getHslaColor, getNormalizedHex, getNumber, getRandomBoolean, getRandomCharacters, getRandomColor, getRandomFloat, getRandomHex, getRandomInteger, getRandomItem, getRandomItems, getRgbColor, getRgbaColor, getString, getTimedPromise, getUuid, getValue, groupBy, handleResult, hasValue, hexToHsl, hexToHsla, hexToRgb, hexToRgba, hslToHex, hslToRgb, hslToRgba, ignoreKey, inMap, inSet, includes, includesArray, indexOf, indexOfArray, insert, intersection, isArrayOrPlainObject, isColor, isConstructor, isEmpty, isError, isFulfilled, isHexColor, isHslColor, isHslLike, isHslaColor, isInstanceOf, isKey, isNonArrayOrPlainObject, isNonConstructor, isNonEmpty, isNonInstanceOf, isNonKey, isNonNullable, isNonNullableOrEmpty, isNonNullableOrWhitespace, isNonNumber, isNonNumerical, isNonObject, isNonPlainObject, isNonPrimitive, isNonTypedArray, isNullable, isNullableOrEmpty, isNullableOrWhitespace, isNumber, isNumerical, isObject, isOk, isPlainObject, isPrimitive, isRejected, isResult, isRgbColor, isRgbLike, isRgbaColor, isTypedArray, join, kebabCase, last, logger, lowerCase, matchResult, max, median, memoize, merge, min, move, noop, ok, omit, once, parse, partition, pascalCase, pick, pipe, promises, push, queue, range, retry, reverse, rgbToHex, rgbToHsl, rgbToHsla, round, select, setValue, settlePromise, shuffle, single, slice, smush, snakeCase, sort, splice, startsWith, startsWithArray, sum, swap, take, template, throttle, timed, times, titleCase, toMap, toQuery, toRecord, toResult, toSet, toggle, trim, truncate, tryDecode, tryEncode, union, unique, unsmush, unwrap, update, upperCase, words };
|
package/dist/index.mjs
CHANGED
|
@@ -175,7 +175,7 @@ function compact(array, strict) {
|
|
|
175
175
|
//#endregion
|
|
176
176
|
//#region src/internal/array/index-of.ts
|
|
177
177
|
function indexOf(array, ...parameters) {
|
|
178
|
-
return findValue(FIND_VALUE_INDEX, array, parameters);
|
|
178
|
+
return findValue(FIND_VALUE_INDEX, array, parameters, false);
|
|
179
179
|
}
|
|
180
180
|
//#endregion
|
|
181
181
|
//#region src/internal/is.ts
|
|
@@ -2316,6 +2316,314 @@ const delimiters = {
|
|
|
2316
2316
|
let memoizedCapitalize;
|
|
2317
2317
|
let memoizedTitleCase;
|
|
2318
2318
|
//#endregion
|
|
2319
|
+
//#region src/is.ts
|
|
2320
|
+
/**
|
|
2321
|
+
* Is the value empty, or only containing `null` or `undefined` values?
|
|
2322
|
+
* @param value Value to check
|
|
2323
|
+
* @returns `true` if the value is considered empty, otherwise `false`
|
|
2324
|
+
*/
|
|
2325
|
+
function isEmpty(value) {
|
|
2326
|
+
if (value == null) return true;
|
|
2327
|
+
if (typeof value === "string") return value.length === 0;
|
|
2328
|
+
const values = getArray(value);
|
|
2329
|
+
const { length } = values;
|
|
2330
|
+
for (let index = 0; index < length; index += 1) if (values[index] != null) return false;
|
|
2331
|
+
return true;
|
|
2332
|
+
}
|
|
2333
|
+
/**
|
|
2334
|
+
* Is the value not empty, or holding non-empty values?
|
|
2335
|
+
* @param value Value to check
|
|
2336
|
+
* @returns `true` if the value is not considered empty, otherwise `false`
|
|
2337
|
+
*/
|
|
2338
|
+
function isNonEmpty(value) {
|
|
2339
|
+
return !isEmpty(value);
|
|
2340
|
+
}
|
|
2341
|
+
/**
|
|
2342
|
+
* Is the value not `undefined` or `null`?
|
|
2343
|
+
* @param value Value to check
|
|
2344
|
+
* @returns `true` if the value is not `undefined` or `null`, otherwise `false`
|
|
2345
|
+
*/
|
|
2346
|
+
function isNonNullable(value) {
|
|
2347
|
+
return value != null;
|
|
2348
|
+
}
|
|
2349
|
+
/**
|
|
2350
|
+
* Is the value not `undefined`, `null`, or stringified as an empty _(no whitespace)_ string?
|
|
2351
|
+
* @param value Value to check
|
|
2352
|
+
* @returns `true` if the value is not `undefined`, `null`, or matches an empty string, otherwise `false`
|
|
2353
|
+
*/
|
|
2354
|
+
function isNonNullableOrEmpty(value) {
|
|
2355
|
+
return value != null && getString(value) !== "";
|
|
2356
|
+
}
|
|
2357
|
+
/**
|
|
2358
|
+
* Is the value not `undefined`, `null`, or stringified as a whitespace-only string?
|
|
2359
|
+
* @param value Value to check
|
|
2360
|
+
* @returns `true` if the value is not `undefined`, `null`, or matches a whitespace-only string, otherwise `false`
|
|
2361
|
+
*/
|
|
2362
|
+
function isNonNullableOrWhitespace(value) {
|
|
2363
|
+
return value != null && !EXPRESSION_WHITESPACE.test(getString(value));
|
|
2364
|
+
}
|
|
2365
|
+
/**
|
|
2366
|
+
* Is the value not a number or a number-like string?
|
|
2367
|
+
* @param value Value to check
|
|
2368
|
+
* @returns `true` if the value is not a number or a number-like string, otherwise `false`
|
|
2369
|
+
*/
|
|
2370
|
+
function isNonNumerical(value) {
|
|
2371
|
+
return !isNumerical(value);
|
|
2372
|
+
}
|
|
2373
|
+
/**
|
|
2374
|
+
* Is the value not an object _(or function)_?
|
|
2375
|
+
* @param value Value to check
|
|
2376
|
+
* @returns `true` if the value is not an object, otherwise `false`
|
|
2377
|
+
*/
|
|
2378
|
+
function isNonObject(value) {
|
|
2379
|
+
return !isObject(value);
|
|
2380
|
+
}
|
|
2381
|
+
/**
|
|
2382
|
+
* Is the value `undefined` or `null`?
|
|
2383
|
+
* @param value Value to check
|
|
2384
|
+
* @returns `true` if the value is `undefined` or `null`, otherwise `false`
|
|
2385
|
+
*/
|
|
2386
|
+
function isNullable(value) {
|
|
2387
|
+
return value == null;
|
|
2388
|
+
}
|
|
2389
|
+
/**
|
|
2390
|
+
* Is the value `undefined`, `null`, or stringified as an empty _(no whitespace)_ string?
|
|
2391
|
+
* @param value Value to check
|
|
2392
|
+
* @returns `true` if the value is nullable or matches an empty string, otherwise `false`
|
|
2393
|
+
*/
|
|
2394
|
+
function isNullableOrEmpty(value) {
|
|
2395
|
+
return value == null || getString(value) === "";
|
|
2396
|
+
}
|
|
2397
|
+
/**
|
|
2398
|
+
* Is the value `undefined`, `null`, or stringified as a whitespace-only string?
|
|
2399
|
+
* @param value Value to check
|
|
2400
|
+
* @returns `true` if the value is nullable or matches a whitespace-only string, otherwise `false`
|
|
2401
|
+
*/
|
|
2402
|
+
function isNullableOrWhitespace(value) {
|
|
2403
|
+
return value == null || EXPRESSION_WHITESPACE.test(getString(value));
|
|
2404
|
+
}
|
|
2405
|
+
/**
|
|
2406
|
+
* Is the value a number or a number-like string?
|
|
2407
|
+
* @param value Value to check
|
|
2408
|
+
* @returns `true` if the value is a number or a number-like string, otherwise `false`
|
|
2409
|
+
*/
|
|
2410
|
+
function isNumerical(value) {
|
|
2411
|
+
return isNumber(value) || typeof value === "string" && value.trim().length > 0 && !Number.isNaN(+value);
|
|
2412
|
+
}
|
|
2413
|
+
/**
|
|
2414
|
+
* Is the value an object _(or function)_?
|
|
2415
|
+
* @param value Value to check
|
|
2416
|
+
* @returns `true` if the value matches, otherwise `false`
|
|
2417
|
+
*/
|
|
2418
|
+
function isObject(value) {
|
|
2419
|
+
return typeof value === "object" && value !== null || typeof value === "function";
|
|
2420
|
+
}
|
|
2421
|
+
const EXPRESSION_WHITESPACE = /^\s*$/;
|
|
2422
|
+
//#endregion
|
|
2423
|
+
//#region src/string/match.ts
|
|
2424
|
+
/**
|
|
2425
|
+
* Check if a string ends with a specified substring
|
|
2426
|
+
* @param haystack String to look in
|
|
2427
|
+
* @param needle String to look for
|
|
2428
|
+
* @param ignoreCase Ignore case when matching? _(defaults to `false`)_
|
|
2429
|
+
* @returns `true` if the string ends with the given substring, otherwise `false`
|
|
2430
|
+
*/
|
|
2431
|
+
function endsWith(haystack, needle, ignoreCase) {
|
|
2432
|
+
return match(MATCH_ENDS_WITH, haystack, needle, ignoreCase === true);
|
|
2433
|
+
}
|
|
2434
|
+
/**
|
|
2435
|
+
* Check if a string includes a specified substring
|
|
2436
|
+
* @param haystack String to look in
|
|
2437
|
+
* @param needle String to look for
|
|
2438
|
+
* @param ignoreCase Ignore case when matching? _(defaults to `false`)_
|
|
2439
|
+
* @returns `true` if the string includes the given substring, otherwise `false`
|
|
2440
|
+
*/
|
|
2441
|
+
function includes(haystack, needle, ignoreCase) {
|
|
2442
|
+
return match(MATCH_INCLUDES, haystack, needle, ignoreCase === true);
|
|
2443
|
+
}
|
|
2444
|
+
function match(type, haystack, needle, ignoreCase) {
|
|
2445
|
+
if (typeof haystack !== "string" || typeof needle !== "string") return false;
|
|
2446
|
+
matchMemoizers[type] ??= memoize(matchCallback.bind(type));
|
|
2447
|
+
return matchMemoizers[type].run(haystack, needle, ignoreCase);
|
|
2448
|
+
}
|
|
2449
|
+
function matchCallback(haystack, needle, ignoreCase) {
|
|
2450
|
+
return (ignoreCase ? haystack.toLocaleLowerCase() : haystack)[this](ignoreCase ? needle.toLocaleLowerCase() : needle);
|
|
2451
|
+
}
|
|
2452
|
+
/**
|
|
2453
|
+
* Check if a string starts with a specified substring
|
|
2454
|
+
* @param haystack String to look in
|
|
2455
|
+
* @param needle String to look for
|
|
2456
|
+
* @param ignoreCase Ignore case when matching? _(defaults to `false`)_
|
|
2457
|
+
* @returns `true` if the string starts with the given substring, otherwise `false`
|
|
2458
|
+
*/
|
|
2459
|
+
function startsWith(haystack, needle, ignoreCase) {
|
|
2460
|
+
return match(MATCH_STARTS_WITH, haystack, needle, ignoreCase === true);
|
|
2461
|
+
}
|
|
2462
|
+
const MATCH_ENDS_WITH = "endsWith";
|
|
2463
|
+
const MATCH_INCLUDES = "includes";
|
|
2464
|
+
const MATCH_STARTS_WITH = "startsWith";
|
|
2465
|
+
const matchMemoizers = {};
|
|
2466
|
+
//#endregion
|
|
2467
|
+
//#region src/string/fuzzy.ts
|
|
2468
|
+
/**
|
|
2469
|
+
* Fuzzy searcher for an array of items
|
|
2470
|
+
*/
|
|
2471
|
+
var Fuzzy = class {
|
|
2472
|
+
#state;
|
|
2473
|
+
/**
|
|
2474
|
+
* Get items currently being searched through
|
|
2475
|
+
*/
|
|
2476
|
+
get items() {
|
|
2477
|
+
return this.#state.items.slice();
|
|
2478
|
+
}
|
|
2479
|
+
/**
|
|
2480
|
+
* Set new items to search through
|
|
2481
|
+
*/
|
|
2482
|
+
set items(items) {
|
|
2483
|
+
if (Array.isArray(items)) {
|
|
2484
|
+
this.#state.items = items.slice();
|
|
2485
|
+
this.#state.strings = items.map(this.#state.handler);
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
/**
|
|
2489
|
+
* Get strings currently being searched through _(the stringified version of `items`)_
|
|
2490
|
+
*/
|
|
2491
|
+
get strings() {
|
|
2492
|
+
return this.#state.strings.slice();
|
|
2493
|
+
}
|
|
2494
|
+
constructor(state) {
|
|
2495
|
+
this.#state = state;
|
|
2496
|
+
}
|
|
2497
|
+
search(value, options) {
|
|
2498
|
+
return search(this.#state.items, this.#state.strings, value, options == null ? this.#state : getOptions$1(options, this.#state));
|
|
2499
|
+
}
|
|
2500
|
+
};
|
|
2501
|
+
function getHandler(input) {
|
|
2502
|
+
if (input == null || input === getString) return getString;
|
|
2503
|
+
switch (typeof input) {
|
|
2504
|
+
case "function": return input;
|
|
2505
|
+
case "string": return (item) => item[input];
|
|
2506
|
+
default:
|
|
2507
|
+
if (isPlainObject(input)) return getHandler(input.key ?? input.handler);
|
|
2508
|
+
throw new TypeError(MESSAGE_HANDLER);
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
function getItems(items) {
|
|
2512
|
+
return items.sort((first, second) => first.haystack.localeCompare(second.haystack)).map(({ item }) => item);
|
|
2513
|
+
}
|
|
2514
|
+
function getOptions$1(input, state) {
|
|
2515
|
+
const options = isPlainObject(input) ? input : {};
|
|
2516
|
+
const limit = typeof input === "number" ? input : options.limit;
|
|
2517
|
+
if (typeof limit === "number" && !Number.isNaN(limit) && limit >= 1) options.limit = Math.floor(limit);
|
|
2518
|
+
else options.limit = state?.limit;
|
|
2519
|
+
if (typeof options.tolerance === "number" && !Number.isNaN(options.tolerance)) options.tolerance = options.tolerance < 0 ? 0 : Math.floor(options.tolerance);
|
|
2520
|
+
else options.tolerance = state?.tolerance ?? PROXIMITY_THRESHOLD;
|
|
2521
|
+
return options;
|
|
2522
|
+
}
|
|
2523
|
+
function getState$1(items, input) {
|
|
2524
|
+
const handler = getHandler(input);
|
|
2525
|
+
const options = getOptions$1(input);
|
|
2526
|
+
return {
|
|
2527
|
+
handler,
|
|
2528
|
+
items: items.slice(),
|
|
2529
|
+
limit: options.limit,
|
|
2530
|
+
strings: items.map(handler),
|
|
2531
|
+
tolerance: options.tolerance
|
|
2532
|
+
};
|
|
2533
|
+
}
|
|
2534
|
+
function fuzzy(items, configuration) {
|
|
2535
|
+
if (!Array.isArray(items)) throw new TypeError(MESSAGE_ARRAY);
|
|
2536
|
+
return new Fuzzy(getState$1(items, configuration));
|
|
2537
|
+
}
|
|
2538
|
+
function isSubsequence(haystack, needle) {
|
|
2539
|
+
const lowerCaseHaystack = lowerCase(haystack);
|
|
2540
|
+
const lowerCaseNeedle = lowerCase(needle);
|
|
2541
|
+
const haystackLength = lowerCaseHaystack.length;
|
|
2542
|
+
const needleLength = lowerCaseNeedle.length;
|
|
2543
|
+
let needleIndex = 0;
|
|
2544
|
+
for (let haystackIndex = 0; haystackIndex < haystackLength; haystackIndex += 1) {
|
|
2545
|
+
if (lowerCaseHaystack[haystackIndex] === lowerCaseNeedle[needleIndex]) needleIndex += 1;
|
|
2546
|
+
if (needleIndex === needleLength) return true;
|
|
2547
|
+
}
|
|
2548
|
+
return false;
|
|
2549
|
+
}
|
|
2550
|
+
function getScore(haystack, needle) {
|
|
2551
|
+
if (!isSubsequence(haystack, needle)) return -1;
|
|
2552
|
+
const lowerCaseHaystack = lowerCase(haystack);
|
|
2553
|
+
const lowerCaseNeedle = lowerCase(needle);
|
|
2554
|
+
const needleLength = lowerCaseNeedle.length;
|
|
2555
|
+
let needleIndex = 0;
|
|
2556
|
+
let previousMatchIndex = -1;
|
|
2557
|
+
let score = 0;
|
|
2558
|
+
for (let haystackIndex = 0; haystackIndex < lowerCaseHaystack.length; haystackIndex += 1) {
|
|
2559
|
+
if (lowerCaseHaystack[haystackIndex] === lowerCaseNeedle[needleIndex]) {
|
|
2560
|
+
score += 1;
|
|
2561
|
+
if (haystackIndex === 0) score += 1;
|
|
2562
|
+
if (previousMatchIndex !== -1) {
|
|
2563
|
+
const gap = haystackIndex - previousMatchIndex - 1;
|
|
2564
|
+
score += Math.max(0, PROXIMITY_THRESHOLD - gap);
|
|
2565
|
+
}
|
|
2566
|
+
previousMatchIndex = haystackIndex;
|
|
2567
|
+
needleIndex += 1;
|
|
2568
|
+
}
|
|
2569
|
+
if (needleIndex === needleLength) break;
|
|
2570
|
+
}
|
|
2571
|
+
score -= Math.floor(lowerCaseHaystack.length / LENGTH_DIVISOR);
|
|
2572
|
+
return Math.max(0, score);
|
|
2573
|
+
}
|
|
2574
|
+
function search(items, strings, input, options) {
|
|
2575
|
+
const result = {
|
|
2576
|
+
exact: [],
|
|
2577
|
+
similar: []
|
|
2578
|
+
};
|
|
2579
|
+
const value = typeof input === "string" ? input.trim() : "";
|
|
2580
|
+
if (value.length === 0) {
|
|
2581
|
+
result.exact = items.slice(0, options.limit);
|
|
2582
|
+
return result;
|
|
2583
|
+
}
|
|
2584
|
+
let { length } = items;
|
|
2585
|
+
const exact = [];
|
|
2586
|
+
const similar = [];
|
|
2587
|
+
const scored = {};
|
|
2588
|
+
for (let index = 0; index < length; index += 1) {
|
|
2589
|
+
const item = items[index];
|
|
2590
|
+
const haystack = strings[index];
|
|
2591
|
+
if (includes(haystack, value, true)) {
|
|
2592
|
+
exact.push({
|
|
2593
|
+
item,
|
|
2594
|
+
haystack
|
|
2595
|
+
});
|
|
2596
|
+
continue;
|
|
2597
|
+
}
|
|
2598
|
+
const score = getScore(haystack, value);
|
|
2599
|
+
if (score > -1) {
|
|
2600
|
+
scored[score] ??= [];
|
|
2601
|
+
scored[score].push({
|
|
2602
|
+
item,
|
|
2603
|
+
haystack
|
|
2604
|
+
});
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
const keys = Object.keys(scored).map(Number).sort((first, second) => second - first);
|
|
2608
|
+
length = keys.length;
|
|
2609
|
+
if (length > 0 && options.tolerance > 0) {
|
|
2610
|
+
const maxScore = keys[0];
|
|
2611
|
+
for (let index = 0; index < length; index += 1) {
|
|
2612
|
+
const key = keys[index];
|
|
2613
|
+
if (maxScore - key > options.tolerance) break;
|
|
2614
|
+
similar.push(...getItems(scored[key]));
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
result.exact = getItems(options.limit == null ? exact : exact.slice(0, options.limit));
|
|
2618
|
+
if (options.limit == null) result.similar = similar;
|
|
2619
|
+
else result.similar = similar.slice(0, options.limit - result.exact.length);
|
|
2620
|
+
return result;
|
|
2621
|
+
}
|
|
2622
|
+
const LENGTH_DIVISOR = 3;
|
|
2623
|
+
const MESSAGE_ARRAY = "Fuzzy requires an array of items";
|
|
2624
|
+
const MESSAGE_HANDLER = "Fuzzy requires a key or function to stringify items";
|
|
2625
|
+
const PROXIMITY_THRESHOLD = 5;
|
|
2626
|
+
//#endregion
|
|
2319
2627
|
//#region src/string/index.ts
|
|
2320
2628
|
function dedent(value, ...values) {
|
|
2321
2629
|
let actual;
|
|
@@ -2404,50 +2712,6 @@ function truncate(value, length, suffix) {
|
|
|
2404
2712
|
}
|
|
2405
2713
|
const ZERO = "0";
|
|
2406
2714
|
//#endregion
|
|
2407
|
-
//#region src/string/match.ts
|
|
2408
|
-
/**
|
|
2409
|
-
* Check if a string ends with a specified substring
|
|
2410
|
-
* @param haystack String to look in
|
|
2411
|
-
* @param needle String to look for
|
|
2412
|
-
* @param ignoreCase Ignore case when matching? _(defaults to `false`)_
|
|
2413
|
-
* @returns `true` if the string ends with the given substring, otherwise `false`
|
|
2414
|
-
*/
|
|
2415
|
-
function endsWith(haystack, needle, ignoreCase) {
|
|
2416
|
-
return match(MATCH_ENDS_WITH, haystack, needle, ignoreCase === true);
|
|
2417
|
-
}
|
|
2418
|
-
/**
|
|
2419
|
-
* Check if a string includes a specified substring
|
|
2420
|
-
* @param haystack String to look in
|
|
2421
|
-
* @param needle String to look for
|
|
2422
|
-
* @param ignoreCase Ignore case when matching? _(defaults to `false`)_
|
|
2423
|
-
* @returns `true` if the string includes the given substring, otherwise `false`
|
|
2424
|
-
*/
|
|
2425
|
-
function includes(haystack, needle, ignoreCase) {
|
|
2426
|
-
return match(MATCH_INCLUDES, haystack, needle, ignoreCase === true);
|
|
2427
|
-
}
|
|
2428
|
-
function match(type, haystack, needle, ignoreCase) {
|
|
2429
|
-
if (typeof haystack !== "string" || typeof needle !== "string") return false;
|
|
2430
|
-
matchMemoizers[type] ??= memoize(matchCallback.bind(type));
|
|
2431
|
-
return matchMemoizers[type].run(haystack, needle, ignoreCase);
|
|
2432
|
-
}
|
|
2433
|
-
function matchCallback(haystack, needle, ignoreCase) {
|
|
2434
|
-
return (ignoreCase ? haystack.toLocaleLowerCase() : haystack)[this](ignoreCase ? needle.toLocaleLowerCase() : needle);
|
|
2435
|
-
}
|
|
2436
|
-
/**
|
|
2437
|
-
* Check if a string starts with a specified substring
|
|
2438
|
-
* @param haystack String to look in
|
|
2439
|
-
* @param needle String to look for
|
|
2440
|
-
* @param ignoreCase Ignore case when matching? _(defaults to `false`)_
|
|
2441
|
-
* @returns `true` if the string starts with the given substring, otherwise `false`
|
|
2442
|
-
*/
|
|
2443
|
-
function startsWith(haystack, needle, ignoreCase) {
|
|
2444
|
-
return match(MATCH_STARTS_WITH, haystack, needle, ignoreCase === true);
|
|
2445
|
-
}
|
|
2446
|
-
const MATCH_ENDS_WITH = "endsWith";
|
|
2447
|
-
const MATCH_INCLUDES = "includes";
|
|
2448
|
-
const MATCH_STARTS_WITH = "startsWith";
|
|
2449
|
-
const matchMemoizers = {};
|
|
2450
|
-
//#endregion
|
|
2451
2715
|
//#region src/string/template.ts
|
|
2452
2716
|
function getTemplateOptions(input) {
|
|
2453
2717
|
const options = isPlainObject(input) ? input : {};
|
|
@@ -3737,110 +4001,6 @@ function getColor(value) {
|
|
|
3737
4001
|
return isColor(value) ? value : new Color(value);
|
|
3738
4002
|
}
|
|
3739
4003
|
//#endregion
|
|
3740
|
-
//#region src/is.ts
|
|
3741
|
-
/**
|
|
3742
|
-
* Is the value empty, or only containing `null` or `undefined` values?
|
|
3743
|
-
* @param value Value to check
|
|
3744
|
-
* @returns `true` if the value is considered empty, otherwise `false`
|
|
3745
|
-
*/
|
|
3746
|
-
function isEmpty(value) {
|
|
3747
|
-
if (value == null) return true;
|
|
3748
|
-
if (typeof value === "string") return value.length === 0;
|
|
3749
|
-
const values = getArray(value);
|
|
3750
|
-
const { length } = values;
|
|
3751
|
-
for (let index = 0; index < length; index += 1) if (values[index] != null) return false;
|
|
3752
|
-
return true;
|
|
3753
|
-
}
|
|
3754
|
-
/**
|
|
3755
|
-
* Is the value not empty, or holding non-empty values?
|
|
3756
|
-
* @param value Value to check
|
|
3757
|
-
* @returns `true` if the value is not considered empty, otherwise `false`
|
|
3758
|
-
*/
|
|
3759
|
-
function isNonEmpty(value) {
|
|
3760
|
-
return !isEmpty(value);
|
|
3761
|
-
}
|
|
3762
|
-
/**
|
|
3763
|
-
* Is the value not `undefined` or `null`?
|
|
3764
|
-
* @param value Value to check
|
|
3765
|
-
* @returns `true` if the value is not `undefined` or `null`, otherwise `false`
|
|
3766
|
-
*/
|
|
3767
|
-
function isNonNullable(value) {
|
|
3768
|
-
return value != null;
|
|
3769
|
-
}
|
|
3770
|
-
/**
|
|
3771
|
-
* Is the value not `undefined`, `null`, or stringified as an empty _(no whitespace)_ string?
|
|
3772
|
-
* @param value Value to check
|
|
3773
|
-
* @returns `true` if the value is not `undefined`, `null`, or matches an empty string, otherwise `false`
|
|
3774
|
-
*/
|
|
3775
|
-
function isNonNullableOrEmpty(value) {
|
|
3776
|
-
return value != null && getString(value) !== "";
|
|
3777
|
-
}
|
|
3778
|
-
/**
|
|
3779
|
-
* Is the value not `undefined`, `null`, or stringified as a whitespace-only string?
|
|
3780
|
-
* @param value Value to check
|
|
3781
|
-
* @returns `true` if the value is not `undefined`, `null`, or matches a whitespace-only string, otherwise `false`
|
|
3782
|
-
*/
|
|
3783
|
-
function isNonNullableOrWhitespace(value) {
|
|
3784
|
-
return value != null && !EXPRESSION_WHITESPACE.test(getString(value));
|
|
3785
|
-
}
|
|
3786
|
-
/**
|
|
3787
|
-
* Is the value not a number or a number-like string?
|
|
3788
|
-
* @param value Value to check
|
|
3789
|
-
* @returns `true` if the value is not a number or a number-like string, otherwise `false`
|
|
3790
|
-
*/
|
|
3791
|
-
function isNonNumerical(value) {
|
|
3792
|
-
return !isNumerical(value);
|
|
3793
|
-
}
|
|
3794
|
-
/**
|
|
3795
|
-
* Is the value not an object _(or function)_?
|
|
3796
|
-
* @param value Value to check
|
|
3797
|
-
* @returns `true` if the value is not an object, otherwise `false`
|
|
3798
|
-
*/
|
|
3799
|
-
function isNonObject(value) {
|
|
3800
|
-
return !isObject(value);
|
|
3801
|
-
}
|
|
3802
|
-
/**
|
|
3803
|
-
* Is the value `undefined` or `null`?
|
|
3804
|
-
* @param value Value to check
|
|
3805
|
-
* @returns `true` if the value is `undefined` or `null`, otherwise `false`
|
|
3806
|
-
*/
|
|
3807
|
-
function isNullable(value) {
|
|
3808
|
-
return value == null;
|
|
3809
|
-
}
|
|
3810
|
-
/**
|
|
3811
|
-
* Is the value `undefined`, `null`, or stringified as an empty _(no whitespace)_ string?
|
|
3812
|
-
* @param value Value to check
|
|
3813
|
-
* @returns `true` if the value is nullable or matches an empty string, otherwise `false`
|
|
3814
|
-
*/
|
|
3815
|
-
function isNullableOrEmpty(value) {
|
|
3816
|
-
return value == null || getString(value) === "";
|
|
3817
|
-
}
|
|
3818
|
-
/**
|
|
3819
|
-
* Is the value `undefined`, `null`, or stringified as a whitespace-only string?
|
|
3820
|
-
* @param value Value to check
|
|
3821
|
-
* @returns `true` if the value is nullable or matches a whitespace-only string, otherwise `false`
|
|
3822
|
-
*/
|
|
3823
|
-
function isNullableOrWhitespace(value) {
|
|
3824
|
-
return value == null || EXPRESSION_WHITESPACE.test(getString(value));
|
|
3825
|
-
}
|
|
3826
|
-
/**
|
|
3827
|
-
* Is the value a number or a number-like string?
|
|
3828
|
-
* @param value Value to check
|
|
3829
|
-
* @returns `true` if the value is a number or a number-like string, otherwise `false`
|
|
3830
|
-
*/
|
|
3831
|
-
function isNumerical(value) {
|
|
3832
|
-
return isNumber(value) || typeof value === "string" && value.trim().length > 0 && !Number.isNaN(+value);
|
|
3833
|
-
}
|
|
3834
|
-
/**
|
|
3835
|
-
* Is the value an object _(or function)_?
|
|
3836
|
-
* @param value Value to check
|
|
3837
|
-
* @returns `true` if the value matches, otherwise `false`
|
|
3838
|
-
*/
|
|
3839
|
-
function isObject(value) {
|
|
3840
|
-
return typeof value === "object" && value !== null || typeof value === "function";
|
|
3841
|
-
}
|
|
3842
|
-
const EXPRESSION_WHITESPACE = /^\s*$/;
|
|
3843
|
-
//#endregion
|
|
3844
4004
|
//#region src/logger.ts
|
|
3845
4005
|
var Logger = class {
|
|
3846
4006
|
/**
|
|
@@ -4800,4 +4960,4 @@ var SizedSet = class extends Set {
|
|
|
4800
4960
|
}
|
|
4801
4961
|
};
|
|
4802
4962
|
//#endregion
|
|
4803
|
-
export { CancelablePromise, PROMISE_ABORT_EVENT, PROMISE_ABORT_OPTIONS, PROMISE_ERROR_NAME, PROMISE_MESSAGE_EXPECTATION_ATTEMPT, PROMISE_MESSAGE_EXPECTATION_RESULT, PROMISE_MESSAGE_EXPECTATION_TIMED, PROMISE_MESSAGE_TIMEOUT, PROMISE_STRATEGY_ALL, PROMISE_STRATEGY_DEFAULT, PROMISE_TYPE_FULFILLED, PROMISE_TYPE_REJECTED, PromiseTimeoutError, QueueError, RetryError, SORT_DIRECTION_ASCENDING, SORT_DIRECTION_DESCENDING, SizedMap, SizedSet, assert, attempt, attemptFlow, attemptPipe, attemptPromise, average, beacon, between, camelCase, cancelable, capitalize, ceil, chunk, clamp, clone, compact, compare, count, debounce, dedent, delay, diff, difference, drop, endsWith, endsWithArray, equal, error, exists, filter, find, first, flatten, floor, flow, fromQuery, toPromise as fromResult, toPromise, getArray, getArrayPosition, getColor, getError, getForegroundColor, getHexColor, getHexaColor, getHslColor, getHslaColor, getNormalizedHex, getNumber, getRandomBoolean, getRandomCharacters, getRandomColor, getRandomFloat, getRandomHex, getRandomInteger, getRandomItem, getRandomItems, getRgbColor, getRgbaColor, getString, getTimedPromise, getUuid, getValue, groupBy, handleResult, hasValue, hexToHsl, hexToHsla, hexToRgb, hexToRgba, hslToHex, hslToRgb, hslToRgba, ignoreKey, inMap, inSet, includes, includesArray, indexOf, indexOfArray, insert, intersection, isArrayOrPlainObject, isColor, isConstructor, isEmpty, isError, isFulfilled, isHexColor, isHslColor, isHslLike, isHslaColor, isInstanceOf, isKey, isNonArrayOrPlainObject, isNonConstructor, isNonEmpty, isNonInstanceOf, isNonKey, isNonNullable, isNonNullableOrEmpty, isNonNullableOrWhitespace, isNonNumber, isNonNumerical, isNonObject, isNonPlainObject, isNonPrimitive, isNonTypedArray, isNullable, isNullableOrEmpty, isNullableOrWhitespace, isNumber, isNumerical, isObject, isOk, isPlainObject, isPrimitive, isRejected, isResult, isRgbColor, isRgbLike, isRgbaColor, isTypedArray, join, kebabCase, last, logger, lowerCase, matchResult, max, median, memoize, merge, min, move, noop, ok, omit, once, parse, partition, pascalCase, pick, pipe, promises, push, queue, range, retry, reverse, rgbToHex, rgbToHsl, rgbToHsla, round, select, setValue, settlePromise, shuffle, single, slice, smush, snakeCase, sort, splice, startsWith, startsWithArray, sum, swap, take, template, throttle, timed, times, titleCase, toMap, toQuery, toRecord, toResult, toSet, toggle, trim, truncate, tryDecode, tryEncode, union, unique, unsmush, unwrap, update, upperCase, words };
|
|
4963
|
+
export { CancelablePromise, PROMISE_ABORT_EVENT, PROMISE_ABORT_OPTIONS, PROMISE_ERROR_NAME, PROMISE_MESSAGE_EXPECTATION_ATTEMPT, PROMISE_MESSAGE_EXPECTATION_RESULT, PROMISE_MESSAGE_EXPECTATION_TIMED, PROMISE_MESSAGE_TIMEOUT, PROMISE_STRATEGY_ALL, PROMISE_STRATEGY_DEFAULT, PROMISE_TYPE_FULFILLED, PROMISE_TYPE_REJECTED, PromiseTimeoutError, QueueError, RetryError, SORT_DIRECTION_ASCENDING, SORT_DIRECTION_DESCENDING, SizedMap, SizedSet, assert, attempt, attemptFlow, attemptPipe, attemptPromise, average, beacon, between, camelCase, cancelable, capitalize, ceil, chunk, clamp, clone, compact, compare, count, debounce, dedent, delay, diff, difference, drop, endsWith, endsWithArray, equal, error, exists, filter, find, first, flatten, floor, flow, fromQuery, toPromise as fromResult, toPromise, fuzzy, getArray, getArrayPosition, getColor, getError, getForegroundColor, getHexColor, getHexaColor, getHslColor, getHslaColor, getNormalizedHex, getNumber, getRandomBoolean, getRandomCharacters, getRandomColor, getRandomFloat, getRandomHex, getRandomInteger, getRandomItem, getRandomItems, getRgbColor, getRgbaColor, getString, getTimedPromise, getUuid, getValue, groupBy, handleResult, hasValue, hexToHsl, hexToHsla, hexToRgb, hexToRgba, hslToHex, hslToRgb, hslToRgba, ignoreKey, inMap, inSet, includes, includesArray, indexOf, indexOfArray, insert, intersection, isArrayOrPlainObject, isColor, isConstructor, isEmpty, isError, isFulfilled, isHexColor, isHslColor, isHslLike, isHslaColor, isInstanceOf, isKey, isNonArrayOrPlainObject, isNonConstructor, isNonEmpty, isNonInstanceOf, isNonKey, isNonNullable, isNonNullableOrEmpty, isNonNullableOrWhitespace, isNonNumber, isNonNumerical, isNonObject, isNonPlainObject, isNonPrimitive, isNonTypedArray, isNullable, isNullableOrEmpty, isNullableOrWhitespace, isNumber, isNumerical, isObject, isOk, isPlainObject, isPrimitive, isRejected, isResult, isRgbColor, isRgbLike, isRgbaColor, isTypedArray, join, kebabCase, last, logger, lowerCase, matchResult, max, median, memoize, merge, min, move, noop, ok, omit, once, parse, partition, pascalCase, pick, pipe, promises, push, queue, range, retry, reverse, rgbToHex, rgbToHsl, rgbToHsla, round, select, setValue, settlePromise, shuffle, single, slice, smush, snakeCase, sort, splice, startsWith, startsWithArray, sum, swap, take, template, throttle, timed, times, titleCase, toMap, toQuery, toRecord, toResult, toSet, toggle, trim, truncate, tryDecode, tryEncode, union, unique, unsmush, unwrap, update, upperCase, words };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { FIND_VALUE_INDEX, findValue } from "./find.mjs";
|
|
2
2
|
//#region src/internal/array/index-of.ts
|
|
3
3
|
function indexOf(array, ...parameters) {
|
|
4
|
-
return findValue(FIND_VALUE_INDEX, array, parameters);
|
|
4
|
+
return findValue(FIND_VALUE_INDEX, array, parameters, false);
|
|
5
5
|
}
|
|
6
6
|
//#endregion
|
|
7
7
|
export { indexOf };
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { PlainObject } from "../models.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/string/fuzzy.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Fuzzy searcher for an array of items
|
|
6
|
+
*/
|
|
7
|
+
declare class Fuzzy<Item> {
|
|
8
|
+
#private;
|
|
9
|
+
/**
|
|
10
|
+
* Get items currently being searched through
|
|
11
|
+
*/
|
|
12
|
+
get items(): Item[];
|
|
13
|
+
/**
|
|
14
|
+
* Set new items to search through
|
|
15
|
+
*/
|
|
16
|
+
set items(items: Item[]);
|
|
17
|
+
/**
|
|
18
|
+
* Get strings currently being searched through _(the stringified version of `items`)_
|
|
19
|
+
*/
|
|
20
|
+
get strings(): string[];
|
|
21
|
+
constructor(state: FuzzyState<Item>);
|
|
22
|
+
/**
|
|
23
|
+
* Search for items matching the given value
|
|
24
|
+
* @param value Value to search for
|
|
25
|
+
* @param options Search options
|
|
26
|
+
* @returns Search results, with exact matches _(ordered)_ and similar matches _(ordered by relevance)_
|
|
27
|
+
*/
|
|
28
|
+
search(value: string, options?: FuzzyOptions): FuzzyResult<Item>;
|
|
29
|
+
/**
|
|
30
|
+
* Search for items matching the given value
|
|
31
|
+
* @param value Value to search for
|
|
32
|
+
* @param limit Maximum number of combined items to return in `exact` and `similar`
|
|
33
|
+
* @returns Search results, with exact matches _(ordered)_ and similar matches _(ordered by relevance)_
|
|
34
|
+
*/
|
|
35
|
+
search(value: string, limit: number): FuzzyResult<Item>;
|
|
36
|
+
}
|
|
37
|
+
type FuzzyConfiguration<Item> = {
|
|
38
|
+
/**
|
|
39
|
+
* Handler to stringify items
|
|
40
|
+
*
|
|
41
|
+
* May be a function that takes an item and returns a string, or if items are plain objects, a key of the item to use to grab a string value from
|
|
42
|
+
*
|
|
43
|
+
* Defaults to `getString`
|
|
44
|
+
*/
|
|
45
|
+
handler?: (item: Item) => string;
|
|
46
|
+
} & (Item extends PlainObject ? {
|
|
47
|
+
/**
|
|
48
|
+
* Key to use to stringify items
|
|
49
|
+
*
|
|
50
|
+
* Prioritized over `handler`
|
|
51
|
+
*/
|
|
52
|
+
key?: keyof Item;
|
|
53
|
+
} : {}) & FuzzyOptions;
|
|
54
|
+
type FuzzyOptions = {
|
|
55
|
+
/**
|
|
56
|
+
* Maximum number of combined items to return in `exact` and `similar` _(defaults to all matches)_
|
|
57
|
+
*/
|
|
58
|
+
limit?: number;
|
|
59
|
+
/**
|
|
60
|
+
* Maximum score difference between the best and worst similar matches included in results; higher values cast a wider net _(defaults to 5)_
|
|
61
|
+
*/
|
|
62
|
+
tolerance?: number;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Search results from a fuzzy search, with exact and similar matches
|
|
66
|
+
*/
|
|
67
|
+
type FuzzyResult<Item> = {
|
|
68
|
+
/**
|
|
69
|
+
* Ordered items that exactly match the search value
|
|
70
|
+
*/
|
|
71
|
+
exact: Item[];
|
|
72
|
+
/**
|
|
73
|
+
* Ordered items that are similar to the search value, ranked by relevance
|
|
74
|
+
*/
|
|
75
|
+
similar: Item[];
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Options for fuzzy searching
|
|
79
|
+
*/
|
|
80
|
+
type FuzzySearchOptions = FuzzyOptions;
|
|
81
|
+
type FuzzyState<Item> = {
|
|
82
|
+
handler: (item: Item) => string;
|
|
83
|
+
items: Item[];
|
|
84
|
+
limit?: number;
|
|
85
|
+
strings: string[];
|
|
86
|
+
tolerance: number;
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Create a fuzzy searcher for an array of items
|
|
90
|
+
* @param items Items to search through
|
|
91
|
+
* @param key Key to use to stringify items
|
|
92
|
+
* @returns Fuzzy searcher
|
|
93
|
+
*/
|
|
94
|
+
declare function fuzzy<Item extends PlainObject, ItemKey extends keyof Item>(items: Item[], key?: ItemKey): Fuzzy<Item>;
|
|
95
|
+
/**
|
|
96
|
+
* Create a fuzzy searcher for an array of items
|
|
97
|
+
* @param items Items to search through
|
|
98
|
+
* @param handler Handler to stringify items
|
|
99
|
+
* @returns Fuzzy searcher
|
|
100
|
+
*/
|
|
101
|
+
declare function fuzzy<Item>(items: Item[], handler?: (item: Item) => string): Fuzzy<Item>;
|
|
102
|
+
/**
|
|
103
|
+
* Create a fuzzy searcher for an array of items
|
|
104
|
+
* @param items Items to search through
|
|
105
|
+
* @param configuration Fuzzy configuration
|
|
106
|
+
* @returns Fuzzy searcher
|
|
107
|
+
*/
|
|
108
|
+
declare function fuzzy<Item>(items: Item[], configuration?: FuzzyConfiguration<Item>): Fuzzy<Item>;
|
|
109
|
+
//#endregion
|
|
110
|
+
export { FuzzyConfiguration, FuzzyOptions, FuzzyResult, FuzzySearchOptions, fuzzy };
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { isPlainObject } from "../internal/is.mjs";
|
|
2
|
+
import { getString } from "../internal/string.mjs";
|
|
3
|
+
import { lowerCase } from "./case.mjs";
|
|
4
|
+
import { includes } from "./match.mjs";
|
|
5
|
+
//#region src/string/fuzzy.ts
|
|
6
|
+
/**
|
|
7
|
+
* Fuzzy searcher for an array of items
|
|
8
|
+
*/
|
|
9
|
+
var Fuzzy = class {
|
|
10
|
+
#state;
|
|
11
|
+
/**
|
|
12
|
+
* Get items currently being searched through
|
|
13
|
+
*/
|
|
14
|
+
get items() {
|
|
15
|
+
return this.#state.items.slice();
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Set new items to search through
|
|
19
|
+
*/
|
|
20
|
+
set items(items) {
|
|
21
|
+
if (Array.isArray(items)) {
|
|
22
|
+
this.#state.items = items.slice();
|
|
23
|
+
this.#state.strings = items.map(this.#state.handler);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Get strings currently being searched through _(the stringified version of `items`)_
|
|
28
|
+
*/
|
|
29
|
+
get strings() {
|
|
30
|
+
return this.#state.strings.slice();
|
|
31
|
+
}
|
|
32
|
+
constructor(state) {
|
|
33
|
+
this.#state = state;
|
|
34
|
+
}
|
|
35
|
+
search(value, options) {
|
|
36
|
+
return search(this.#state.items, this.#state.strings, value, options == null ? this.#state : getOptions(options, this.#state));
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
function getHandler(input) {
|
|
40
|
+
if (input == null || input === getString) return getString;
|
|
41
|
+
switch (typeof input) {
|
|
42
|
+
case "function": return input;
|
|
43
|
+
case "string": return (item) => item[input];
|
|
44
|
+
default:
|
|
45
|
+
if (isPlainObject(input)) return getHandler(input.key ?? input.handler);
|
|
46
|
+
throw new TypeError(MESSAGE_HANDLER);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function getItems(items) {
|
|
50
|
+
return items.sort((first, second) => first.haystack.localeCompare(second.haystack)).map(({ item }) => item);
|
|
51
|
+
}
|
|
52
|
+
function getOptions(input, state) {
|
|
53
|
+
const options = isPlainObject(input) ? input : {};
|
|
54
|
+
const limit = typeof input === "number" ? input : options.limit;
|
|
55
|
+
if (typeof limit === "number" && !Number.isNaN(limit) && limit >= 1) options.limit = Math.floor(limit);
|
|
56
|
+
else options.limit = state?.limit;
|
|
57
|
+
if (typeof options.tolerance === "number" && !Number.isNaN(options.tolerance)) options.tolerance = options.tolerance < 0 ? 0 : Math.floor(options.tolerance);
|
|
58
|
+
else options.tolerance = state?.tolerance ?? PROXIMITY_THRESHOLD;
|
|
59
|
+
return options;
|
|
60
|
+
}
|
|
61
|
+
function getState(items, input) {
|
|
62
|
+
const handler = getHandler(input);
|
|
63
|
+
const options = getOptions(input);
|
|
64
|
+
return {
|
|
65
|
+
handler,
|
|
66
|
+
items: items.slice(),
|
|
67
|
+
limit: options.limit,
|
|
68
|
+
strings: items.map(handler),
|
|
69
|
+
tolerance: options.tolerance
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function fuzzy(items, configuration) {
|
|
73
|
+
if (!Array.isArray(items)) throw new TypeError(MESSAGE_ARRAY);
|
|
74
|
+
return new Fuzzy(getState(items, configuration));
|
|
75
|
+
}
|
|
76
|
+
function isSubsequence(haystack, needle) {
|
|
77
|
+
const lowerCaseHaystack = lowerCase(haystack);
|
|
78
|
+
const lowerCaseNeedle = lowerCase(needle);
|
|
79
|
+
const haystackLength = lowerCaseHaystack.length;
|
|
80
|
+
const needleLength = lowerCaseNeedle.length;
|
|
81
|
+
let needleIndex = 0;
|
|
82
|
+
for (let haystackIndex = 0; haystackIndex < haystackLength; haystackIndex += 1) {
|
|
83
|
+
if (lowerCaseHaystack[haystackIndex] === lowerCaseNeedle[needleIndex]) needleIndex += 1;
|
|
84
|
+
if (needleIndex === needleLength) return true;
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
function getScore(haystack, needle) {
|
|
89
|
+
if (!isSubsequence(haystack, needle)) return -1;
|
|
90
|
+
const lowerCaseHaystack = lowerCase(haystack);
|
|
91
|
+
const lowerCaseNeedle = lowerCase(needle);
|
|
92
|
+
const needleLength = lowerCaseNeedle.length;
|
|
93
|
+
let needleIndex = 0;
|
|
94
|
+
let previousMatchIndex = -1;
|
|
95
|
+
let score = 0;
|
|
96
|
+
for (let haystackIndex = 0; haystackIndex < lowerCaseHaystack.length; haystackIndex += 1) {
|
|
97
|
+
if (lowerCaseHaystack[haystackIndex] === lowerCaseNeedle[needleIndex]) {
|
|
98
|
+
score += 1;
|
|
99
|
+
if (haystackIndex === 0) score += 1;
|
|
100
|
+
if (previousMatchIndex !== -1) {
|
|
101
|
+
const gap = haystackIndex - previousMatchIndex - 1;
|
|
102
|
+
score += Math.max(0, PROXIMITY_THRESHOLD - gap);
|
|
103
|
+
}
|
|
104
|
+
previousMatchIndex = haystackIndex;
|
|
105
|
+
needleIndex += 1;
|
|
106
|
+
}
|
|
107
|
+
if (needleIndex === needleLength) break;
|
|
108
|
+
}
|
|
109
|
+
score -= Math.floor(lowerCaseHaystack.length / LENGTH_DIVISOR);
|
|
110
|
+
return Math.max(0, score);
|
|
111
|
+
}
|
|
112
|
+
function search(items, strings, input, options) {
|
|
113
|
+
const result = {
|
|
114
|
+
exact: [],
|
|
115
|
+
similar: []
|
|
116
|
+
};
|
|
117
|
+
const value = typeof input === "string" ? input.trim() : "";
|
|
118
|
+
if (value.length === 0) {
|
|
119
|
+
result.exact = items.slice(0, options.limit);
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
let { length } = items;
|
|
123
|
+
const exact = [];
|
|
124
|
+
const similar = [];
|
|
125
|
+
const scored = {};
|
|
126
|
+
for (let index = 0; index < length; index += 1) {
|
|
127
|
+
const item = items[index];
|
|
128
|
+
const haystack = strings[index];
|
|
129
|
+
if (includes(haystack, value, true)) {
|
|
130
|
+
exact.push({
|
|
131
|
+
item,
|
|
132
|
+
haystack
|
|
133
|
+
});
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const score = getScore(haystack, value);
|
|
137
|
+
if (score > -1) {
|
|
138
|
+
scored[score] ??= [];
|
|
139
|
+
scored[score].push({
|
|
140
|
+
item,
|
|
141
|
+
haystack
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const keys = Object.keys(scored).map(Number).sort((first, second) => second - first);
|
|
146
|
+
length = keys.length;
|
|
147
|
+
if (length > 0 && options.tolerance > 0) {
|
|
148
|
+
const maxScore = keys[0];
|
|
149
|
+
for (let index = 0; index < length; index += 1) {
|
|
150
|
+
const key = keys[index];
|
|
151
|
+
if (maxScore - key > options.tolerance) break;
|
|
152
|
+
similar.push(...getItems(scored[key]));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
result.exact = getItems(options.limit == null ? exact : exact.slice(0, options.limit));
|
|
156
|
+
if (options.limit == null) result.similar = similar;
|
|
157
|
+
else result.similar = similar.slice(0, options.limit - result.exact.length);
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
160
|
+
const LENGTH_DIVISOR = 3;
|
|
161
|
+
const MESSAGE_ARRAY = "Fuzzy requires an array of items";
|
|
162
|
+
const MESSAGE_HANDLER = "Fuzzy requires a key or function to stringify items";
|
|
163
|
+
const PROXIMITY_THRESHOLD = 5;
|
|
164
|
+
//#endregion
|
|
165
|
+
export { fuzzy };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oscarpalmer/atoms",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.179.1",
|
|
4
4
|
"description": "Atomic utilities for making your JavaScript better.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"helper",
|
|
@@ -192,6 +192,10 @@
|
|
|
192
192
|
"types": "./dist/string/case.d.mts",
|
|
193
193
|
"default": "./dist/string/case.mjs"
|
|
194
194
|
},
|
|
195
|
+
"./string/fuzzy": {
|
|
196
|
+
"types": "./dist/string/fuzzy.d.mts",
|
|
197
|
+
"default": "./dist/string/fuzzy.mjs"
|
|
198
|
+
},
|
|
195
199
|
"./string/match": {
|
|
196
200
|
"types": "./dist/string/match.d.mts",
|
|
197
201
|
"default": "./dist/string/match.mjs"
|
|
@@ -253,4 +257,4 @@
|
|
|
253
257
|
"vitest": "npm:@voidzero-dev/vite-plus-test@latest"
|
|
254
258
|
},
|
|
255
259
|
"packageManager": "npm@11.11.1"
|
|
256
|
-
}
|
|
260
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -48,7 +48,7 @@ export function indexOf<Item>(
|
|
|
48
48
|
export function indexOf<Item>(array: Item[], item: Item): number;
|
|
49
49
|
|
|
50
50
|
export function indexOf(array: unknown[], ...parameters: unknown[]): number {
|
|
51
|
-
return findValue(FIND_VALUE_INDEX, array, parameters) as number;
|
|
51
|
+
return findValue(FIND_VALUE_INDEX, array, parameters, false) as number;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
// #endregion
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
import {getString} from '../internal/string';
|
|
2
|
+
import {isPlainObject} from '../is';
|
|
3
|
+
import type {PlainObject, RequiredKeys} from '../models';
|
|
4
|
+
import {lowerCase} from './case';
|
|
5
|
+
import {includes} from './match';
|
|
6
|
+
|
|
7
|
+
// #region Types
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Fuzzy searcher for an array of items
|
|
11
|
+
*/
|
|
12
|
+
class Fuzzy<Item> {
|
|
13
|
+
#state: FuzzyState<Item>;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get items currently being searched through
|
|
17
|
+
*/
|
|
18
|
+
get items(): Item[] {
|
|
19
|
+
return this.#state.items.slice();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Set new items to search through
|
|
24
|
+
*/
|
|
25
|
+
set items(items: Item[]) {
|
|
26
|
+
if (Array.isArray(items)) {
|
|
27
|
+
this.#state.items = items.slice();
|
|
28
|
+
this.#state.strings = items.map(this.#state.handler);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get strings currently being searched through _(the stringified version of `items`)_
|
|
34
|
+
*/
|
|
35
|
+
get strings(): string[] {
|
|
36
|
+
return this.#state.strings.slice();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
constructor(state: FuzzyState<Item>) {
|
|
40
|
+
this.#state = state;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Search for items matching the given value
|
|
45
|
+
* @param value Value to search for
|
|
46
|
+
* @param options Search options
|
|
47
|
+
* @returns Search results, with exact matches _(ordered)_ and similar matches _(ordered by relevance)_
|
|
48
|
+
*/
|
|
49
|
+
search(value: string, options?: FuzzyOptions): FuzzyResult<Item>;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Search for items matching the given value
|
|
53
|
+
* @param value Value to search for
|
|
54
|
+
* @param limit Maximum number of combined items to return in `exact` and `similar`
|
|
55
|
+
* @returns Search results, with exact matches _(ordered)_ and similar matches _(ordered by relevance)_
|
|
56
|
+
*/
|
|
57
|
+
search(value: string, limit: number): FuzzyResult<Item>;
|
|
58
|
+
|
|
59
|
+
search(value: string, options?: number | FuzzyOptions): FuzzyResult<Item> {
|
|
60
|
+
return search(
|
|
61
|
+
this.#state.items,
|
|
62
|
+
this.#state.strings,
|
|
63
|
+
value,
|
|
64
|
+
options == null ? this.#state : getOptions(options, this.#state),
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type FuzzyConfiguration<Item> = {
|
|
70
|
+
/**
|
|
71
|
+
* Handler to stringify items
|
|
72
|
+
*
|
|
73
|
+
* May be a function that takes an item and returns a string, or if items are plain objects, a key of the item to use to grab a string value from
|
|
74
|
+
*
|
|
75
|
+
* Defaults to `getString`
|
|
76
|
+
*/
|
|
77
|
+
handler?: (item: Item) => string;
|
|
78
|
+
} & (Item extends PlainObject
|
|
79
|
+
? {
|
|
80
|
+
/**
|
|
81
|
+
* Key to use to stringify items
|
|
82
|
+
*
|
|
83
|
+
* Prioritized over `handler`
|
|
84
|
+
*/
|
|
85
|
+
key?: keyof Item;
|
|
86
|
+
}
|
|
87
|
+
: {}) &
|
|
88
|
+
FuzzyOptions;
|
|
89
|
+
|
|
90
|
+
type FuzzyItem<Item> = {
|
|
91
|
+
item: Item;
|
|
92
|
+
haystack: string;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export type FuzzyOptions = {
|
|
96
|
+
/**
|
|
97
|
+
* Maximum number of combined items to return in `exact` and `similar` _(defaults to all matches)_
|
|
98
|
+
*/
|
|
99
|
+
limit?: number;
|
|
100
|
+
/**
|
|
101
|
+
* Maximum score difference between the best and worst similar matches included in results; higher values cast a wider net _(defaults to 5)_
|
|
102
|
+
*/
|
|
103
|
+
tolerance?: number;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Search results from a fuzzy search, with exact and similar matches
|
|
108
|
+
*/
|
|
109
|
+
export type FuzzyResult<Item> = {
|
|
110
|
+
/**
|
|
111
|
+
* Ordered items that exactly match the search value
|
|
112
|
+
*/
|
|
113
|
+
exact: Item[];
|
|
114
|
+
/**
|
|
115
|
+
* Ordered items that are similar to the search value, ranked by relevance
|
|
116
|
+
*/
|
|
117
|
+
similar: Item[];
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Options for fuzzy searching
|
|
122
|
+
*/
|
|
123
|
+
export type FuzzySearchOptions = FuzzyOptions;
|
|
124
|
+
|
|
125
|
+
type FuzzyState<Item> = {
|
|
126
|
+
handler: (item: Item) => string;
|
|
127
|
+
items: Item[];
|
|
128
|
+
limit?: number;
|
|
129
|
+
strings: string[];
|
|
130
|
+
tolerance: number;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// #endregion
|
|
134
|
+
|
|
135
|
+
// #region Functions
|
|
136
|
+
|
|
137
|
+
function getHandler<Item>(input: unknown): (item: Item) => string {
|
|
138
|
+
if (input == null || input === getString) {
|
|
139
|
+
return getString;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
switch (typeof input) {
|
|
143
|
+
case 'function':
|
|
144
|
+
return input as (item: Item) => string;
|
|
145
|
+
|
|
146
|
+
case 'string':
|
|
147
|
+
return (item: Item) => (item as PlainObject)[input] as string;
|
|
148
|
+
|
|
149
|
+
default: {
|
|
150
|
+
if (isPlainObject(input)) {
|
|
151
|
+
return getHandler(
|
|
152
|
+
(input as FuzzyConfiguration<PlainObject>).key ??
|
|
153
|
+
(input as FuzzyConfiguration<Item>).handler,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
throw new TypeError(MESSAGE_HANDLER);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function getItems<Item>(items: FuzzyItem<Item>[]): Item[] {
|
|
163
|
+
return items
|
|
164
|
+
.sort((first, second) => first.haystack.localeCompare(second.haystack))
|
|
165
|
+
.map(({item}) => item);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function getOptions<Item>(
|
|
169
|
+
input: unknown,
|
|
170
|
+
state?: FuzzyState<Item>,
|
|
171
|
+
): RequiredKeys<FuzzyOptions, 'tolerance'> {
|
|
172
|
+
const options: FuzzyOptions = isPlainObject(input) ? input : {};
|
|
173
|
+
|
|
174
|
+
const limit = typeof input === 'number' ? input : options.limit;
|
|
175
|
+
|
|
176
|
+
if (typeof limit === 'number' && !Number.isNaN(limit) && limit >= 1) {
|
|
177
|
+
options.limit = Math.floor(limit);
|
|
178
|
+
} else {
|
|
179
|
+
options.limit = state?.limit;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (typeof options.tolerance === 'number' && !Number.isNaN(options.tolerance)) {
|
|
183
|
+
options.tolerance = options.tolerance < 0 ? 0 : Math.floor(options.tolerance);
|
|
184
|
+
} else {
|
|
185
|
+
options.tolerance = state?.tolerance ?? PROXIMITY_THRESHOLD;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return options as RequiredKeys<FuzzyOptions, 'tolerance'>;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function getState<Item>(items: Item[], input: unknown): FuzzyState<Item> {
|
|
192
|
+
const handler = getHandler(input);
|
|
193
|
+
const options = getOptions(input);
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
handler,
|
|
197
|
+
items: items.slice(),
|
|
198
|
+
limit: options.limit,
|
|
199
|
+
strings: items.map(handler),
|
|
200
|
+
tolerance: options.tolerance,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Create a fuzzy searcher for an array of items
|
|
206
|
+
* @param items Items to search through
|
|
207
|
+
* @param key Key to use to stringify items
|
|
208
|
+
* @returns Fuzzy searcher
|
|
209
|
+
*/
|
|
210
|
+
export function fuzzy<Item extends PlainObject, ItemKey extends keyof Item>(
|
|
211
|
+
items: Item[],
|
|
212
|
+
key?: ItemKey,
|
|
213
|
+
): Fuzzy<Item>;
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Create a fuzzy searcher for an array of items
|
|
217
|
+
* @param items Items to search through
|
|
218
|
+
* @param handler Handler to stringify items
|
|
219
|
+
* @returns Fuzzy searcher
|
|
220
|
+
*/
|
|
221
|
+
export function fuzzy<Item>(items: Item[], handler?: (item: Item) => string): Fuzzy<Item>;
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Create a fuzzy searcher for an array of items
|
|
225
|
+
* @param items Items to search through
|
|
226
|
+
* @param configuration Fuzzy configuration
|
|
227
|
+
* @returns Fuzzy searcher
|
|
228
|
+
*/
|
|
229
|
+
export function fuzzy<Item>(items: Item[], configuration?: FuzzyConfiguration<Item>): Fuzzy<Item>;
|
|
230
|
+
|
|
231
|
+
export function fuzzy(items: unknown[], configuration?: unknown): Fuzzy<unknown> {
|
|
232
|
+
if (!Array.isArray(items)) {
|
|
233
|
+
throw new TypeError(MESSAGE_ARRAY);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return new Fuzzy(getState(items, configuration));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function isSubsequence(haystack: string, needle: string): boolean {
|
|
240
|
+
const lowerCaseHaystack = lowerCase(haystack);
|
|
241
|
+
const lowerCaseNeedle = lowerCase(needle);
|
|
242
|
+
|
|
243
|
+
const haystackLength = lowerCaseHaystack.length;
|
|
244
|
+
const needleLength = lowerCaseNeedle.length;
|
|
245
|
+
|
|
246
|
+
let needleIndex = 0;
|
|
247
|
+
|
|
248
|
+
for (let haystackIndex = 0; haystackIndex < haystackLength; haystackIndex += 1) {
|
|
249
|
+
// Advance needle pointer only on a matching character
|
|
250
|
+
if (lowerCaseHaystack[haystackIndex] === lowerCaseNeedle[needleIndex]) {
|
|
251
|
+
needleIndex += 1;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// All needle characters matched in order
|
|
255
|
+
if (needleIndex === needleLength) {
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function getScore(haystack: string, needle: string): number {
|
|
264
|
+
if (!isSubsequence(haystack, needle)) {
|
|
265
|
+
return -1;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const lowerCaseHaystack = lowerCase(haystack);
|
|
269
|
+
const lowerCaseNeedle = lowerCase(needle);
|
|
270
|
+
|
|
271
|
+
const needleLength = lowerCaseNeedle.length;
|
|
272
|
+
|
|
273
|
+
let needleIndex = 0;
|
|
274
|
+
let previousMatchIndex = -1;
|
|
275
|
+
let score = 0;
|
|
276
|
+
|
|
277
|
+
for (let haystackIndex = 0; haystackIndex < lowerCaseHaystack.length; haystackIndex += 1) {
|
|
278
|
+
if (lowerCaseHaystack[haystackIndex] === lowerCaseNeedle[needleIndex]) {
|
|
279
|
+
// +1 for each matched character
|
|
280
|
+
score += 1;
|
|
281
|
+
|
|
282
|
+
// Bonus for matching at the start of the string
|
|
283
|
+
if (haystackIndex === 0) {
|
|
284
|
+
score += 1;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Proximity bonus: decays as gap between consecutive matches widens
|
|
288
|
+
if (previousMatchIndex !== -1) {
|
|
289
|
+
const gap = haystackIndex - previousMatchIndex - 1;
|
|
290
|
+
|
|
291
|
+
score += Math.max(0, PROXIMITY_THRESHOLD - gap);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
previousMatchIndex = haystackIndex;
|
|
295
|
+
|
|
296
|
+
needleIndex += 1;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// All needle characters matched; no need to scan further
|
|
300
|
+
if (needleIndex === needleLength) {
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Penalty for longer strings to favour tighter matches
|
|
306
|
+
score -= Math.floor(lowerCaseHaystack.length / LENGTH_DIVISOR);
|
|
307
|
+
|
|
308
|
+
return Math.max(0, score);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function search<Item>(
|
|
312
|
+
items: Item[],
|
|
313
|
+
strings: string[],
|
|
314
|
+
input: string,
|
|
315
|
+
options: RequiredKeys<FuzzyOptions, 'tolerance'>,
|
|
316
|
+
) {
|
|
317
|
+
const result: FuzzyResult<Item> = {
|
|
318
|
+
exact: [],
|
|
319
|
+
similar: [],
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const value = typeof input === 'string' ? input.trim() : '';
|
|
323
|
+
|
|
324
|
+
if (value.length === 0) {
|
|
325
|
+
result.exact = items.slice(0, options.limit);
|
|
326
|
+
|
|
327
|
+
return result;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
let {length} = items;
|
|
331
|
+
|
|
332
|
+
const exact: Array<FuzzyItem<Item>> = [];
|
|
333
|
+
const similar: Array<Item> = [];
|
|
334
|
+
|
|
335
|
+
const scored: Record<number, Array<FuzzyItem<Item>>> = {};
|
|
336
|
+
|
|
337
|
+
for (let index = 0; index < length; index += 1) {
|
|
338
|
+
const item = items[index];
|
|
339
|
+
const haystack = strings[index];
|
|
340
|
+
|
|
341
|
+
if (includes(haystack, value, true)) {
|
|
342
|
+
exact.push({item, haystack});
|
|
343
|
+
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const score = getScore(haystack, value);
|
|
348
|
+
|
|
349
|
+
if (score > -1) {
|
|
350
|
+
scored[score] ??= [];
|
|
351
|
+
|
|
352
|
+
scored[score].push({item, haystack});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const keys = Object.keys(scored)
|
|
357
|
+
.map(Number)
|
|
358
|
+
.sort((first, second) => second - first);
|
|
359
|
+
|
|
360
|
+
length = keys.length;
|
|
361
|
+
|
|
362
|
+
if (length > 0 && options.tolerance > 0) {
|
|
363
|
+
const maxScore = keys[0];
|
|
364
|
+
|
|
365
|
+
for (let index = 0; index < length; index += 1) {
|
|
366
|
+
const key = keys[index];
|
|
367
|
+
|
|
368
|
+
if (maxScore - key > options.tolerance) {
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
similar.push(...getItems(scored[key]));
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
result.exact = getItems(options.limit == null ? exact : exact.slice(0, options.limit));
|
|
377
|
+
|
|
378
|
+
if (options.limit == null) {
|
|
379
|
+
result.similar = similar;
|
|
380
|
+
} else {
|
|
381
|
+
result.similar = similar.slice(0, options.limit - result.exact.length);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return result;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// #endregion
|
|
388
|
+
|
|
389
|
+
// #region Variables
|
|
390
|
+
|
|
391
|
+
const LENGTH_DIVISOR = 3;
|
|
392
|
+
|
|
393
|
+
const MESSAGE_ARRAY = 'Fuzzy requires an array of items';
|
|
394
|
+
|
|
395
|
+
const MESSAGE_HANDLER = 'Fuzzy requires a key or function to stringify items';
|
|
396
|
+
|
|
397
|
+
const PROXIMITY_THRESHOLD = 5;
|
|
398
|
+
|
|
399
|
+
// #endregion
|