Skip to content

Commit

Permalink
feat(filters): add support for regex match mode
Browse files Browse the repository at this point in the history
  • Loading branch information
ChronicStone committed Jul 17, 2024
1 parent 0565978 commit 8ff9c13
Show file tree
Hide file tree
Showing 7 changed files with 357 additions and 14 deletions.
1 change: 1 addition & 0 deletions docs/features/filtering.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,4 +174,5 @@ ArrayQuery supports various match modes for different types of comparisons. Each
- 'lessThanOrEqual'
- 'exists'
- 'arrayLength'
- 'regex'
- 'objectMatch'
160 changes: 160 additions & 0 deletions docs/filter-match-modes/regex.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# Regex Match Mode

The regex match mode allows you to filter data using regular expressions. This powerful feature enables complex pattern matching across your dataset.

## Basic Usage

To use the regex match mode, set the `matchMode` to `'regex'` in your filter configuration:

```ts twoslash
import { query } from '@chronicstone/array-query'

const data = [
{ id: 1, email: '[email protected]' },
{ id: 2, email: '[email protected]' },
{ id: 3, email: '[email protected]' }
]

const result = query(data, {
filter: [
{ key: 'email', matchMode: 'regex', value: '@example\\.(com|org)' }
]
})
```

This query will return all items where the email matches the pattern `@example.com` or `@example.org`.

## Flags

You can modify the behavior of the regex matching by providing flags. Flags are specified using the `params` option:

```ts twoslash
import { query } from '@chronicstone/array-query'

const data = [
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'jane smith' },
{ id: 3, name: 'Bob Johnson' }
]

const result = query(data, {
filter: [
{
key: 'name',
matchMode: 'regex',
value: '^j.*',
params: { flags: 'i' }
}
]
})
```

## Raw RegExp

Instead of defining the regex pattern as a string, you can also pass a regular expression object directly:

```ts twoslash
import { query } from '@chronicstone/array-query'

const data = [
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'jane smith' },
{ id: 3, name: 'Bob Johnson' }
]

const result = query(data, {
filter: [
{
key: 'name',
matchMode: 'regex',
value: /^j.*/i
}
]
})
```

For a detailed explanation of available flags and their usage, please refer to the [MDN documentation on Regular Expression Flags](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions#advanced_searching_with_flags).

## Examples

### Case-insensitive Matching

```ts twoslash
import { query } from '@chronicstone/array-query'

const data = [
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'jane smith' },
{ id: 3, name: 'Bob Johnson' }
]

const result = query(data, {
filter: [
{
key: 'name',
matchMode: 'regex',
value: '^j.*',
params: { flags: 'i' }
}
]
})
```

This will match both "John Doe" and "jane smith".

### Multi-line Matching

```ts twoslash
import { query } from '@chronicstone/array-query'

const data = [
{ id: 1, description: 'First line\nSecond line' },
{ id: 2, description: 'One line only' },
{ id: 3, description: 'First line\nLast line' }
]

const result = query(data, {
filter: [
{
key: 'description',
matchMode: 'regex',
value: '^Last',
params: { flags: 'm' }
}
]
})
```

This will match the item with id 3, where "Last" appears at the start of a line.

## Combining with Other Filters

You can combine regex filters with other filter types:

```typescript
const result = query(data, {
filter: [
{
key: 'name',
matchMode: 'regex',
value: '^j.*',
params: { flags: 'i' }
},
{ key: 'age', matchMode: 'greaterThan', value: 28 }
]
})
```

This will find items where the name starts with 'j' (case-insensitive) AND the age is greater than 28.

## Performance Considerations

While regex matching is powerful, it can be computationally expensive, especially on large datasets or with complex patterns. Use it judiciously and consider performance implications in your use case.

## Escaping Special Characters

Remember to properly escape special regex characters in your pattern strings. For example, to match a literal period, use `\\.` instead of `.`.

## Further Reading

For more information on JavaScript regular expressions, including pattern syntax and usage, refer to the [MDN Regular Expressions Guide](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions).
10 changes: 10 additions & 0 deletions src/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,16 @@ export function query<T extends GenericObject, P extends QueryParams<T>>(
})
}

if (filter.matchMode === 'regex') {
return processFilterWithLookup({
type: 'regex',
params: filter?.params ?? null,
operator,
value,
filter: filter.value,
})
}

