github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/cmd/deck/static/prow/fuzzy-search.ts (about) 1 /* 2 Copyright 2018 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 export class FuzzySearch { 18 public dict: string[]; 19 20 constructor(dict: string[]) { 21 dict.sort(); 22 this.dict = dict; 23 } 24 25 /** 26 * Returns a list of string from dictionary that matches against the pattern. 27 */ 28 public search(pattern: string): string[] { 29 if (!this.dict || this.dict.length === 0) { 30 return []; 31 } 32 if (!pattern || pattern.length === 0) { 33 return this.dict; 34 } 35 const dictScr = this.dict 36 .filter((x) => this.basicMatch(pattern, x)) 37 .map((x) => ({score: this.getMaxScore(pattern, x), str: x})); 38 dictScr.sort((a, b) => { 39 if (a.score === b.score) { 40 return a.str < b.str ? -1 : (a.str > b.str ? 1 : 0); 41 } 42 return a.score > b.score ? -1 : 1; 43 }); 44 45 return dictScr.filter((x) => x.score !== 0).map((x) => x.str); 46 } 47 48 /** 49 * Sets the dictionary for the fuzzy search. 50 */ 51 public setDict(dict: string[]) { 52 dict.sort(); 53 this.dict = dict; 54 } 55 56 /** 57 * Returns true if the string contains all the pattern characters. 58 */ 59 private basicMatch(pttn: string, str: string): boolean { 60 let i = 0; 61 let j = 0; 62 while (i < pttn.length && j < str.length) { 63 if (pttn[i].toLowerCase() === str[j].toLowerCase()) { i += 1; } 64 j += 1; 65 } 66 return i === pttn.length; 67 } 68 69 /** 70 * Calculates the score that a matching can get. The string is calculated based on 4 71 * criteria: 72 * 1. +3 score for the matching that occurs near the beginning of the string. 73 * 2. +5 score for the matching that is not an alphabetical character. 74 * 3. +3 score for the matching that the string character is upper case. 75 * 4. +10 score for the matching that matches the uppercase which is just before a 76 * separator. 77 */ 78 private calcScore(i: number, str: string): number { 79 let score = 0; 80 const isAlphabetical = (c: number): boolean => { 81 return (c > 64 && c < 91) || (c > 96 && c < 123); 82 }; 83 // Bonus if the matching is near the start of the string 84 if (i < 3) { 85 score += 3; 86 } 87 // Bonus if the matching is not a alphabetical character 88 if (!isAlphabetical(str.charCodeAt(i))) { 89 score += 5; 90 } 91 // Bonus if the matching is an UpperCase character 92 if (str[i].toUpperCase() === str[i]) { 93 score += 3; 94 } 95 96 // Bonus if matching after a separator 97 const separatorBehind = (i === 0 || !isAlphabetical(str.charCodeAt(i - 1))); 98 if (separatorBehind && isAlphabetical(str.charCodeAt(i))) { 99 score += 10; 100 score += (str[i].toUpperCase() === str[i] ? 5 : 0); 101 } 102 return score; 103 } 104 105 /** 106 * Get maximum score that a string can get against the pattern. 107 */ 108 private getMaxScore(pttn: string, str: string): number { 109 // Rewards perfect match a value of Number.MAX_SAFE_INTEGER 110 if (pttn === str) { 111 return Number.MAX_SAFE_INTEGER; 112 } 113 114 let i = 0; 115 while (i < Math.min(pttn.length, str.length) && pttn[i] === str[i]) { 116 i++; 117 } 118 const streak = i; 119 120 const score: number[][] = []; 121 for (i = 0; i < 2; i++) { 122 score[i] = []; 123 for (let j = 0; j < str.length; j++) { 124 score[i][j] = 0; 125 } 126 } 127 128 for (i = 0; i < pttn.length; i++) { 129 const t = i % 2; 130 for (let j = 0; j < str.length; j++) { 131 let scoreVal = pttn[i].toLowerCase() === str[j].toLowerCase() ? 132 this.calcScore(j, str) : Number.MIN_SAFE_INTEGER; 133 if (streak > 4 && i === streak - 1 && j === streak - 1) { 134 scoreVal += 10 * streak; 135 } 136 if (i === 0) { 137 score[t][j] = scoreVal; 138 if (j > 0) { score[t][j] = Math.max(score[t][j], score[t][j - 1]); } 139 } else { 140 if (j > 0) { 141 score[t][j] = Math.max(score[t][j], score[t][j - 1]); 142 score[t][j] = Math.max(score[t][j], score[Math.abs(t - 1)][j - 1] + scoreVal); 143 } 144 } 145 } 146 } 147 return score[(pttn.length - 1) % 2][str.length - 1]; 148 } 149 }