@@ -8,11 +8,12 @@ const GROUP_END = ')'
88const EMPTY_QUOTES_STR = '""'
99const KEY_SEPARATOR = '.'
1010const NEGATED_PREFIX = 'not'
11- const RANGE_REGEXP = / ^ [ - \D ] * ( - ? \d + ( \. \d + ) ? ) ? [ - \D ] * - [ - \D ] * ( - ? \d + ( \. \d + ) ? ) ? [ - \D ] * $ /
11+ const RANGE_REGEXP = / ^ [ ^ - \d ] * ( - ? \d + ( \. \d + ) ? ) ? [ ^ - \d ] * - [ ^ - \d ] * ( - ? \d + ( \. \d + ) ? ) ? [ ^ - \d ] * $ /
1212const TOKENIZER = new RegExp ( ` *(${ NEGATED_PREFIX } )? *(\\${ GROUP_START } )| *(${ NEGATED_PREFIX } +)?(?:((?:\\\\.|[^ ${ GROUP_START } ${ GROUP_END } \\\\${ REGEX_CHAR } ${ RANGE_CHAR } ${ TOKEN_SEPARATOR } ])+) *([${ REGEX_CHAR } ${ RANGE_CHAR } ]?${ TOKEN_SEPARATOR } ))? *("((?:\\\\.|[^"\\\\])+)"|(?:\\\\.|[^ ${ GROUP_START } ${ GROUP_END } \\\\])+)? *(and|or|\\${ GROUP_END } |$)` , 'g' )
1313const TOKEN = { GROUP_NEGATED : 1 , GROUP_START : 2 , NEGATED : 3 , KEY : 4 , TYPE : 5 , VALUE : 6 , QUOTED_VALUE : 7 , OPERATOR : 8 }
1414const UNKNOWN = - 1
1515const EMPTY_ARR : any [ ] = [ ]
16+ const EMPTY_OBJ : any = { }
1617const EMPTY_STR = ''
1718const STRING = 'string'
1819const NUMBER = 'number'
@@ -59,19 +60,122 @@ namespace Operator {
5960 }
6061}
6162
63+ interface SearchOptions {
64+ /** Array of keys to exclude from search. */
65+ excludeKeys ?: string [ ] ,
66+ /** Whether to consider numeric strings in the range search. Disabling improves range search performance. Default is true. */
67+ allowNumericString ?: boolean
68+ /** Whether to match keys and values when no quotes are used in the query and no value is provided. Disabling improves key search performance. Default is true.
69+ * @example
70+ * The query "foo" (no quotes), will match "foo: anyValue" and "anyField: foo".
71+ * The query "foo:bar" has a value and will not be affected by this option.
72+ */
73+ allowKeyValueMatching ?: boolean
74+ }
6275
6376/**
64- * Search through an array of objects using a powerful query syntax
65- *
66- * @param objList - Array of objects to search
67- * @param queryStr - Query string (e.g. "name:john and age~:20-30")
68- * @param exclude - Array of keys to exclude from search
69- * @returns Array of matched objects
77+ * SearchEngine class provides methods to search through an array of objects using a query syntax.
78+ * The query syntax allows for complex searches including conditions, negations, and grouping.
7079 */
71- function search < T extends Record < string , any > > ( objList : T [ ] , queryStr : string , exclude ?: string [ ] ) : T [ ] {
72- if ( ! objList ) { return [ ] }
73- if ( ! queryStr || queryStr . trim ( ) === EMPTY_STR ) { return objList . slice ( ) }
74- return [ ...evaluateGroup ( new Set ( objList ) , extractConditionsFromQuery ( queryStr . toLowerCase ( ) ) , exclude ) ]
80+ class SearchEngine {
81+ #options: SearchOptions
82+
83+ /**
84+ * Creates a new instance of SearchEngine with the specified options.
85+ * @param options - Search options
86+ */
87+ constructor ( options : SearchOptions = { } ) {
88+ options . allowNumericString === void 0 && ( options . allowNumericString = true )
89+ options . allowKeyValueMatching === void 0 && ( options . allowKeyValueMatching = true )
90+ this . #options = options
91+ }
92+
93+ /**
94+ * Search through an array of objects using the query syntax.
95+ *
96+ * @param objList - Array of objects to search
97+ * @param queryStr - Query string (e.g. "name:john and age~:20-30")
98+ * @returns Array of matched objects
99+ */
100+ search < T extends Record < string , any > > ( objList : T [ ] , queryStr : string ) : T [ ] {
101+ if ( ! objList ) { return [ ] }
102+ if ( ! queryStr || queryStr . trim ( ) === EMPTY_STR ) { return objList . slice ( ) }
103+ return [ ...this . #evaluateGroup( new Set ( objList ) , extractConditionsFromQuery ( queryStr . toLowerCase ( ) ) ) ]
104+ }
105+
106+ /**
107+ * Search through an array of objects using the query syntax.
108+ *
109+ * @param objList - Array of objects to search
110+ * @param queryStr - Query string (e.g. "name:john and age~:20-30")
111+ * @param options - Search options
112+ * @returns Array of matched objects
113+ */
114+ static search < T extends Record < string , any > > ( objList : T [ ] , queryStr : string , options : SearchOptions = EMPTY_OBJ ) : T [ ] {
115+ return ( new SearchEngine ( options ) ) . search ( objList , queryStr )
116+ }
117+
118+ #evaluateGroup< T > ( objList : Set < T > , group : GroupQuery ) : Set < T > {
119+ if ( group . conditions . length === 0 ) { return group . negated ? new Set ( ) : objList }
120+
121+ let currentResults = this . #evaluateCondition( objList , group . conditions [ 0 ] )
122+
123+ for ( let i = 1 ; i < group . conditions . length ; i ++ ) {
124+ const condition = group . conditions [ i ]
125+ const previousOperator = group . conditions [ i - 1 ] . operator
126+
127+ if ( previousOperator && previousOperator === Operator . OR ) {
128+ const nextResults = this . #evaluateCondition( objList , condition )
129+ nextResults . forEach ( item => currentResults . add ( item ) )
130+ } else {
131+ currentResults = this . #evaluateCondition( currentResults , condition )
132+ }
133+ }
134+
135+ if ( ! group . negated ) { return currentResults }
136+
137+ // If the group is negated, return everything except the group results
138+ const negatedResult = new Set < T > ( )
139+ for ( const obj of objList ) { currentResults . has ( obj ) || negatedResult . add ( obj ) }
140+ return negatedResult
141+ }
142+
143+ #evaluateCondition< T > ( objList : Set < T > , condition : Query | GroupQuery ) : Set < T > {
144+ if ( 'conditions' in condition ) { return this . #evaluateGroup( objList , condition ) }
145+
146+ const resultSet = new Set < T > ( )
147+ objList . forEach ( obj => {
148+ if ( condition . negated !== this . #findQuery( obj , condition , EMPTY_STR ) ) {
149+ resultSet . add ( obj )
150+ }
151+ } )
152+ return resultSet
153+ }
154+
155+ #findQuery( obj : any , query : Query , nestedKeys : string , keyFound ?: boolean ) : boolean {
156+ const keys = getObjectKeys ( obj )
157+ nestedKeys += KEY_SEPARATOR
158+ for ( const key of keys ) {
159+ const newNestedKeys = nestedKeys + key . toLowerCase ( )
160+
161+ if ( isExcluded ( newNestedKeys , this . #options. excludeKeys ) ) { continue }
162+
163+ if ( keyFound === void 0 ) {
164+ if ( newNestedKeys . indexOf ( query . key ) === UNKNOWN ) {
165+ if ( this . #findQuery( obj [ key ] , query , newNestedKeys ) ) { return true }
166+ if ( this . #options. allowKeyValueMatching && query . value === void 0 && match ( query . key , obj [ key ] , query . type , this . #options) ) { return true }
167+ continue
168+ }
169+
170+ if ( query . value === void 0 ) { return true }
171+ }
172+
173+ if ( match ( query . value , obj [ key ] , query . type , this . #options) || this . #findQuery( obj [ key ] , query , newNestedKeys , true ) ) {
174+ return true
175+ }
176+ }
177+ return false
178+ }
75179}
76180
77181function extractConditionsFromQuery ( query : string , regex = new RegExp ( TOKENIZER ) , group = new GroupQuery ( ) ) : GroupQuery {
@@ -134,7 +238,9 @@ function getQuery(negated: boolean, type?: string, key?: string, value?: string)
134238 return query
135239 }
136240
137- query . value = { min : parseFloat ( matches [ 1 ] ) || void 0 , max : parseFloat ( matches [ 3 ] ) || void 0 }
241+ query . value = { min : parseFloat ( matches [ 1 ] ) , max : parseFloat ( matches [ 3 ] ) }
242+ ! query . value . min && query . value . min !== 0 && delete query . value . min
243+ ! query . value . max && query . value . max !== 0 && delete query . value . max
138244
139245 if ( query . value . min === void 0 && query . value . max === void 0 ) {
140246 delete query . type
@@ -144,77 +250,14 @@ function getQuery(negated: boolean, type?: string, key?: string, value?: string)
144250 return query
145251}
146252
147- function evaluateGroup < T > ( objList : Set < T > , group : GroupQuery , exclude ?: string [ ] ) : Set < T > {
148- if ( group . conditions . length === 0 ) { return group . negated ? new Set ( ) : objList }
149-
150- let currentResults = evaluateCondition ( objList , group . conditions [ 0 ] , exclude )
151-
152- for ( let i = 1 ; i < group . conditions . length ; i ++ ) {
153- const condition = group . conditions [ i ]
154- const previousOperator = group . conditions [ i - 1 ] . operator
155-
156- if ( previousOperator && previousOperator === Operator . OR ) {
157- const nextResults = evaluateCondition ( objList , condition , exclude )
158- nextResults . forEach ( item => currentResults . add ( item ) )
159- } else {
160- currentResults = evaluateCondition ( currentResults , condition , exclude )
161- }
162- }
163-
164- if ( ! group . negated ) { return currentResults }
165-
166- // If the group is negated, return everything except the group results
167- const negatedResult = new Set < T > ( )
168- for ( const obj of objList ) { currentResults . has ( obj ) || negatedResult . add ( obj ) }
169- return negatedResult
170- }
171-
172- function evaluateCondition < T > ( objList : Set < T > , condition : Query | GroupQuery , exclude ?: string [ ] ) : Set < T > {
173- if ( 'conditions' in condition ) { return evaluateGroup ( objList , condition , exclude ) }
174-
175- const resultSet = new Set < T > ( )
176- objList . forEach ( obj => {
177- if ( condition . negated !== findQuery ( obj , condition , EMPTY_STR , exclude ) ) {
178- resultSet . add ( obj )
179- }
180- } )
181- return resultSet
182- }
183-
184- function findQuery ( obj : any , query : Query , nestedKeys : string , excludedKeys ?: string [ ] , keyFound ?: boolean ) : boolean {
185- const keys = getObjectKeys ( obj )
186- nestedKeys += KEY_SEPARATOR
187- for ( const key of keys ) {
188- const newNestedKeys = nestedKeys + key . toLowerCase ( )
189-
190- if ( isExcluded ( newNestedKeys , excludedKeys ) ) { continue }
191-
192- if ( keyFound === void 0 ) {
193- if ( newNestedKeys . indexOf ( query . key ! ) === UNKNOWN ) {
194- if ( findQuery ( obj [ key ] , query , newNestedKeys , excludedKeys ) ) {
195- return true
196- }
197- continue
198- }
199-
200- if ( query . value === void 0 ) { return true }
201- }
202-
203- if ( match ( query . value , obj [ key ] , query . type ) || findQuery ( obj [ key ] , query , newNestedKeys , excludedKeys , true ) ) {
204- return true
205- }
206- }
207- return false
208- }
209-
210- function match ( expectedValue : any , value : any , type ?: string ) : boolean {
253+ function match ( expectedValue : any , value : any , type : string , options : SearchOptions ) : boolean {
211254 if ( value === null || value === void 0 ) { return false }
212255
213256 const typeOf = typeof value
214257
215258 if ( type === RANGE_CHAR ) {
216- if ( typeOf !== NUMBER && typeOf !== BIGINT ) { return false }
217- return matchRange ( expectedValue as Range , Number ( value ) )
259+ if ( typeOf !== NUMBER && typeOf !== BIGINT && ! ( options . allowNumericString && typeOf === STRING && ! isNaN ( value = + value ) ) ) { return false }
260+ return matchRange ( expectedValue as Range , value )
218261 }
219262
220263 if ( type === REGEX_CHAR ) { return ( expectedValue as RegExp ) . test ( value ) }
@@ -250,4 +293,4 @@ function removeEscapeChar(str?: string): string | void {
250293 return str ? str . replace ( / \\ ( .) / g, '$1' ) : str
251294}
252295
253- export { search }
296+ export default SearchEngine
0 commit comments