if (filter.matchMode === 'objectMatch') {
const params = typeof filter.params === 'function' ? filter.params(filter.value) : filter.params
const filterValue = params?.transformFilterValue?.(filter.value) ?? filter.value
Expand Down
34 changes: 21 additions & 13 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,20 @@ export type FilterMatchMode =
| 'lessThan'
| 'lessThanOrEqual'
| 'exists'
| 'regex'
| 'arrayLength'
| 'objectMatch'

export type NonObjectMatchMode = Exclude<FilterMatchMode, 'objectMatch'>
export type ComparatorMatchMode = Extract<FilterMatchMode, 'between' | 'greaterThan' | 'greaterThanOrEqual' | 'lessThan' | 'lessThanOrEqual'>

export type RegexMatchMode = Extract<FilterMatchMode, 'regex'>
export interface ComparatorParams {
dateMode?: boolean
}

export interface RegexParams {
flags?: string
}
export interface ObjectMapFilterParams {
operator: 'AND' | 'OR'
properties: Array<{
Expand All @@ -59,10 +63,13 @@ export interface ObjectMapFilterParams {
}

export type MatchModeCore = ({
matchMode: Exclude<FilterMatchMode, 'objectStringMap' | 'objectMatch' | ComparatorMatchMode>
matchMode: Exclude<FilterMatchMode, RegexMatchMode | 'objectMatch' | ComparatorMatchMode>
} | {
matchMode: ComparatorMatchMode
params?: ComparatorParams
} | {
matchMode: 'regex'
params?: RegexParams
} | {
matchMode: 'objectMatch'
params: ObjectMapFilterParams | ((value: any) => ObjectMapFilterParams)
Expand Down Expand Up @@ -108,15 +115,16 @@ export interface QueryParams<
export type QueryResult<T extends GenericObject, P extends QueryParams<T>> = P extends { limit: number } ? { totalRows: number, totalPages: number, rows: T[], unpaginatedRows: T[] } : { rows: T[] }

export interface MatchModeProcessorMap {
equals: ({ value, filter }: { value: any, filter: any }) => boolean
notEquals: ({ value, filter }: { value: any, filter: any }) => boolean
exists: ({ value, filter }: { value: any, filter: any }) => boolean
contains: ({ value, filter }: { value: any, filter: any }) => boolean
greaterThan: ({ value, filter }: { value: any, filter: any, params?: ComparatorParams }) => boolean
greaterThanOrEqual: ({ value, filter }: { value: any, filter: any, params?: ComparatorParams }) => boolean
lessThan: ({ value, filter }: { value: any, filter: any, params?: ComparatorParams }) => boolean
lessThanOrEqual: ({ value, filter }: { value: any, filter: any, params?: ComparatorParams }) => boolean
between: ({ value, filter }: { value: any, filter: any, params?: ComparatorParams }) => boolean
arrayLength: ({ value, filter }: { value: any, filter: any }) => boolean
objectMatch: ({ value, filter, params }: { value: any, filter: any, params: ObjectMapFilterParams, index?: number }) => boolean
equals: (f: { value: any, filter: any }) => boolean
notEquals: (f: { value: any, filter: any }) => boolean
exists: (f: { value: any, filter: any }) => boolean
contains: (f: { value: any, filter: any }) => boolean
greaterThan: (f: { value: any, filter: any, params?: ComparatorParams }) => boolean
greaterThanOrEqual: (f: { value: any, filter: any, params?: ComparatorParams }) => boolean
lessThan: (f: { value: any, filter: any, params?: ComparatorParams }) => boolean
lessThanOrEqual: (f: { value: any, filter: any, params?: ComparatorParams }) => boolean
between: (f: { value: any, filter: any, params?: ComparatorParams }) => boolean
arrayLength: (f: { value: any, filter: any }) => boolean
objectMatch: (f: { value: any, filter: any, params: ObjectMapFilterParams, index?: number }) => boolean
regex: (f: { value: any, filter: any, params?: RegexParams }) => boolean
}
2 changes: 2 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ export const MatchModeProcessor: MatchModeProcessorMap = {
between: ({ value, filter, params }) => {
return params?.dateMode ? new Date(value) >= new Date(filter[0]) && new Date(value) <= new Date(filter[1]) : value >= filter[0] && value <= filter[1]
},
regex: ({ value, filter, params }) =>
typeof value === 'string' && new RegExp(filter, params?.flags ?? '').test(value),
arrayLength: ({ value, filter }) => Array.isArray(value) && value.length === filter,
objectMatch: ({ value, filter, params, index }) => {
const properties = typeof index !== 'undefined' && params.matchPropertyAtIndex ? [params.properties[index]] : params.properties
Expand Down
Loading

0 comments on commit 8ff9c13

Please sign in to comment.