-
-
Notifications
You must be signed in to change notification settings - Fork 41
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(plugin-inquirer): add fuzzy match str util funtion
link #22
- Loading branch information
Showing
1 changed file
with
29 additions
and
110 deletions.
There are no files selected for viewing
139 changes: 29 additions & 110 deletions
139
packages/@cz-git/plugin-inquirer/src/shared/utils/fuzzy.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,127 +1,46 @@ | ||
/** | ||
* Powered by Fuzzy | ||
* https://github.com/myork/fuzzy | ||
* | ||
* Copyright (c) 2012 Matt York | ||
* Licensed under the MIT license. | ||
* | ||
* @description: A standalone fuzzy search / fuzzy filter. provide inquirer usage | ||
* @description: provide list and checkBox fuzzy search | ||
* @author: @Zhengqbbb (zhengqbbb@gmail.com) | ||
* @license: MIT | ||
*/ | ||
|
||
import { FilterFucType, MatchOptions, MatchResult } from "../types"; | ||
|
||
/** | ||
* @description: If `pattern` matches `inputString`, wrap each matching character in `opts.pre` | ||
* and `opts.post`. If no match, return null. | ||
* @param {string} pattern inputString | ||
* @param {string} str targetString | ||
* @description: inputString match targetString return match score | ||
* @param {string} input input string | ||
* @param {string} target target string | ||
* @param {boolean} caseSensitive isCaseSensitive, default: false | ||
* @return {number | null} match score. if not match return null | ||
*/ | ||
export const fuzzyMatch = ( | ||
pattern: string, | ||
str: string, | ||
opts?: MatchOptions | ||
): MatchResult | null => { | ||
opts = opts || {}; | ||
const result = []; | ||
const len = str.length; | ||
// prefix | ||
const pre = opts.pre || ""; | ||
// suffix | ||
const post = opts.post || ""; | ||
// String to compare against. This might be a lowercase version of the | ||
// raw string | ||
const compareString = (opts.caseSensitive && str) || str.toLowerCase(); | ||
let patternIdx = 0, | ||
input: string, | ||
target: string, | ||
caseSensitive?: boolean | ||
): number | null => { | ||
if (typeof input !== "string" || typeof target !== "string") return null; | ||
const matchResult = []; | ||
const len = target.length; | ||
const shimTarget = (caseSensitive && target) || target.toLowerCase(); | ||
input = (caseSensitive && input) || input.toLowerCase(); | ||
let inputIndex = 0, | ||
totalScore = 0, | ||
currScore = 0, | ||
ch; | ||
|
||
pattern = (opts.caseSensitive && pattern) || pattern.toLowerCase(); | ||
|
||
// For each character in the string, either add it to the result | ||
// or wrap in template if it's the next string in the pattern | ||
currentScore = 0, | ||
currentChar; | ||
for (let idx = 0; idx < len; idx++) { | ||
ch = str[idx]; | ||
if (compareString[idx] === pattern[patternIdx]) { | ||
ch = pre + ch + post; | ||
patternIdx += 1; | ||
|
||
// consecutive characters should increase the score more than linearly | ||
currScore += 1 + currScore; | ||
currentChar = input[idx]; | ||
if (shimTarget[idx] === input[inputIndex]) { | ||
// consecutive matches will score higher | ||
inputIndex += 1; | ||
currentScore += 1 + currentScore; | ||
} else { | ||
currScore = 0; | ||
currentScore = 0; | ||
} | ||
totalScore += currScore; | ||
result[result.length] = ch; | ||
totalScore += currentScore; | ||
matchResult[matchResult.length] = currentChar; | ||
} | ||
|
||
// return rendered string if we have a match for every char | ||
if (patternIdx === pattern.length) { | ||
// if the string is an exact match with pattern, totalScore should be maxed | ||
totalScore = compareString === pattern ? Infinity : totalScore; | ||
return { rendered: result.join(""), score: totalScore }; | ||
if (inputIndex === input.length) { | ||
totalScore = shimTarget === input ? Infinity : totalScore; | ||
return totalScore; | ||
} | ||
|
||
return null; | ||
}; | ||
|
||
/** | ||
* @description: Does `pattern` fuzzy match `inputString`? | ||
* @param {string} pattern inputString | ||
* @param {string} str targetString | ||
* @return {boolean} isMatch | ||
*/ | ||
export const fuzzyTest = (pattern: string, str: string): boolean => { | ||
return fuzzyMatch(pattern, str) !== null; | ||
}; | ||
|
||
/** | ||
* @description: The normal entry point. Filters `arr` for matches against `pattern`. | ||
* @param {*} pattern inputString | ||
* @param {*} arr targetArray | ||
* @param {*} opts FilterOptions | ||
*/ | ||
export const fuzzyFilter: FilterFucType<any> = (pattern, arr, opts?) => { | ||
if (!arr || arr.length === 0) { | ||
return []; | ||
} | ||
if (typeof pattern !== "string") { | ||
return arr; | ||
} | ||
opts = opts || {}; | ||
return arr | ||
.reduce(function (prev, element, idx) { | ||
let str = element; | ||
if (opts?.extract) { | ||
str = opts?.extract(element); | ||
} | ||
const rendered = fuzzyMatch(pattern, str, opts); | ||
if (rendered != null) { | ||
prev[prev.length] = { | ||
string: rendered.rendered, | ||
score: rendered.score, | ||
index: idx, | ||
original: element | ||
}; | ||
} | ||
return prev; | ||
}, []) | ||
.sort(function (a: any, b: any) { | ||
const compare = b.score - a.score; | ||
if (compare) return compare; | ||
return a.index - b.index; | ||
}); | ||
}; | ||
|
||
/** | ||
* @description: Return all elements of `array` that have a fuzzy match against `pattern`. | ||
* @param {string} pattern inputString | ||
* @param {Array<string>} array targetArray | ||
*/ | ||
export const fuzzySimpleFilter = (pattern: string, array: string[]): string[] => { | ||
return array.filter(function (str) { | ||
return fuzzyTest(pattern, str); | ||
}); | ||
}; |