@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 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.178.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
@@ -26,6 +26,7 @@ export * from './internal/value/has';
26
26
  export * from './internal/value/set';
27
27
 
28
28
  export * from './string/case';
29
+ export * from './string/fuzzy';
29
30
  export * from './string/index';
30
31
  export * from './string/match';
31
32
  export * from './string/template';
@@ -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