forked from raydium-io/raydium-ui-v3
-
Notifications
You must be signed in to change notification settings - Fork 0
/
searchItems.ts
149 lines (135 loc) · 5.19 KB
/
searchItems.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
import { isNumber, MayFunction } from '@raydium-io/raydium-sdk-v2'
import { isObject, isString } from './judges/judgeType'
import { shakeUndefindedItem } from './shakeUndefindedItem'
import { shrinkToValue } from './shrinkToValue'
type SearchConfigObj<T> = {
text: MayFunction<string | undefined, [item: T]>
entirely?: boolean
}
type SearchConfigItem<T> = SearchConfigObj<T> | string | undefined
type MatchInfo<T> = {
item: T
allConfigs: SearchConfigObj<T>[]
matchedConfigs: {
isEntirelyMatched: boolean
config: SearchConfigObj<T>
configIdx: number
searchedKeywordText: string
searchedKeywordIdx: number
}[]
}
export type SearchOptions<T> = {
searchText?: string /* for controlled component */
matchRules?: SearchConfigItem<T>[]
/**
* apply to searchText
* default is /\s+/ and '-'
**/
splitor?: string | RegExp
}
/**
* pure js fn/
* core of "search" feature
*/
export function searchItems<T>(items: T[], options?: SearchOptions<T>): T[] {
if (!options) return items
if (!options.searchText) return items
const allMatchedStatusInfos = items.map((item) => parseToMatchedInfos(item, options))
const meaningfulMatchedInfos = allMatchedStatusInfos.filter((m) => m?.matchedConfigs.length)
const sortedMatchedInfos = sortByMatchedInfos<T>(meaningfulMatchedInfos)
return sortedMatchedInfos.map((m) => m.item)
}
function extractItemBeSearchedText<T>(item: T): SearchConfigObj<T>[] {
if (isString(item) || isNumber(item)) return [{ text: String(item as unknown as string | number) } as SearchConfigObj<T>]
if (isObject(item)) {
return shakeUndefindedItem(
Object.values({ ...item, id: undefined, key: undefined }).map((value) =>
isString(value) || isNumber(value) ? ({ text: String(value as unknown as string | number) } as SearchConfigObj<T>) : undefined
)
)
}
return [{ text: '' }]
}
function parseToMatchedInfos<T>(item: T, options: SearchOptions<T>) {
const searchText = options.searchText
const matchRules = options.matchRules ?? extractItemBeSearchedText(item)
const splitor = options.splitor ?? /\s+|-/
const searchKeyWords = searchText!.trim().split(splitor)
const searchConfigs = shakeUndefindedItem(
matchRules.map((rule) => (isString(rule) ? { text: rule } : rule) as SearchConfigObj<T> | undefined)
)
return patch({ item, searchKeyWords, searchConfigs })
}
/** it produce matched search config infos */
function patch<T>(options: { item: T; searchKeyWords: string[]; searchConfigs: SearchConfigObj<T>[] }): MatchInfo<T> {
const matchedConfigs = options.searchKeyWords.flatMap((keyword, keywordIdx) =>
shakeUndefindedItem(
options.searchConfigs.map((config, configIdx) => {
const configIsEntirely = config.entirely
const text = shrinkToValue(config.text, [options.item])
const matchEntirely = isStringInsensitivelyEqual(text, keyword)
const matchPartial = isStringInsensitivelyContain(text, keyword)
const isMatchedByConfig = (matchEntirely && configIsEntirely) || (matchPartial && !configIsEntirely)
return isMatchedByConfig
? {
config,
configIdx,
isEntirelyMatched: matchEntirely,
searchedKeywordIdx: keywordIdx,
searchedKeywordText: keyword
}
: undefined
})
)
)
const matchInfos: MatchInfo<T> = {
item: options.item,
allConfigs: options.searchConfigs,
matchedConfigs
}
return matchInfos
}
function sortByMatchedInfos<T>(matchedInfos: MatchInfo<T>[]) {
return [...matchedInfos].sort(
(matchedInfoA, matchedInfoB) => toMatchedStatusSignature(matchedInfoB) - toMatchedStatusSignature(matchedInfoA)
)
}
function isStringInsensitivelyEqual(s1: string | undefined, s2: string | undefined) {
if (s1 == null || s2 == null) return false
return s1.toLowerCase() === s2.toLowerCase()
}
function isStringInsensitivelyContain(s1: string | undefined, s2: string | undefined) {
if (s1 == null || s2 == null) return false
return s1.toLowerCase().includes(s2.toLowerCase())
}
/**
* so user can compare just use return number
*
* matchedInfo => [0, 1, 2, 0, 2, 1] => [ 2 * 4 + 2 * 2, 1 * 5 + 1 * 1] (index is weight) =>
* 2 - entirely mathched
* 1 - partialy matched
* 0 - not matched
*
* @returns item's weight number
*/
function toMatchedStatusSignature<T>(matchedInfo: MatchInfo<T>): number {
const originalConfigs = matchedInfo.allConfigs
const entriesSequence = Array.from({ length: originalConfigs.length }, () => 0)
const partialSequence = Array.from({ length: originalConfigs.length }, () => 0)
matchedInfo.matchedConfigs.forEach(({ configIdx, isEntirelyMatched }) => {
if (isEntirelyMatched) {
entriesSequence[configIdx] = 2 // [0, 0, 2, 0, 2, 0]
} else {
partialSequence[configIdx] = 1 // [0, 1, 0, 0, 2, 1]
}
})
const calcCharateristicN = (sequence: number[]) => {
const max = Math.max(...sequence)
return sequence.reduce((acc, currentValue, currentIdx) => acc + currentValue * (max + 1) ** (sequence.length - currentIdx), 0)
}
const characteristicSequence = calcCharateristicN([
calcCharateristicN(entriesSequence),
calcCharateristicN(partialSequence) // 1 * 5 + 1 * 1
])
return characteristicSequence
}