@oscarpalmer/atoms 0.219.0 → 0.220.0
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 +48 -19
- package/dist/index.mjs +239 -163
- package/dist/models/string/string.fuzzy.model.d.mts +46 -18
- package/dist/models/string/string.fuzzy.model.mjs +3 -1
- package/dist/string/fuzzy.d.mts +3 -3
- package/dist/string/fuzzy.mjs +171 -97
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -6367,26 +6367,28 @@ type Fuzzy<Item> = {
|
|
|
6367
6367
|
*
|
|
6368
6368
|
* @param value Value to search for
|
|
6369
6369
|
* @param options Search options
|
|
6370
|
-
* @returns Search results, with exact
|
|
6370
|
+
* @returns Search results, with exact, included, and similar matches
|
|
6371
6371
|
*/
|
|
6372
6372
|
search(value: string, options?: FuzzyOptions): FuzzyResult<Item>;
|
|
6373
6373
|
/**
|
|
6374
6374
|
* Search for items matching the given value
|
|
6375
6375
|
*
|
|
6376
6376
|
* @param value Value to search for
|
|
6377
|
-
* @param limit Maximum number of combined items to return in `exact` and `similar`
|
|
6378
|
-
* @returns Search results, with exact
|
|
6377
|
+
* @param limit Maximum number of combined items to return in `exact`, `included`, and `similar`
|
|
6378
|
+
* @returns Search results, with exact, included, and similar matches
|
|
6379
6379
|
*/
|
|
6380
6380
|
search(value: string, limit: number): FuzzyResult<Item>;
|
|
6381
6381
|
};
|
|
6382
6382
|
type FuzzyConfiguration<Item> = {
|
|
6383
|
+
handler?: (item: Item) => string;
|
|
6383
6384
|
/**
|
|
6384
|
-
*
|
|
6385
|
+
* Function to compare the stringified items for results _(defaults to `Intl.Collator().compare`)_
|
|
6385
6386
|
*
|
|
6386
|
-
*
|
|
6387
|
-
*
|
|
6387
|
+
* @param first First string to compare
|
|
6388
|
+
* @param second Second string to compare
|
|
6389
|
+
* @returns Comparison result, similar to `Array.prototype.sort` callback
|
|
6388
6390
|
*/
|
|
6389
|
-
|
|
6391
|
+
compare?: (first: string, second: string) => number;
|
|
6390
6392
|
} & (Item extends PlainObject ? {
|
|
6391
6393
|
/**
|
|
6392
6394
|
* Key to use to stringify items
|
|
@@ -6395,13 +6397,13 @@ type FuzzyConfiguration<Item> = {
|
|
|
6395
6397
|
*/
|
|
6396
6398
|
key?: keyof Item;
|
|
6397
6399
|
} : {}) & FuzzyOptions;
|
|
6398
|
-
type FuzzyItem<
|
|
6399
|
-
|
|
6400
|
-
|
|
6400
|
+
type FuzzyItem<Data> = {
|
|
6401
|
+
data: Data;
|
|
6402
|
+
string: FuzzyString;
|
|
6401
6403
|
};
|
|
6402
6404
|
type FuzzyOptions = {
|
|
6403
6405
|
/**
|
|
6404
|
-
* Maximum number of combined items to return in `exact` and `similar` _(defaults to all matches)_
|
|
6406
|
+
* Maximum number of combined items to return in `exact`, `included`, and `similar` _(defaults to all matches)_
|
|
6405
6407
|
*/
|
|
6406
6408
|
limit?: number;
|
|
6407
6409
|
/**
|
|
@@ -6412,29 +6414,56 @@ type FuzzyOptions = {
|
|
|
6412
6414
|
*/
|
|
6413
6415
|
tolerance?: number;
|
|
6414
6416
|
};
|
|
6417
|
+
type FuzzyRanked<Data> = {
|
|
6418
|
+
position: number;
|
|
6419
|
+
/**
|
|
6420
|
+
* `0` for starting with the value, `1` for the value starting a word, `2` for anything else
|
|
6421
|
+
*/
|
|
6422
|
+
rank: number;
|
|
6423
|
+
} & FuzzyItem<Data>;
|
|
6415
6424
|
/**
|
|
6416
|
-
* Search results from a fuzzy search, with exact and similar matches
|
|
6425
|
+
* Search results from a fuzzy search, with exact, included, and similar matches
|
|
6417
6426
|
*/
|
|
6418
6427
|
type FuzzyResult<Item> = {
|
|
6419
6428
|
/**
|
|
6420
|
-
*
|
|
6429
|
+
* Items whose strings exactly match the search value, ordered alphabetically
|
|
6421
6430
|
*/
|
|
6422
6431
|
exact: Item[];
|
|
6423
6432
|
/**
|
|
6424
|
-
*
|
|
6433
|
+
* Items whose strings contain the search value, ordered alphabetically
|
|
6434
|
+
*/
|
|
6435
|
+
included: Item[];
|
|
6436
|
+
/**
|
|
6437
|
+
* Items whose strings are similar to the search value, ranked by relevance, sorted by score, and ordered alphabetically within the same score
|
|
6425
6438
|
*/
|
|
6426
6439
|
similar: Item[];
|
|
6427
6440
|
};
|
|
6428
6441
|
type FuzzyState<Item> = {
|
|
6442
|
+
compare: FuzzyStateCompare<Item>;
|
|
6429
6443
|
handler(item: Item): string;
|
|
6430
|
-
items: Item
|
|
6431
|
-
limit
|
|
6432
|
-
strings: string[];
|
|
6444
|
+
items: FuzzyStateItems<Item>;
|
|
6445
|
+
limit: number;
|
|
6433
6446
|
tolerance: number;
|
|
6434
6447
|
};
|
|
6448
|
+
type FuzzyStateCompare<Item = unknown> = {
|
|
6449
|
+
item: (first: FuzzyItem<Item>, second: FuzzyItem<Item>) => number;
|
|
6450
|
+
ranked: (first: FuzzyRanked<Item>, second: FuzzyRanked<Item>) => number;
|
|
6451
|
+
};
|
|
6452
|
+
type FuzzyStateItems<Item> = {
|
|
6453
|
+
original: Array<FuzzyItem<Item>>;
|
|
6454
|
+
sorted?: Array<FuzzyItem<Item>>;
|
|
6455
|
+
};
|
|
6456
|
+
type FuzzyString = {
|
|
6457
|
+
lowercase: string;
|
|
6458
|
+
original: string;
|
|
6459
|
+
};
|
|
6460
|
+
type InternalFuzzy<Item = unknown> = {
|
|
6461
|
+
[FUZZY_SYMBOL]: FuzzyState<Item>;
|
|
6462
|
+
} & Fuzzy<Item>;
|
|
6463
|
+
declare const FUZZY_SYMBOL: unique symbol;
|
|
6435
6464
|
//#endregion
|
|
6436
6465
|
//#region src/string/fuzzy.d.ts
|
|
6437
|
-
declare function Fuzzy(this:
|
|
6466
|
+
declare function Fuzzy(this: InternalFuzzy, state: FuzzyState<unknown>): void;
|
|
6438
6467
|
/**
|
|
6439
6468
|
* Create a fuzzy searcher for an array of items
|
|
6440
6469
|
*
|
|
@@ -9453,4 +9482,4 @@ export declare class SizedSet<Value = unknown> extends Set<Value> {
|
|
|
9453
9482
|
get(value: Value, update?: boolean): Value | undefined;
|
|
9454
9483
|
}
|
|
9455
9484
|
//#endregion
|
|
9456
|
-
export { type ArrayComparisonSorter, type ArrayKeySorter, type ArrayKeySorters, type ArraySorter, type ArraySorters, type ArrayValueSorter, type Asserter, type AssignOptions, type Assigner, type AsyncFailure, type AsyncFlow, type AsyncLimiter, type AsyncOnce, type AsyncPlan, type AsyncPlanned, type AsyncStep, type AsyncSuccess, type Beacon, type BeaconOptions, type CloneOptions, type Cloner, type Color, type ColorType, type ColorTypeExtended, type ColorValue, type ColorValueExtended, type CompareCallback, type ComparisonSorter, type DiffOptions, type DiffResult, type EqualOptions, type Equalizer, type Failure, type Flow, type FulfilledPromise, type Fuzzy, type
|
|
9485
|
+
export { type ArrayComparisonSorter, type ArrayKeySorter, type ArrayKeySorters, type ArraySorter, type ArraySorters, type ArrayValueSorter, type Asserter, type AssignOptions, type Assigner, type AsyncFailure, type AsyncFlow, type AsyncLimiter, type AsyncOnce, type AsyncPlan, type AsyncPlanned, type AsyncStep, type AsyncSuccess, type Beacon, type BeaconOptions, type CloneOptions, type Cloner, type Color, type ColorType, type ColorTypeExtended, type ColorValue, type ColorValueExtended, type CompareCallback, type ComparisonSorter, type DiffOptions, type DiffResult, type EqualOptions, type Equalizer, type Failure, type Flow, type FulfilledPromise, type Fuzzy, type FuzzyOptions, type FuzzyResult, type HSLAColor, type HSLColor, type HWBAColor, type HWBColor, type Herald, type HeraldEvents, type HeraldOptions, Key$1 as Key, type KeyedQueue, type Limiter, Logger, type Memoized, type MemoizedOptions, type MergeOptions, type Merger, type NormalizeOptions, type Normalizer, type Observable, type Observer, type Once, type Plan, type PlanError, type PlanOk, type PlanReturned, type Planned, type PromiseOptions, type PromiseStrategy, type PromisesOptions, type Queue, type QueueOptions, type Queued, type RGBAColor, type RGBColor, type RejectedPromise, type Renderer, type Shaken, type Smushed, type SortDirection, type Sorter, type Step, type Subscription, type SubscriptionParameters, type SubscriptionProperty, type Subscriptions, type SubscriptionsParameters, type Success, type TemplateOptions, type Templater, type TemplaterOptions, type TemplaterRenderer, type TimeLogger, type TransformCallback, type TransformCallbacks, type Transformer, type Unsmushed, push as append, filterAndMap as select, toPromise as fromResult, getRandomFloatingNumber as getRandomFloat, isLogger, isTimeLogger, mapAndFilter as reverseSelect };
|
package/dist/index.mjs
CHANGED
|
@@ -3728,79 +3728,12 @@ function attemptPipe(initial, first, ...seconds) {
|
|
|
3728
3728
|
attemptPipe.async = attemptAsyncPipe;
|
|
3729
3729
|
//#endregion
|
|
3730
3730
|
//#region src/models/string/string.fuzzy.model.ts
|
|
3731
|
+
const FUZZY_BOUNDARY_SEPARATOR = /[\s./:\\_-]/;
|
|
3731
3732
|
const FUZZY_MESSAGE_ARRAY = "Fuzzy requires an array of items";
|
|
3732
3733
|
const FUZZY_MESSAGE_HANDLER = "Fuzzy requires a key or function to stringify items";
|
|
3733
3734
|
const FUZZY_PROPERTY = "$fuzzy";
|
|
3734
3735
|
const FUZZY_SYMBOL = Symbol(FUZZY_PROPERTY);
|
|
3735
3736
|
//#endregion
|
|
3736
|
-
//#region src/string/match.ts
|
|
3737
|
-
/**
|
|
3738
|
-
* Check if a string ends with a specified substring
|
|
3739
|
-
*
|
|
3740
|
-
* @param haystack String to look in
|
|
3741
|
-
* @param needle String to look for
|
|
3742
|
-
* @param ignoreCase Ignore case when matching? _(defaults to `false`)_
|
|
3743
|
-
* @returns `true` if the string ends with the given substring, otherwise `false`
|
|
3744
|
-
*
|
|
3745
|
-
* @example
|
|
3746
|
-
* ```typescript
|
|
3747
|
-
* endsWith('this is a test', 'test'); // => true
|
|
3748
|
-
* endsWith('this is a test', 'Test'); // => false
|
|
3749
|
-
* endsWith('this is a test', 'Test', true); // => true
|
|
3750
|
-
* endsWith('this is a test', 'not'); // => false
|
|
3751
|
-
* ```
|
|
3752
|
-
*/
|
|
3753
|
-
function endsWith(haystack, needle, ignoreCase) {
|
|
3754
|
-
return match(STRING_MATCH_ENDS_WITH, haystack, needle, ignoreCase === true);
|
|
3755
|
-
}
|
|
3756
|
-
/**
|
|
3757
|
-
* Check if a string includes a specified substring
|
|
3758
|
-
*
|
|
3759
|
-
* @param haystack String to look in
|
|
3760
|
-
* @param needle String to look for
|
|
3761
|
-
* @param ignoreCase Ignore case when matching? _(defaults to `false`)_
|
|
3762
|
-
* @returns `true` if the string includes the given substring, otherwise `false`
|
|
3763
|
-
*
|
|
3764
|
-
* @example
|
|
3765
|
-
* ```typescript
|
|
3766
|
-
* includes('this is a test', 'is'); // => true
|
|
3767
|
-
* includes('this is a test', 'Is'); // => false
|
|
3768
|
-
* includes('this is a test', 'Is', true); // => true
|
|
3769
|
-
* includes('this is a test', 'not'); // => false
|
|
3770
|
-
* ```
|
|
3771
|
-
*/
|
|
3772
|
-
function includes(haystack, needle, ignoreCase) {
|
|
3773
|
-
return match(STRING_MATCH_INCLUDES, haystack, needle, ignoreCase === true);
|
|
3774
|
-
}
|
|
3775
|
-
function match(type, haystack, needle, ignoreCase) {
|
|
3776
|
-
if (typeof haystack !== "string" || typeof needle !== "string") return false;
|
|
3777
|
-
matchMemoizers[type] ??= memoize(matchCallback.bind(type));
|
|
3778
|
-
return matchMemoizers[type].run(haystack, needle, ignoreCase);
|
|
3779
|
-
}
|
|
3780
|
-
function matchCallback(haystack, needle, ignoreCase) {
|
|
3781
|
-
return (ignoreCase ? haystack.toLocaleLowerCase() : haystack)[this](ignoreCase ? needle.toLocaleLowerCase() : needle);
|
|
3782
|
-
}
|
|
3783
|
-
/**
|
|
3784
|
-
* Check if a string starts with a specified substring
|
|
3785
|
-
*
|
|
3786
|
-
* @param haystack String to look in
|
|
3787
|
-
* @param needle String to look for
|
|
3788
|
-
* @param ignoreCase Ignore case when matching? _(defaults to `false`)_
|
|
3789
|
-
* @returns `true` if the string starts with the given substring, otherwise `false`
|
|
3790
|
-
*
|
|
3791
|
-
* @example
|
|
3792
|
-
* ```typescript
|
|
3793
|
-
* startsWith('this is a test', 'this'); // => true
|
|
3794
|
-
* startsWith('this is a test', 'This'); // => false
|
|
3795
|
-
* startsWith('this is a test', 'This', true); // => true
|
|
3796
|
-
* startsWith('this is a test', 'not'); // => false
|
|
3797
|
-
* ```
|
|
3798
|
-
*/
|
|
3799
|
-
function startsWith(haystack, needle, ignoreCase) {
|
|
3800
|
-
return match(STRING_MATCH_STARTS_WITH, haystack, needle, ignoreCase === true);
|
|
3801
|
-
}
|
|
3802
|
-
const matchMemoizers = {};
|
|
3803
|
-
//#endregion
|
|
3804
3737
|
//#region src/string/fuzzy.ts
|
|
3805
3738
|
function Fuzzy(state) {
|
|
3806
3739
|
this[FUZZY_SYMBOL] = state;
|
|
@@ -3810,39 +3743,76 @@ Fuzzy.prototype.search = search;
|
|
|
3810
3743
|
Object.defineProperties(Fuzzy.prototype, {
|
|
3811
3744
|
items: {
|
|
3812
3745
|
enumerable: true,
|
|
3813
|
-
get:
|
|
3814
|
-
set:
|
|
3746
|
+
get: getItems,
|
|
3747
|
+
set: setItems
|
|
3815
3748
|
},
|
|
3816
3749
|
strings: {
|
|
3817
3750
|
enumerable: true,
|
|
3818
|
-
get:
|
|
3751
|
+
get: getStrings
|
|
3819
3752
|
}
|
|
3820
3753
|
});
|
|
3821
|
-
function
|
|
3822
|
-
return
|
|
3754
|
+
function compareItem(sorter) {
|
|
3755
|
+
return (first, second) => sorter(first.string.original, second.string.original);
|
|
3823
3756
|
}
|
|
3824
|
-
function
|
|
3825
|
-
|
|
3826
|
-
|
|
3757
|
+
function compareRanked(sorter) {
|
|
3758
|
+
return (first, second) => first.rank - second.rank || first.position - second.position || sorter(first.string.original, second.string.original);
|
|
3759
|
+
}
|
|
3760
|
+
function createItems(state, items) {
|
|
3761
|
+
state.items.original = items.map((item) => ({
|
|
3762
|
+
data: item,
|
|
3763
|
+
string: createString(state.handler(item))
|
|
3764
|
+
}));
|
|
3765
|
+
state.items.sorted = void 0;
|
|
3766
|
+
}
|
|
3767
|
+
function createOptions(input, state) {
|
|
3768
|
+
const options = {};
|
|
3769
|
+
const original = isPlainObject(input) ? input : {};
|
|
3770
|
+
const limit = typeof input === "number" ? input : original.limit;
|
|
3827
3771
|
if (typeof limit === "number" && !Number.isNaN(limit) && limit >= 1) options.limit = floor(limit);
|
|
3828
|
-
else options.limit = state?.limit;
|
|
3829
|
-
options.tolerance = getTolerance(
|
|
3772
|
+
else options.limit = state?.limit ?? Number.POSITIVE_INFINITY;
|
|
3773
|
+
options.tolerance = getTolerance(original.tolerance, state);
|
|
3830
3774
|
return options;
|
|
3831
3775
|
}
|
|
3832
|
-
function
|
|
3833
|
-
|
|
3834
|
-
|
|
3776
|
+
function createRanked(item, needle, position) {
|
|
3777
|
+
let boundary = position;
|
|
3778
|
+
while (boundary > -1 && !isBoundary(item.string, boundary)) boundary = item.string.lowercase.indexOf(needle, boundary + 1);
|
|
3779
|
+
if (boundary === -1) return {
|
|
3780
|
+
...item,
|
|
3781
|
+
position,
|
|
3782
|
+
rank: 2
|
|
3783
|
+
};
|
|
3835
3784
|
return {
|
|
3785
|
+
...item,
|
|
3786
|
+
position: boundary,
|
|
3787
|
+
rank: boundary === 0 ? 0 : 1
|
|
3788
|
+
};
|
|
3789
|
+
}
|
|
3790
|
+
function createState(items, input) {
|
|
3791
|
+
const handler = getHandler(input);
|
|
3792
|
+
const options = createOptions(input);
|
|
3793
|
+
const sorter = getComparator(input);
|
|
3794
|
+
const state = {
|
|
3795
|
+
compare: {
|
|
3796
|
+
item: compareItem(sorter),
|
|
3797
|
+
ranked: compareRanked(sorter)
|
|
3798
|
+
},
|
|
3836
3799
|
handler,
|
|
3837
|
-
items:
|
|
3800
|
+
items: { original: [] },
|
|
3838
3801
|
limit: options.limit,
|
|
3839
|
-
strings: items.map(handler),
|
|
3840
3802
|
tolerance: options.tolerance
|
|
3841
3803
|
};
|
|
3804
|
+
createItems(state, items);
|
|
3805
|
+
return state;
|
|
3806
|
+
}
|
|
3807
|
+
function createString(original) {
|
|
3808
|
+
return {
|
|
3809
|
+
original,
|
|
3810
|
+
lowercase: lowerCase(original)
|
|
3811
|
+
};
|
|
3842
3812
|
}
|
|
3843
3813
|
function fuzzy(items, configuration) {
|
|
3844
3814
|
if (!Array.isArray(items)) throw new TypeError(FUZZY_MESSAGE_ARRAY);
|
|
3845
|
-
return new Fuzzy(
|
|
3815
|
+
return new Fuzzy(createState(items, configuration));
|
|
3846
3816
|
}
|
|
3847
3817
|
/**
|
|
3848
3818
|
* Does the needle match the haystack in a fuzzy way?
|
|
@@ -3856,53 +3826,117 @@ function fuzzy(items, configuration) {
|
|
|
3856
3826
|
function fuzzyMatch(haystack, needle) {
|
|
3857
3827
|
if (typeof haystack !== "string" || typeof needle !== "string") return false;
|
|
3858
3828
|
const trimmed = needle.trim();
|
|
3859
|
-
|
|
3860
|
-
|
|
3829
|
+
const lowercaseHaystack = lowerCase(haystack);
|
|
3830
|
+
const lowercaseNeedle = lowerCase(trimmed);
|
|
3831
|
+
if (lowercaseHaystack.includes(lowercaseNeedle)) return true;
|
|
3832
|
+
return getScore({
|
|
3833
|
+
lowercase: lowercaseHaystack,
|
|
3834
|
+
original: haystack
|
|
3835
|
+
}, lowercaseNeedle) > -1;
|
|
3836
|
+
}
|
|
3837
|
+
function getComparator(input) {
|
|
3838
|
+
if (isPlainObject(input) && typeof input.compare === "function") return input.compare;
|
|
3839
|
+
defaultSorter ??= Intl.Collator().compare;
|
|
3840
|
+
return defaultSorter;
|
|
3861
3841
|
}
|
|
3862
3842
|
function getHandler(input) {
|
|
3863
3843
|
if (input == null || input === getString) return getString;
|
|
3864
3844
|
switch (typeof input) {
|
|
3865
|
-
case "function": return input;
|
|
3866
|
-
case "
|
|
3845
|
+
case "function": return (item) => getString(input(item));
|
|
3846
|
+
case "number":
|
|
3847
|
+
case "string":
|
|
3848
|
+
case "symbol": return (item) => getString(item?.[input]);
|
|
3867
3849
|
default:
|
|
3868
3850
|
if (isPlainObject(input)) return getHandler(input.key ?? input.handler);
|
|
3869
3851
|
throw new TypeError(FUZZY_MESSAGE_HANDLER);
|
|
3870
3852
|
}
|
|
3871
3853
|
}
|
|
3872
|
-
function
|
|
3873
|
-
return this[FUZZY_SYMBOL].items.
|
|
3874
|
-
}
|
|
3875
|
-
function getFuzzyStrings() {
|
|
3876
|
-
return this[FUZZY_SYMBOL].strings.slice();
|
|
3854
|
+
function getItems() {
|
|
3855
|
+
return this[FUZZY_SYMBOL].items.original.map((item) => item.data);
|
|
3877
3856
|
}
|
|
3878
|
-
function
|
|
3879
|
-
|
|
3880
|
-
const
|
|
3881
|
-
const lowerCaseNeedle = lowerCase(needle);
|
|
3882
|
-
const needleLength = lowerCaseNeedle.length;
|
|
3857
|
+
function getMatchEnd(haystack, needle, cursor) {
|
|
3858
|
+
const haystackLength = haystack.length;
|
|
3859
|
+
const needleLength = needle.length;
|
|
3883
3860
|
let needleIndex = 0;
|
|
3884
|
-
let
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3861
|
+
for (let index = cursor; index < haystackLength; index += 1) if (haystack[index] === needle[needleIndex]) {
|
|
3862
|
+
needleIndex += 1;
|
|
3863
|
+
if (needleIndex === needleLength) return index;
|
|
3864
|
+
}
|
|
3865
|
+
return -1;
|
|
3866
|
+
}
|
|
3867
|
+
function getResult(state, exact, included, scored, options) {
|
|
3868
|
+
const result = {
|
|
3869
|
+
exact: [],
|
|
3870
|
+
included: [],
|
|
3871
|
+
similar: []
|
|
3872
|
+
};
|
|
3873
|
+
exact.sort(state.compare.item);
|
|
3874
|
+
result.exact = exact.slice(0, options.limit).map((item) => item.data);
|
|
3875
|
+
included.sort(state.compare.ranked);
|
|
3876
|
+
result.included = included.slice(0, options.limit - result.exact.length).map((item) => item.data);
|
|
3877
|
+
const remaining = options.limit - result.exact.length - result.included.length;
|
|
3878
|
+
if (remaining === 0) return result;
|
|
3879
|
+
const keys = Object.keys(scored).map(Number).sort((first, second) => second - first);
|
|
3880
|
+
const keysLength = keys.length;
|
|
3881
|
+
if (keysLength === 0) return result;
|
|
3882
|
+
const maxScore = keys[0];
|
|
3883
|
+
outer: for (let index = 0; index < keysLength; index += 1) {
|
|
3884
|
+
const key = keys[index];
|
|
3885
|
+
if (maxScore - key > options.tolerance) break;
|
|
3886
|
+
const keyed = scored[key];
|
|
3887
|
+
const keyedLength = keyed.length;
|
|
3888
|
+
keyed.sort(state.compare.item);
|
|
3889
|
+
for (let keyedIndex = 0; keyedIndex < keyedLength; keyedIndex += 1) {
|
|
3890
|
+
result.similar.push(keyed[keyedIndex].data);
|
|
3891
|
+
if (result.similar.length >= remaining) break outer;
|
|
3892
|
+
}
|
|
3893
|
+
}
|
|
3894
|
+
return result;
|
|
3895
|
+
}
|
|
3896
|
+
function getScore(item, needle) {
|
|
3897
|
+
const haystackLength = item.lowercase.length;
|
|
3898
|
+
const needleLength = needle.length;
|
|
3899
|
+
if (needleLength > haystackLength) return -1;
|
|
3900
|
+
let best = -1;
|
|
3901
|
+
let cursor = 0;
|
|
3902
|
+
while (cursor < haystackLength) {
|
|
3903
|
+
const end = getMatchEnd(item.lowercase, needle, cursor);
|
|
3904
|
+
if (end === -1) break;
|
|
3905
|
+
let index = end;
|
|
3906
|
+
let needleIndex = needleLength - 1;
|
|
3907
|
+
let next = -1;
|
|
3908
|
+
let score = 0;
|
|
3909
|
+
let start = end;
|
|
3910
|
+
while (needleIndex >= 0) {
|
|
3911
|
+
if (item.lowercase[index] === needle[needleIndex]) {
|
|
3912
|
+
score += 1;
|
|
3913
|
+
if (next !== -1) score += max([0, 5 - (next - index - 1)]);
|
|
3914
|
+
if (isBoundary(item, index)) score += 2;
|
|
3915
|
+
next = index;
|
|
3916
|
+
start = index;
|
|
3917
|
+
needleIndex -= 1;
|
|
3893
3918
|
}
|
|
3894
|
-
|
|
3895
|
-
needleIndex += 1;
|
|
3919
|
+
index -= 1;
|
|
3896
3920
|
}
|
|
3897
|
-
if (
|
|
3921
|
+
if (start === 0) score += 1;
|
|
3922
|
+
if (score > best) best = score;
|
|
3923
|
+
cursor = start + 1;
|
|
3898
3924
|
}
|
|
3899
|
-
|
|
3900
|
-
return max([0,
|
|
3925
|
+
if (best === -1) return -1;
|
|
3926
|
+
return max([0, best - floor(haystackLength / 3)]);
|
|
3927
|
+
}
|
|
3928
|
+
function getStrings() {
|
|
3929
|
+
return this[FUZZY_SYMBOL].items.original.map((item) => item.string.original);
|
|
3901
3930
|
}
|
|
3902
3931
|
function getTolerance(input, state) {
|
|
3903
3932
|
if (typeof input === "number" && !Number.isNaN(input)) return input < 0 ? 0 : floor(input);
|
|
3904
3933
|
return state?.tolerance ?? 5;
|
|
3905
3934
|
}
|
|
3935
|
+
function isBoundary(string, index) {
|
|
3936
|
+
if (index === 0 || FUZZY_BOUNDARY_SEPARATOR.test(string.lowercase[index - 1])) return true;
|
|
3937
|
+
if (string.original.length !== string.lowercase.length) return false;
|
|
3938
|
+
return string.original[index] !== string.lowercase[index] && string.original[index - 1] === string.lowercase[index - 1];
|
|
3939
|
+
}
|
|
3906
3940
|
/**
|
|
3907
3941
|
* Is the value a fuzzy searcher?
|
|
3908
3942
|
*
|
|
@@ -3914,74 +3948,48 @@ function getTolerance(input, state) {
|
|
|
3914
3948
|
function isFuzzy(value) {
|
|
3915
3949
|
return typeof value === "object" && value !== null && "$fuzzy" in value && value["$fuzzy"] === true;
|
|
3916
3950
|
}
|
|
3917
|
-
function isSubsequence(haystack, needle) {
|
|
3918
|
-
const lowerCaseHaystack = lowerCase(haystack);
|
|
3919
|
-
const lowerCaseNeedle = lowerCase(needle);
|
|
3920
|
-
const haystackLength = lowerCaseHaystack.length;
|
|
3921
|
-
const needleLength = lowerCaseNeedle.length;
|
|
3922
|
-
let needleIndex = 0;
|
|
3923
|
-
for (let haystackIndex = 0; haystackIndex < haystackLength; haystackIndex += 1) {
|
|
3924
|
-
if (lowerCaseHaystack[haystackIndex] === lowerCaseNeedle[needleIndex]) needleIndex += 1;
|
|
3925
|
-
if (needleIndex === needleLength) return true;
|
|
3926
|
-
}
|
|
3927
|
-
return false;
|
|
3928
|
-
}
|
|
3929
3951
|
function search(input, option) {
|
|
3930
3952
|
const state = this[FUZZY_SYMBOL];
|
|
3931
|
-
const options = option == null ? state :
|
|
3932
|
-
const result = {
|
|
3933
|
-
exact: [],
|
|
3934
|
-
similar: []
|
|
3935
|
-
};
|
|
3953
|
+
const options = option == null ? state : createOptions(option, state);
|
|
3936
3954
|
const value = typeof input === "string" ? input.trim() : "";
|
|
3955
|
+
const lowercaseValue = lowerCase(value);
|
|
3937
3956
|
if (value.length === 0) {
|
|
3938
|
-
|
|
3939
|
-
return
|
|
3957
|
+
state.items.sorted ??= state.items.original.toSorted(state.compare.item);
|
|
3958
|
+
return {
|
|
3959
|
+
exact: state.items.sorted.slice(0, options.limit).map((item) => item.data),
|
|
3960
|
+
included: [],
|
|
3961
|
+
similar: []
|
|
3962
|
+
};
|
|
3940
3963
|
}
|
|
3941
|
-
let { length } = state.items;
|
|
3942
3964
|
const exact = [];
|
|
3943
|
-
const
|
|
3965
|
+
const included = [];
|
|
3944
3966
|
const scored = {};
|
|
3967
|
+
const { length } = state.items.original;
|
|
3945
3968
|
for (let index = 0; index < length; index += 1) {
|
|
3946
|
-
const item = state.items[index];
|
|
3947
|
-
const
|
|
3948
|
-
if (
|
|
3949
|
-
exact.push(
|
|
3950
|
-
item,
|
|
3951
|
-
haystack
|
|
3952
|
-
});
|
|
3969
|
+
const item = state.items.original[index];
|
|
3970
|
+
const { string } = item;
|
|
3971
|
+
if (string.lowercase === lowercaseValue) {
|
|
3972
|
+
exact.push(item);
|
|
3953
3973
|
continue;
|
|
3954
3974
|
}
|
|
3955
|
-
const
|
|
3975
|
+
const position = string.lowercase.indexOf(lowercaseValue);
|
|
3976
|
+
if (position > -1) {
|
|
3977
|
+
included.push(createRanked(item, lowercaseValue, position));
|
|
3978
|
+
continue;
|
|
3979
|
+
}
|
|
3980
|
+
const score = getScore(string, lowercaseValue);
|
|
3956
3981
|
if (score > -1) {
|
|
3957
3982
|
scored[score] ??= [];
|
|
3958
|
-
scored[score].push(
|
|
3959
|
-
item,
|
|
3960
|
-
haystack
|
|
3961
|
-
});
|
|
3962
|
-
}
|
|
3963
|
-
}
|
|
3964
|
-
const keys = Object.keys(scored).map(Number).sort((first, second) => second - first);
|
|
3965
|
-
length = keys.length;
|
|
3966
|
-
if (length > 0 && options.tolerance > 0) {
|
|
3967
|
-
const maxScore = keys[0];
|
|
3968
|
-
for (let index = 0; index < length; index += 1) {
|
|
3969
|
-
const key = keys[index];
|
|
3970
|
-
if (maxScore - key > options.tolerance) break;
|
|
3971
|
-
similar.push(...createFuzzyItems(scored[key]));
|
|
3983
|
+
scored[score].push(item);
|
|
3972
3984
|
}
|
|
3973
3985
|
}
|
|
3974
|
-
|
|
3975
|
-
if (options.limit == null) result.similar = similar;
|
|
3976
|
-
else result.similar = similar.slice(0, options.limit - result.exact.length);
|
|
3977
|
-
return result;
|
|
3986
|
+
return getResult(state, exact, included, scored, options);
|
|
3978
3987
|
}
|
|
3979
|
-
function
|
|
3988
|
+
function setItems(value) {
|
|
3980
3989
|
if (!Array.isArray(value)) throw new TypeError(FUZZY_MESSAGE_ARRAY);
|
|
3981
|
-
|
|
3982
|
-
state.items = value.slice();
|
|
3983
|
-
state.strings = value.map((item) => state.handler(item));
|
|
3990
|
+
createItems(this[FUZZY_SYMBOL], value);
|
|
3984
3991
|
}
|
|
3992
|
+
let defaultSorter;
|
|
3985
3993
|
fuzzy.is = isFuzzy;
|
|
3986
3994
|
fuzzy.match = fuzzyMatch;
|
|
3987
3995
|
//#endregion
|
|
@@ -4089,6 +4097,74 @@ function truncate(value, length, suffix) {
|
|
|
4089
4097
|
return `${value.slice(0, truncatedLength)}${actualSuffix}`;
|
|
4090
4098
|
}
|
|
4091
4099
|
//#endregion
|
|
4100
|
+
//#region src/string/match.ts
|
|
4101
|
+
/**
|
|
4102
|
+
* Check if a string ends with a specified substring
|
|
4103
|
+
*
|
|
4104
|
+
* @param haystack String to look in
|
|
4105
|
+
* @param needle String to look for
|
|
4106
|
+
* @param ignoreCase Ignore case when matching? _(defaults to `false`)_
|
|
4107
|
+
* @returns `true` if the string ends with the given substring, otherwise `false`
|
|
4108
|
+
*
|
|
4109
|
+
* @example
|
|
4110
|
+
* ```typescript
|
|
4111
|
+
* endsWith('this is a test', 'test'); // => true
|
|
4112
|
+
* endsWith('this is a test', 'Test'); // => false
|
|
4113
|
+
* endsWith('this is a test', 'Test', true); // => true
|
|
4114
|
+
* endsWith('this is a test', 'not'); // => false
|
|
4115
|
+
* ```
|
|
4116
|
+
*/
|
|
4117
|
+
function endsWith(haystack, needle, ignoreCase) {
|
|
4118
|
+
return match(STRING_MATCH_ENDS_WITH, haystack, needle, ignoreCase === true);
|
|
4119
|
+
}
|
|
4120
|
+
/**
|
|
4121
|
+
* Check if a string includes a specified substring
|
|
4122
|
+
*
|
|
4123
|
+
* @param haystack String to look in
|
|
4124
|
+
* @param needle String to look for
|
|
4125
|
+
* @param ignoreCase Ignore case when matching? _(defaults to `false`)_
|
|
4126
|
+
* @returns `true` if the string includes the given substring, otherwise `false`
|
|
4127
|
+
*
|
|
4128
|
+
* @example
|
|
4129
|
+
* ```typescript
|
|
4130
|
+
* includes('this is a test', 'is'); // => true
|
|
4131
|
+
* includes('this is a test', 'Is'); // => false
|
|
4132
|
+
* includes('this is a test', 'Is', true); // => true
|
|
4133
|
+
* includes('this is a test', 'not'); // => false
|
|
4134
|
+
* ```
|
|
4135
|
+
*/
|
|
4136
|
+
function includes(haystack, needle, ignoreCase) {
|
|
4137
|
+
return match(STRING_MATCH_INCLUDES, haystack, needle, ignoreCase === true);
|
|
4138
|
+
}
|
|
4139
|
+
function match(type, haystack, needle, ignoreCase) {
|
|
4140
|
+
if (typeof haystack !== "string" || typeof needle !== "string") return false;
|
|
4141
|
+
matchMemoizers[type] ??= memoize(matchCallback.bind(type));
|
|
4142
|
+
return matchMemoizers[type].run(haystack, needle, ignoreCase);
|
|
4143
|
+
}
|
|
4144
|
+
function matchCallback(haystack, needle, ignoreCase) {
|
|
4145
|
+
return (ignoreCase ? haystack.toLocaleLowerCase() : haystack)[this](ignoreCase ? needle.toLocaleLowerCase() : needle);
|
|
4146
|
+
}
|
|
4147
|
+
/**
|
|
4148
|
+
* Check if a string starts with a specified substring
|
|
4149
|
+
*
|
|
4150
|
+
* @param haystack String to look in
|
|
4151
|
+
* @param needle String to look for
|
|
4152
|
+
* @param ignoreCase Ignore case when matching? _(defaults to `false`)_
|
|
4153
|
+
* @returns `true` if the string starts with the given substring, otherwise `false`
|
|
4154
|
+
*
|
|
4155
|
+
* @example
|
|
4156
|
+
* ```typescript
|
|
4157
|
+
* startsWith('this is a test', 'this'); // => true
|
|
4158
|
+
* startsWith('this is a test', 'This'); // => false
|
|
4159
|
+
* startsWith('this is a test', 'This', true); // => true
|
|
4160
|
+
* startsWith('this is a test', 'not'); // => false
|
|
4161
|
+
* ```
|
|
4162
|
+
*/
|
|
4163
|
+
function startsWith(haystack, needle, ignoreCase) {
|
|
4164
|
+
return match(STRING_MATCH_STARTS_WITH, haystack, needle, ignoreCase === true);
|
|
4165
|
+
}
|
|
4166
|
+
const matchMemoizers = {};
|
|
4167
|
+
//#endregion
|
|
4092
4168
|
//#region src/models/string/string.normalize.model.ts
|
|
4093
4169
|
const NORMALIZE_DEBURR_CHARACTERS = {
|
|
4094
4170
|
Æ: "AE",
|
|
@@ -27,26 +27,28 @@ export type Fuzzy<Item> = {
|
|
|
27
27
|
*
|
|
28
28
|
* @param value Value to search for
|
|
29
29
|
* @param options Search options
|
|
30
|
-
* @returns Search results, with exact
|
|
30
|
+
* @returns Search results, with exact, included, and similar matches
|
|
31
31
|
*/
|
|
32
32
|
search(value: string, options?: FuzzyOptions): FuzzyResult<Item>;
|
|
33
33
|
/**
|
|
34
34
|
* Search for items matching the given value
|
|
35
35
|
*
|
|
36
36
|
* @param value Value to search for
|
|
37
|
-
* @param limit Maximum number of combined items to return in `exact` and `similar`
|
|
38
|
-
* @returns Search results, with exact
|
|
37
|
+
* @param limit Maximum number of combined items to return in `exact`, `included`, and `similar`
|
|
38
|
+
* @returns Search results, with exact, included, and similar matches
|
|
39
39
|
*/
|
|
40
40
|
search(value: string, limit: number): FuzzyResult<Item>;
|
|
41
41
|
};
|
|
42
42
|
export type FuzzyConfiguration<Item> = {
|
|
43
|
+
handler?: (item: Item) => string;
|
|
43
44
|
/**
|
|
44
|
-
*
|
|
45
|
+
* Function to compare the stringified items for results _(defaults to `Intl.Collator().compare`)_
|
|
45
46
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
47
|
+
* @param first First string to compare
|
|
48
|
+
* @param second Second string to compare
|
|
49
|
+
* @returns Comparison result, similar to `Array.prototype.sort` callback
|
|
48
50
|
*/
|
|
49
|
-
|
|
51
|
+
compare?: (first: string, second: string) => number;
|
|
50
52
|
} & (Item extends PlainObject ? {
|
|
51
53
|
/**
|
|
52
54
|
* Key to use to stringify items
|
|
@@ -55,13 +57,13 @@ export type FuzzyConfiguration<Item> = {
|
|
|
55
57
|
*/
|
|
56
58
|
key?: keyof Item;
|
|
57
59
|
} : {}) & FuzzyOptions;
|
|
58
|
-
export type FuzzyItem<
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
export type FuzzyItem<Data> = {
|
|
61
|
+
data: Data;
|
|
62
|
+
string: FuzzyString;
|
|
61
63
|
};
|
|
62
64
|
export type FuzzyOptions = {
|
|
63
65
|
/**
|
|
64
|
-
* Maximum number of combined items to return in `exact` and `similar` _(defaults to all matches)_
|
|
66
|
+
* Maximum number of combined items to return in `exact`, `included`, and `similar` _(defaults to all matches)_
|
|
65
67
|
*/
|
|
66
68
|
limit?: number;
|
|
67
69
|
/**
|
|
@@ -72,16 +74,27 @@ export type FuzzyOptions = {
|
|
|
72
74
|
*/
|
|
73
75
|
tolerance?: number;
|
|
74
76
|
};
|
|
77
|
+
export type FuzzyRanked<Data> = {
|
|
78
|
+
position: number;
|
|
79
|
+
/**
|
|
80
|
+
* `0` for starting with the value, `1` for the value starting a word, `2` for anything else
|
|
81
|
+
*/
|
|
82
|
+
rank: number;
|
|
83
|
+
} & FuzzyItem<Data>;
|
|
75
84
|
/**
|
|
76
|
-
* Search results from a fuzzy search, with exact and similar matches
|
|
85
|
+
* Search results from a fuzzy search, with exact, included, and similar matches
|
|
77
86
|
*/
|
|
78
87
|
export type FuzzyResult<Item> = {
|
|
79
88
|
/**
|
|
80
|
-
*
|
|
89
|
+
* Items whose strings exactly match the search value, ordered alphabetically
|
|
81
90
|
*/
|
|
82
91
|
exact: Item[];
|
|
83
92
|
/**
|
|
84
|
-
*
|
|
93
|
+
* Items whose strings contain the search value, ordered alphabetically
|
|
94
|
+
*/
|
|
95
|
+
included: Item[];
|
|
96
|
+
/**
|
|
97
|
+
* Items whose strings are similar to the search value, ranked by relevance, sorted by score, and ordered alphabetically within the same score
|
|
85
98
|
*/
|
|
86
99
|
similar: Item[];
|
|
87
100
|
};
|
|
@@ -90,19 +103,34 @@ export type FuzzyResult<Item> = {
|
|
|
90
103
|
*/
|
|
91
104
|
export type FuzzySearchOptions = { [Key in keyof FuzzyOptions]: FuzzyOptions[Key]; };
|
|
92
105
|
export type FuzzyState<Item> = {
|
|
106
|
+
compare: FuzzyStateCompare<Item>;
|
|
93
107
|
handler(item: Item): string;
|
|
94
|
-
items: Item
|
|
95
|
-
limit
|
|
96
|
-
strings: string[];
|
|
108
|
+
items: FuzzyStateItems<Item>;
|
|
109
|
+
limit: number;
|
|
97
110
|
tolerance: number;
|
|
98
111
|
};
|
|
112
|
+
type FuzzyStateCompare<Item = unknown> = {
|
|
113
|
+
item: (first: FuzzyItem<Item>, second: FuzzyItem<Item>) => number;
|
|
114
|
+
ranked: (first: FuzzyRanked<Item>, second: FuzzyRanked<Item>) => number;
|
|
115
|
+
};
|
|
116
|
+
type FuzzyStateItems<Item> = {
|
|
117
|
+
original: Array<FuzzyItem<Item>>;
|
|
118
|
+
sorted?: Array<FuzzyItem<Item>>;
|
|
119
|
+
};
|
|
120
|
+
export type FuzzyString = {
|
|
121
|
+
lowercase: string;
|
|
122
|
+
original: string;
|
|
123
|
+
};
|
|
99
124
|
export type InternalFuzzy<Item = unknown> = {
|
|
100
125
|
[FUZZY_SYMBOL]: FuzzyState<Item>;
|
|
101
126
|
} & Fuzzy<Item>;
|
|
127
|
+
export declare const FUZZY_BOUNDARY_BONUS = 2;
|
|
128
|
+
export declare const FUZZY_BOUNDARY_SEPARATOR: RegExp;
|
|
102
129
|
export declare const FUZZY_LENGTH_DIVISOR = 3;
|
|
103
130
|
export declare const FUZZY_MESSAGE_ARRAY = "Fuzzy requires an array of items";
|
|
104
131
|
export declare const FUZZY_MESSAGE_HANDLER = "Fuzzy requires a key or function to stringify items";
|
|
105
132
|
export declare const FUZZY_PROPERTY = "$fuzzy";
|
|
106
133
|
export declare const FUZZY_PROXIMITY_THRESHOLD = 5;
|
|
107
134
|
export declare const FUZZY_SYMBOL: unique symbol;
|
|
108
|
-
//#endregion
|
|
135
|
+
//#endregion
|
|
136
|
+
export {};
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
//#region src/models/string/string.fuzzy.model.ts
|
|
2
|
+
const FUZZY_BOUNDARY_BONUS = 2;
|
|
3
|
+
const FUZZY_BOUNDARY_SEPARATOR = /[\s./:\\_-]/;
|
|
2
4
|
const FUZZY_LENGTH_DIVISOR = 3;
|
|
3
5
|
const FUZZY_MESSAGE_ARRAY = "Fuzzy requires an array of items";
|
|
4
6
|
const FUZZY_MESSAGE_HANDLER = "Fuzzy requires a key or function to stringify items";
|
|
@@ -6,4 +8,4 @@ const FUZZY_PROPERTY = "$fuzzy";
|
|
|
6
8
|
const FUZZY_PROXIMITY_THRESHOLD = 5;
|
|
7
9
|
const FUZZY_SYMBOL = Symbol(FUZZY_PROPERTY);
|
|
8
10
|
//#endregion
|
|
9
|
-
export { FUZZY_LENGTH_DIVISOR, FUZZY_MESSAGE_ARRAY, FUZZY_MESSAGE_HANDLER, FUZZY_PROPERTY, FUZZY_PROXIMITY_THRESHOLD, FUZZY_SYMBOL };
|
|
11
|
+
export { FUZZY_BOUNDARY_BONUS, FUZZY_BOUNDARY_SEPARATOR, FUZZY_LENGTH_DIVISOR, FUZZY_MESSAGE_ARRAY, FUZZY_MESSAGE_HANDLER, FUZZY_PROPERTY, FUZZY_PROXIMITY_THRESHOLD, FUZZY_SYMBOL };
|
package/dist/string/fuzzy.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { PlainObject } from "../models/index.mjs";
|
|
2
|
-
import { Fuzzy, FuzzyConfiguration,
|
|
2
|
+
import { Fuzzy, FuzzyConfiguration, FuzzyOptions, FuzzyResult, FuzzyState, InternalFuzzy } from "../models/string/string.fuzzy.model.mjs";
|
|
3
3
|
//#region src/string/fuzzy.d.ts
|
|
4
|
-
declare function Fuzzy(this:
|
|
4
|
+
declare function Fuzzy(this: InternalFuzzy, state: FuzzyState<unknown>): void;
|
|
5
5
|
/**
|
|
6
6
|
* Create a fuzzy searcher for an array of items
|
|
7
7
|
*
|
|
@@ -50,4 +50,4 @@ export declare namespace fuzzy {
|
|
|
50
50
|
export var match: typeof fuzzyMatch;
|
|
51
51
|
}
|
|
52
52
|
//#endregion
|
|
53
|
-
export type { Fuzzy,
|
|
53
|
+
export type { Fuzzy, FuzzyOptions, FuzzyResult };
|
package/dist/string/fuzzy.mjs
CHANGED
|
@@ -3,8 +3,7 @@ import { floor } from "../internal/math/misc.mjs";
|
|
|
3
3
|
import { max } from "../internal/math/aggregate.mjs";
|
|
4
4
|
import { getString } from "../internal/string/misc.mjs";
|
|
5
5
|
import { lowerCase } from "../internal/string/case.mjs";
|
|
6
|
-
import { FUZZY_MESSAGE_ARRAY, FUZZY_MESSAGE_HANDLER, FUZZY_PROPERTY, FUZZY_SYMBOL } from "../models/string/string.fuzzy.model.mjs";
|
|
7
|
-
import { includes } from "./match.mjs";
|
|
6
|
+
import { FUZZY_BOUNDARY_SEPARATOR, FUZZY_MESSAGE_ARRAY, FUZZY_MESSAGE_HANDLER, FUZZY_PROPERTY, FUZZY_SYMBOL } from "../models/string/string.fuzzy.model.mjs";
|
|
8
7
|
//#region src/string/fuzzy.ts
|
|
9
8
|
function Fuzzy(state) {
|
|
10
9
|
this[FUZZY_SYMBOL] = state;
|
|
@@ -14,39 +13,76 @@ Fuzzy.prototype.search = search;
|
|
|
14
13
|
Object.defineProperties(Fuzzy.prototype, {
|
|
15
14
|
items: {
|
|
16
15
|
enumerable: true,
|
|
17
|
-
get:
|
|
18
|
-
set:
|
|
16
|
+
get: getItems,
|
|
17
|
+
set: setItems
|
|
19
18
|
},
|
|
20
19
|
strings: {
|
|
21
20
|
enumerable: true,
|
|
22
|
-
get:
|
|
21
|
+
get: getStrings
|
|
23
22
|
}
|
|
24
23
|
});
|
|
25
|
-
function
|
|
26
|
-
return
|
|
24
|
+
function compareItem(sorter) {
|
|
25
|
+
return (first, second) => sorter(first.string.original, second.string.original);
|
|
27
26
|
}
|
|
28
|
-
function
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
function compareRanked(sorter) {
|
|
28
|
+
return (first, second) => first.rank - second.rank || first.position - second.position || sorter(first.string.original, second.string.original);
|
|
29
|
+
}
|
|
30
|
+
function createItems(state, items) {
|
|
31
|
+
state.items.original = items.map((item) => ({
|
|
32
|
+
data: item,
|
|
33
|
+
string: createString(state.handler(item))
|
|
34
|
+
}));
|
|
35
|
+
state.items.sorted = void 0;
|
|
36
|
+
}
|
|
37
|
+
function createOptions(input, state) {
|
|
38
|
+
const options = {};
|
|
39
|
+
const original = isPlainObject(input) ? input : {};
|
|
40
|
+
const limit = typeof input === "number" ? input : original.limit;
|
|
31
41
|
if (typeof limit === "number" && !Number.isNaN(limit) && limit >= 1) options.limit = floor(limit);
|
|
32
|
-
else options.limit = state?.limit;
|
|
33
|
-
options.tolerance = getTolerance(
|
|
42
|
+
else options.limit = state?.limit ?? Number.POSITIVE_INFINITY;
|
|
43
|
+
options.tolerance = getTolerance(original.tolerance, state);
|
|
34
44
|
return options;
|
|
35
45
|
}
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
|
|
46
|
+
function createRanked(item, needle, position) {
|
|
47
|
+
let boundary = position;
|
|
48
|
+
while (boundary > -1 && !isBoundary(item.string, boundary)) boundary = item.string.lowercase.indexOf(needle, boundary + 1);
|
|
49
|
+
if (boundary === -1) return {
|
|
50
|
+
...item,
|
|
51
|
+
position,
|
|
52
|
+
rank: 2
|
|
53
|
+
};
|
|
39
54
|
return {
|
|
55
|
+
...item,
|
|
56
|
+
position: boundary,
|
|
57
|
+
rank: boundary === 0 ? 0 : 1
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function createState(items, input) {
|
|
61
|
+
const handler = getHandler(input);
|
|
62
|
+
const options = createOptions(input);
|
|
63
|
+
const sorter = getComparator(input);
|
|
64
|
+
const state = {
|
|
65
|
+
compare: {
|
|
66
|
+
item: compareItem(sorter),
|
|
67
|
+
ranked: compareRanked(sorter)
|
|
68
|
+
},
|
|
40
69
|
handler,
|
|
41
|
-
items:
|
|
70
|
+
items: { original: [] },
|
|
42
71
|
limit: options.limit,
|
|
43
|
-
strings: items.map(handler),
|
|
44
72
|
tolerance: options.tolerance
|
|
45
73
|
};
|
|
74
|
+
createItems(state, items);
|
|
75
|
+
return state;
|
|
76
|
+
}
|
|
77
|
+
function createString(original) {
|
|
78
|
+
return {
|
|
79
|
+
original,
|
|
80
|
+
lowercase: lowerCase(original)
|
|
81
|
+
};
|
|
46
82
|
}
|
|
47
83
|
function fuzzy(items, configuration) {
|
|
48
84
|
if (!Array.isArray(items)) throw new TypeError(FUZZY_MESSAGE_ARRAY);
|
|
49
|
-
return new Fuzzy(
|
|
85
|
+
return new Fuzzy(createState(items, configuration));
|
|
50
86
|
}
|
|
51
87
|
/**
|
|
52
88
|
* Does the needle match the haystack in a fuzzy way?
|
|
@@ -60,53 +96,117 @@ function fuzzy(items, configuration) {
|
|
|
60
96
|
function fuzzyMatch(haystack, needle) {
|
|
61
97
|
if (typeof haystack !== "string" || typeof needle !== "string") return false;
|
|
62
98
|
const trimmed = needle.trim();
|
|
63
|
-
|
|
64
|
-
|
|
99
|
+
const lowercaseHaystack = lowerCase(haystack);
|
|
100
|
+
const lowercaseNeedle = lowerCase(trimmed);
|
|
101
|
+
if (lowercaseHaystack.includes(lowercaseNeedle)) return true;
|
|
102
|
+
return getScore({
|
|
103
|
+
lowercase: lowercaseHaystack,
|
|
104
|
+
original: haystack
|
|
105
|
+
}, lowercaseNeedle) > -1;
|
|
106
|
+
}
|
|
107
|
+
function getComparator(input) {
|
|
108
|
+
if (isPlainObject(input) && typeof input.compare === "function") return input.compare;
|
|
109
|
+
defaultSorter ??= Intl.Collator().compare;
|
|
110
|
+
return defaultSorter;
|
|
65
111
|
}
|
|
66
112
|
function getHandler(input) {
|
|
67
113
|
if (input == null || input === getString) return getString;
|
|
68
114
|
switch (typeof input) {
|
|
69
|
-
case "function": return input;
|
|
70
|
-
case "
|
|
115
|
+
case "function": return (item) => getString(input(item));
|
|
116
|
+
case "number":
|
|
117
|
+
case "string":
|
|
118
|
+
case "symbol": return (item) => getString(item?.[input]);
|
|
71
119
|
default:
|
|
72
120
|
if (isPlainObject(input)) return getHandler(input.key ?? input.handler);
|
|
73
121
|
throw new TypeError(FUZZY_MESSAGE_HANDLER);
|
|
74
122
|
}
|
|
75
123
|
}
|
|
76
|
-
function
|
|
77
|
-
return this[FUZZY_SYMBOL].items.
|
|
78
|
-
}
|
|
79
|
-
function getFuzzyStrings() {
|
|
80
|
-
return this[FUZZY_SYMBOL].strings.slice();
|
|
124
|
+
function getItems() {
|
|
125
|
+
return this[FUZZY_SYMBOL].items.original.map((item) => item.data);
|
|
81
126
|
}
|
|
82
|
-
function
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
const lowerCaseNeedle = lowerCase(needle);
|
|
86
|
-
const needleLength = lowerCaseNeedle.length;
|
|
127
|
+
function getMatchEnd(haystack, needle, cursor) {
|
|
128
|
+
const haystackLength = haystack.length;
|
|
129
|
+
const needleLength = needle.length;
|
|
87
130
|
let needleIndex = 0;
|
|
88
|
-
let
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
131
|
+
for (let index = cursor; index < haystackLength; index += 1) if (haystack[index] === needle[needleIndex]) {
|
|
132
|
+
needleIndex += 1;
|
|
133
|
+
if (needleIndex === needleLength) return index;
|
|
134
|
+
}
|
|
135
|
+
return -1;
|
|
136
|
+
}
|
|
137
|
+
function getResult(state, exact, included, scored, options) {
|
|
138
|
+
const result = {
|
|
139
|
+
exact: [],
|
|
140
|
+
included: [],
|
|
141
|
+
similar: []
|
|
142
|
+
};
|
|
143
|
+
exact.sort(state.compare.item);
|
|
144
|
+
result.exact = exact.slice(0, options.limit).map((item) => item.data);
|
|
145
|
+
included.sort(state.compare.ranked);
|
|
146
|
+
result.included = included.slice(0, options.limit - result.exact.length).map((item) => item.data);
|
|
147
|
+
const remaining = options.limit - result.exact.length - result.included.length;
|
|
148
|
+
if (remaining === 0) return result;
|
|
149
|
+
const keys = Object.keys(scored).map(Number).sort((first, second) => second - first);
|
|
150
|
+
const keysLength = keys.length;
|
|
151
|
+
if (keysLength === 0) return result;
|
|
152
|
+
const maxScore = keys[0];
|
|
153
|
+
outer: for (let index = 0; index < keysLength; index += 1) {
|
|
154
|
+
const key = keys[index];
|
|
155
|
+
if (maxScore - key > options.tolerance) break;
|
|
156
|
+
const keyed = scored[key];
|
|
157
|
+
const keyedLength = keyed.length;
|
|
158
|
+
keyed.sort(state.compare.item);
|
|
159
|
+
for (let keyedIndex = 0; keyedIndex < keyedLength; keyedIndex += 1) {
|
|
160
|
+
result.similar.push(keyed[keyedIndex].data);
|
|
161
|
+
if (result.similar.length >= remaining) break outer;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
function getScore(item, needle) {
|
|
167
|
+
const haystackLength = item.lowercase.length;
|
|
168
|
+
const needleLength = needle.length;
|
|
169
|
+
if (needleLength > haystackLength) return -1;
|
|
170
|
+
let best = -1;
|
|
171
|
+
let cursor = 0;
|
|
172
|
+
while (cursor < haystackLength) {
|
|
173
|
+
const end = getMatchEnd(item.lowercase, needle, cursor);
|
|
174
|
+
if (end === -1) break;
|
|
175
|
+
let index = end;
|
|
176
|
+
let needleIndex = needleLength - 1;
|
|
177
|
+
let next = -1;
|
|
178
|
+
let score = 0;
|
|
179
|
+
let start = end;
|
|
180
|
+
while (needleIndex >= 0) {
|
|
181
|
+
if (item.lowercase[index] === needle[needleIndex]) {
|
|
182
|
+
score += 1;
|
|
183
|
+
if (next !== -1) score += max([0, 5 - (next - index - 1)]);
|
|
184
|
+
if (isBoundary(item, index)) score += 2;
|
|
185
|
+
next = index;
|
|
186
|
+
start = index;
|
|
187
|
+
needleIndex -= 1;
|
|
97
188
|
}
|
|
98
|
-
|
|
99
|
-
needleIndex += 1;
|
|
189
|
+
index -= 1;
|
|
100
190
|
}
|
|
101
|
-
if (
|
|
191
|
+
if (start === 0) score += 1;
|
|
192
|
+
if (score > best) best = score;
|
|
193
|
+
cursor = start + 1;
|
|
102
194
|
}
|
|
103
|
-
|
|
104
|
-
return max([0,
|
|
195
|
+
if (best === -1) return -1;
|
|
196
|
+
return max([0, best - floor(haystackLength / 3)]);
|
|
197
|
+
}
|
|
198
|
+
function getStrings() {
|
|
199
|
+
return this[FUZZY_SYMBOL].items.original.map((item) => item.string.original);
|
|
105
200
|
}
|
|
106
201
|
function getTolerance(input, state) {
|
|
107
202
|
if (typeof input === "number" && !Number.isNaN(input)) return input < 0 ? 0 : floor(input);
|
|
108
203
|
return state?.tolerance ?? 5;
|
|
109
204
|
}
|
|
205
|
+
function isBoundary(string, index) {
|
|
206
|
+
if (index === 0 || FUZZY_BOUNDARY_SEPARATOR.test(string.lowercase[index - 1])) return true;
|
|
207
|
+
if (string.original.length !== string.lowercase.length) return false;
|
|
208
|
+
return string.original[index] !== string.lowercase[index] && string.original[index - 1] === string.lowercase[index - 1];
|
|
209
|
+
}
|
|
110
210
|
/**
|
|
111
211
|
* Is the value a fuzzy searcher?
|
|
112
212
|
*
|
|
@@ -118,74 +218,48 @@ function getTolerance(input, state) {
|
|
|
118
218
|
function isFuzzy(value) {
|
|
119
219
|
return typeof value === "object" && value !== null && "$fuzzy" in value && value["$fuzzy"] === true;
|
|
120
220
|
}
|
|
121
|
-
function isSubsequence(haystack, needle) {
|
|
122
|
-
const lowerCaseHaystack = lowerCase(haystack);
|
|
123
|
-
const lowerCaseNeedle = lowerCase(needle);
|
|
124
|
-
const haystackLength = lowerCaseHaystack.length;
|
|
125
|
-
const needleLength = lowerCaseNeedle.length;
|
|
126
|
-
let needleIndex = 0;
|
|
127
|
-
for (let haystackIndex = 0; haystackIndex < haystackLength; haystackIndex += 1) {
|
|
128
|
-
if (lowerCaseHaystack[haystackIndex] === lowerCaseNeedle[needleIndex]) needleIndex += 1;
|
|
129
|
-
if (needleIndex === needleLength) return true;
|
|
130
|
-
}
|
|
131
|
-
return false;
|
|
132
|
-
}
|
|
133
221
|
function search(input, option) {
|
|
134
222
|
const state = this[FUZZY_SYMBOL];
|
|
135
|
-
const options = option == null ? state :
|
|
136
|
-
const result = {
|
|
137
|
-
exact: [],
|
|
138
|
-
similar: []
|
|
139
|
-
};
|
|
223
|
+
const options = option == null ? state : createOptions(option, state);
|
|
140
224
|
const value = typeof input === "string" ? input.trim() : "";
|
|
225
|
+
const lowercaseValue = lowerCase(value);
|
|
141
226
|
if (value.length === 0) {
|
|
142
|
-
|
|
143
|
-
return
|
|
227
|
+
state.items.sorted ??= state.items.original.toSorted(state.compare.item);
|
|
228
|
+
return {
|
|
229
|
+
exact: state.items.sorted.slice(0, options.limit).map((item) => item.data),
|
|
230
|
+
included: [],
|
|
231
|
+
similar: []
|
|
232
|
+
};
|
|
144
233
|
}
|
|
145
|
-
let { length } = state.items;
|
|
146
234
|
const exact = [];
|
|
147
|
-
const
|
|
235
|
+
const included = [];
|
|
148
236
|
const scored = {};
|
|
237
|
+
const { length } = state.items.original;
|
|
149
238
|
for (let index = 0; index < length; index += 1) {
|
|
150
|
-
const item = state.items[index];
|
|
151
|
-
const
|
|
152
|
-
if (
|
|
153
|
-
exact.push(
|
|
154
|
-
item,
|
|
155
|
-
haystack
|
|
156
|
-
});
|
|
239
|
+
const item = state.items.original[index];
|
|
240
|
+
const { string } = item;
|
|
241
|
+
if (string.lowercase === lowercaseValue) {
|
|
242
|
+
exact.push(item);
|
|
157
243
|
continue;
|
|
158
244
|
}
|
|
159
|
-
const
|
|
245
|
+
const position = string.lowercase.indexOf(lowercaseValue);
|
|
246
|
+
if (position > -1) {
|
|
247
|
+
included.push(createRanked(item, lowercaseValue, position));
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
const score = getScore(string, lowercaseValue);
|
|
160
251
|
if (score > -1) {
|
|
161
252
|
scored[score] ??= [];
|
|
162
|
-
scored[score].push(
|
|
163
|
-
item,
|
|
164
|
-
haystack
|
|
165
|
-
});
|
|
253
|
+
scored[score].push(item);
|
|
166
254
|
}
|
|
167
255
|
}
|
|
168
|
-
|
|
169
|
-
length = keys.length;
|
|
170
|
-
if (length > 0 && options.tolerance > 0) {
|
|
171
|
-
const maxScore = keys[0];
|
|
172
|
-
for (let index = 0; index < length; index += 1) {
|
|
173
|
-
const key = keys[index];
|
|
174
|
-
if (maxScore - key > options.tolerance) break;
|
|
175
|
-
similar.push(...createFuzzyItems(scored[key]));
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
result.exact = createFuzzyItems(options.limit == null ? exact : exact.slice(0, options.limit));
|
|
179
|
-
if (options.limit == null) result.similar = similar;
|
|
180
|
-
else result.similar = similar.slice(0, options.limit - result.exact.length);
|
|
181
|
-
return result;
|
|
256
|
+
return getResult(state, exact, included, scored, options);
|
|
182
257
|
}
|
|
183
|
-
function
|
|
258
|
+
function setItems(value) {
|
|
184
259
|
if (!Array.isArray(value)) throw new TypeError(FUZZY_MESSAGE_ARRAY);
|
|
185
|
-
|
|
186
|
-
state.items = value.slice();
|
|
187
|
-
state.strings = value.map((item) => state.handler(item));
|
|
260
|
+
createItems(this[FUZZY_SYMBOL], value);
|
|
188
261
|
}
|
|
262
|
+
let defaultSorter;
|
|
189
263
|
fuzzy.is = isFuzzy;
|
|
190
264
|
fuzzy.match = fuzzyMatch;
|
|
191
265
|
//#endregion
|