diff --git a/README.md b/README.md index 8433f0c..4503d7f 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Try it out on the online playground: ## Features -- Small: just `4.0 kB` when minified and gzipped! The JSON query engine without parse/stringify is only `2.0 kB`. +- Small: just `4.1 kB` when minified and gzipped! The JSON query engine without parse/stringify is only `2.1 kB`. - Feature rich (50+ powerful functions and operators) - Easy to interoperate with thanks to the intermediate JSON format. - Expressive diff --git a/src/functions.ts b/src/functions.ts index 72af077..801d2db 100644 --- a/src/functions.ts +++ b/src/functions.ts @@ -333,6 +333,23 @@ export const functions: FunctionBuildersMap = { return (data: unknown) => regex.test(getter(data) as string) }, + match: (path: JSONQuery, expression: string, options?: string) => { + const regex = new RegExp(expression, options) + const getter = compile(path) + + return (data: unknown) => { + const result = (getter(data) as string).match(regex) + return result ? matchToJSON(result) : null + } + }, + + matchAll: (path: JSONQuery, expression: string, options?: string) => { + const regex = new RegExp(expression, `${options ?? ''}g`) + const getter = compile(path) + + return (data: unknown) => Array.from((getter(data) as string).matchAll(regex)).map(matchToJSON) + }, + eq: buildFunction(isEqual), gt: buildFunction(gt), gte: buildFunction(gte), @@ -374,6 +391,17 @@ const reduce = (data: T[], callback: (previousValue: T, currentValue: T) => T return data.reduce(callback) } +const matchToJSON = (result: RegExpMatchArray) => { + const [value, ...groups] = result + const namedGroups = result.groups + + return groups.length + ? namedGroups + ? { value, groups, namedGroups } + : { value, groups } + : { value } +} + const throwArrayExpected = () => { throwTypeError('Array expected') } diff --git a/test-suite/compile.test.json b/test-suite/compile.test.json index 208ccf0..f540af2 100644 --- a/test-suite/compile.test.json +++ b/test-suite/compile.test.json @@ -2011,6 +2011,155 @@ "input": null, "query": ["regex", "Joe", "^[A-z]+$"], "output": true + }, + { + "input": null, + "query": ["regex", "2025", "^[A-z]+$"], + "output": false + } + ] + }, + { + "category": "match", + "description": "should extract a regular expression match from a string", + "tests": [ + { + "input": null, + "query": ["match", "Hello World!", "[A-z]+"], + "output": { "value": "Hello" } + }, + { + "input": null, + "query": ["match", "2025-11-05", "[A-z]+"], + "output": null + }, + { + "input": null, + "query": ["match", "Hello World!", "([A-Z])([a-z]+)"], + "output": { + "value": "Hello", + "groups": ["H", "ello"] + } + } + ] + }, + { + "category": "match", + "description": "should extract a regular expression match with groups from a string", + "tests": [ + { + "input": null, + "query": [ + "match", + "I'm on holiday from 2025-07-18 till 2025-08-01", + "(?\\d{4})-(?\\d{2})-(?\\d{2})" + ], + "output": { + "value": "2025-07-18", + "groups": ["2025", "07", "18"], + "namedGroups": { "year": "2025", "month": "07", "date": "18" } + } + }, + { + "input": null, + "query": ["match", "Hello World!", "(?\\d{4})-(?\\d{2})-(?\\d{2})"], + "output": null + } + ] + }, + { + "category": "match", + "description": "should extract a regular expression match with flags from a string", + "tests": [ + { + "input": null, + "query": ["match", "Hello World!", "world", ""], + "output": null + }, + { + "input": null, + "query": ["match", "Hello World!", "world!", "i"], + "output": { "value": "World!" } + }, + { + "input": null, + "query": ["match", "Hello World!", "(?world)!", "i"], + "output": { + "value": "World!", + "groups": ["World"], + "namedGroups": { "group1": "World" } + } + } + ] + }, + { + "category": "matchAll", + "description": "should extract all regular expression matches from a string", + "tests": [ + { + "input": null, + "query": ["matchAll", "Hello World!", "[A-z]+"], + "output": [{ "value": "Hello" }, { "value": "World" }] + }, + { + "input": null, + "query": ["matchAll", "2025-05-11", "[A-z]+"], + "output": [] + }, + { + "input": null, + "query": ["matchAll", "Hello World!", "([A-Z])([a-z]+)"], + "output": [ + { "value": "Hello", "groups": ["H", "ello"] }, + { "value": "World", "groups": ["W", "orld"] } + ] + } + ] + }, + { + "category": "matchAll", + "description": "should extract all regular expression matches with groups from a string", + "tests": [ + { + "input": null, + "query": [ + "matchAll", + "I'm on holiday from 2025-07-18 till 2025-08-01", + "(?\\d{4})-(?\\d{2})-(?\\d{2})" + ], + "output": [ + { + "value": "2025-07-18", + "groups": ["2025", "07", "18"], + "namedGroups": { "year": "2025", "month": "07", "date": "18" } + }, + { + "value": "2025-08-01", + "groups": ["2025", "08", "01"], + "namedGroups": { "year": "2025", "month": "08", "date": "01" } + } + ] + }, + { + "input": null, + "query": ["matchAll", "Hello World!", "(?\\d{4})-(?\\d{2})-(?\\d{2})"], + "output": [] + } + ] + }, + { + "category": "matchAll", + "description": "should extract all regular expression matches with a flag from a string", + "tests": [ + { + "input": null, + "query": ["matchAll", "Hello World!", "\\b[a-z]+\\b", ""], + "output": [] + }, + { + "input": null, + "query": ["matchAll", "Hello World!", "\\b[a-z]+\\b", "i"], + "output": [{ "value": "Hello" }, { "value": "World" }] } ] },