A RetroSearch Logo

Home - News ( United States | United Kingdom | Italy | Germany ) - Football scores

Search Query:

Showing content from https://github.com/jsonquerylang/jsonquery/commit/0d39aebab5ec1eeb5c343d9dea387cf8f8bd0e0b below:

implement support for operator precedence (#17) · jsonquerylang/jsonquery@0d39aeb · GitHub

File tree Expand file treeCollapse file tree 22 files changed

+729

-190

lines changed

Filter options

Expand file treeCollapse file tree 22 files changed

+729

-190

lines changed Original file line number Diff line number Diff line change

@@ -1,6 +1,6 @@

1 1

The ISC License

2 2 3 -

Copyright (c) 2024 by Jos de Jong

3 +

Copyright (c) 2024-2025 by Jos de Jong

4 4 5 5

Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.

6 6 Original file line number Diff line number Diff line change

@@ -10,7 +10,7 @@ Try it out on the online playground: <https://jsonquerylang.org>

10 10 11 11

## Features

12 12 13 -

- Small: just `3.3 kB` when minified and gzipped! The JSON query engine without parse/stringify is only `1.7 kB`.

13 +

- Small: just `3.7 kB` when minified and gzipped! The JSON query engine without parse/stringify is only `1.7 kB`.

14 14

- Feature rich (50+ powerful functions and operators)

15 15

- Easy to interoperate with thanks to the intermediate JSON format.

16 16

- Expressive

@@ -152,22 +152,42 @@ Here:

152 152

}

153 153

```

154 154 155 -

You can have a look at the source code of the functions in `/src/functions.ts` for more examples.

156 -

- `operators` is an optional map with operators, for example `{ eq: '==' }`. The defined operators can be used in a text query. Only operators with both a left and right hand side are supported, like `a == b`. They can only be executed when there is a corresponding function. For example:

155 +

You can have a look at the source code of the functions in [`/src/functions.ts`](/src/functions.ts) for more examples.

156 + 157 +

- `operators` is an optional array definitions for custom operators. Each definition describes the new operator, the name of the function that it maps to, and the desired precedence of the operator: the same, before, or after one of the existing operators (`at`, `before`, or `after`):

158 + 159 +

```ts

160 +

type CustomOperator =

161 +

| { name: string; op: string; at: string; vararg?: boolean, leftAssociative?: boolean }

162 +

| { name: string; op: string; after: string; vararg?: boolean, leftAssociative?: boolean }

163 +

| { name: string; op: string; before: string; vararg?: boolean, leftAssociative?: boolean }

164 +

```

165 + 166 +

The defined operators can be used in a text query. Only operators with both a left and right hand side are supported, like `a == b`. They can only be executed when there is a corresponding function. For example:

157 167 158 168

```js

159 -

import { buildFunction } from 'jsonquery'

160 - 169 +

import { buildFunction } from '@jsonquerylang/jsonquery'

170 + 161 171

const options = {

162 -

operators: {

163 -

notEqual: '<>'

164 -

},

172 +

// Define a new function "notEqual".

165 173

functions: {

166 174

notEqual: buildFunction((a, b) => a !== b)

167 -

}

175 +

},

176 + 177 +

// Define a new operator "<>" which maps to the function "notEqual"

178 +

// and has the same precedence as operator "==".

179 +

operators: [

180 +

{ name: 'aboutEq', op: '~=', at: '==' }

181 +

]

168 182

}

169 183

```

170 184 185 +

To allow using a chain of multiple operators without parenthesis, like `a and b and c`, the option `leftAssociative` can be set `true`. Without this, an exception will be thrown, which can be solved by using parenthesis like `(a and b) and c`.

186 + 187 +

When the function of the operator supports more than two arguments, like `and(a, b, c, ...)`, the option `vararg` can be set `true`. In that case, a chain of operators like `a and b and c` will be parsed into the JSON Format `["and", a, b, c, ...]`. Operators that do not support variable arguments, like `1 + 2 + 3`, will be parsed into a nested JSON Format like `["add", ["add", 1, 2], 3]`.

188 + 189 +

All build-in operators and their precedence are listed on the documentation page in the section [Operators](https://jsonquerylang.org/docs/#operators).

190 + 171 191

Here an example of using the function `jsonquery`:

172 192 173 193

```js

@@ -258,9 +278,6 @@ The query engine passes the raw arguments to all functions, and the functions ha

258 278 259 279

```ts

260 280

const options = {

261 -

operators: {

262 -

notEqual: '<>'

263 -

},

264 281

functions: {

265 282

notEqual: (a: JSONQuery, b: JSONQuery) => {

266 283

const aCompiled = compile(a)

@@ -286,9 +303,6 @@ To automatically compile and evaluate the arguments of the function, the helper

286 303

import { jsonquery, buildFunction } from '@jsonquerylang/jsonquery'

287 304 288 305

const options = {

289 -

operators: {

290 -

notEqual: '<>'

291 -

},

292 306

functions: {

293 307

notEqual: buildFunction((a: number, b: number) => a !== b)

294 308

}

Original file line number Diff line number Diff line change

@@ -1,12 +1,16 @@

1 1

import Ajv from 'ajv'

2 2

import { describe, expect, test } from 'vitest'

3 -

import type { CompileTestSuite } from '../test-suite/compile.test'

3 +

import type { CompileTestException, CompileTestSuite } from '../test-suite/compile.test'

4 4

import suite from '../test-suite/compile.test.json'

5 5

import schema from '../test-suite/compile.test.schema.json'

6 6

import { compile } from './compile'

7 7

import { buildFunction } from './functions'

8 8

import type { JSONQuery, JSONQueryCompileOptions } from './types'

9 9 10 +

function isTestException(test: unknown): test is CompileTestException {

11 +

return !!test && typeof (test as Record<string, unknown>).throws === 'string'

12 +

}

13 + 10 14

const data = [

11 15

{ name: 'Chris', age: 23, city: 'New York' },

12 16

{ name: 'Emily', age: 19, city: 'Atlanta' },

@@ -31,13 +35,20 @@ const testsByCategory = groupByCategory(suite.tests) as Record<string, CompileTe

31 35

for (const [category, tests] of Object.entries(testsByCategory)) {

32 36

describe(category, () => {

33 37

for (const currentTest of tests) {

34 -

const { description, input, query, output } = currentTest

35 - 36 -

test(description, () => {

37 -

const actualOutput = compile(query)(input)

38 - 39 -

expect({ input, query, output: actualOutput }).toEqual({ input, query, output })

40 -

})

38 +

if (isTestException(currentTest)) {

39 +

test(currentTest.description, () => {

40 +

const { input, query, throws } = currentTest

41 + 42 +

expect(() => compile(query)(input)).toThrow(throws)

43 +

})

44 +

} else {

45 +

test(currentTest.description, () => {

46 +

const { input, query, output } = currentTest

47 +

const actualOutput = compile(query)(input)

48 + 49 +

expect({ input, query, output: actualOutput }).toEqual({ input, query, output })

50 +

})

51 +

}

41 52

}

42 53

})

43 54

}

Original file line number Diff line number Diff line change

@@ -247,8 +247,8 @@ export const functions: FunctionBuildersMap = {

247 247 248 248

max: () => (data: number[]) => Math.max(...data),

249 249 250 -

and: buildFunction((a, b) => !!(a && b)),

251 -

or: buildFunction((a, b) => !!(a || b)),

250 +

and: buildFunction((...args) => args.reduce((a, b) => !!(a && b))),

251 +

or: buildFunction((...args) => args.reduce((a, b) => !!(a || b))),

252 252

not: buildFunction((a: unknown) => !a),

253 253 254 254

exists: (queryGet: JSONQueryFunction) => {

@@ -297,8 +297,9 @@ export const functions: FunctionBuildersMap = {

297 297

subtract: buildFunction((a: number, b: number) => a - b),

298 298

multiply: buildFunction((a: number, b: number) => a * b),

299 299

divide: buildFunction((a: number, b: number) => a / b),

300 -

pow: buildFunction((a: number, b: number) => a ** b),

301 300

mod: buildFunction((a: number, b: number) => a % b),

301 +

pow: buildFunction((a: number, b: number) => a ** b),

302 + 302 303

abs: buildFunction(Math.abs),

303 304

round: buildFunction((value: number, digits = 0) => {

304 305

const num = Math.round(Number(`${value}e${digits}`))

Original file line number Diff line number Diff line change

@@ -41,25 +41,26 @@ describe('jsonquery', () => {

41 41 42 42

test('should execute a JSON query with custom operators', () => {

43 43

const options: JSONQueryOptions = {

44 -

operators: {

45 -

aboutEq: '~='

44 +

functions: {

45 +

aboutEq: buildFunction((a: string, b: string) => a.toLowerCase() === b.toLowerCase())

46 46

}

47 47

}

48 48 49 -

expect(jsonquery({ name: 'Joe' }, ['get', 'name'], options)).toEqual('Joe')

49 +

expect(jsonquery({ name: 'Joe' }, ['aboutEq', ['get', 'name'], 'joe'], options)).toEqual(true)

50 50

})

51 51 52 52

test('should execute a text query with custom operators', () => {

53 53

const options: JSONQueryOptions = {

54 -

operators: {

55 -

aboutEq: '~='

54 +

operators: [{ name: 'aboutEq', op: '~=', at: '==' }],

55 +

functions: {

56 +

aboutEq: buildFunction((a: string, b: string) => a.toLowerCase() === b.toLowerCase())

56 57

}

57 58

}

58 59 59 -

expect(jsonquery({ name: 'Joe' }, '.name', options)).toEqual('Joe')

60 +

expect(jsonquery({ name: 'Joe' }, '.name ~= "joe"', options)).toEqual(true)

60 61

})

61 62 62 -

test('have exported all documented functions', () => {

63 +

test('have exported all documented functions and objects', () => {

63 64

expect(jsonquery).toBeTypeOf('function')

64 65

expect(parse).toBeTypeOf('function')

65 66

expect(stringify).toBeTypeOf('function')

Original file line number Diff line number Diff line change

@@ -0,0 +1,44 @@

1 +

import { describe, expect, test } from 'vitest'

2 +

import { extendOperators } from './operators'

3 + 4 +

describe('operators', () => {

5 +

test('should extend operators (at)', () => {

6 +

const ops = [{ add: '+', subtract: '-' }, { eq: '==' }]

7 + 8 +

expect(extendOperators(ops, [{ name: 'aboutEq', op: '~=', at: '==' }])).toEqual([

9 +

{ add: '+', subtract: '-' },

10 +

{ eq: '==', aboutEq: '~=' }

11 +

])

12 +

})

13 + 14 +

test('should extend operators (after)', () => {

15 +

const ops = [{ add: '+', subtract: '-' }, { eq: '==' }]

16 + 17 +

expect(extendOperators(ops, [{ name: 'aboutEq', op: '~=', after: '+' }])).toEqual([

18 +

{ add: '+', subtract: '-' },

19 +

{ aboutEq: '~=' },

20 +

{ eq: '==' }

21 +

])

22 +

})

23 + 24 +

test('should extend operators (before)', () => {

25 +

const ops = [{ add: '+', subtract: '-' }, { eq: '==' }]

26 + 27 +

expect(extendOperators(ops, [{ name: 'aboutEq', op: '~=', before: '==' }])).toEqual([

28 +

{ add: '+', subtract: '-' },

29 +

{ aboutEq: '~=' },

30 +

{ eq: '==' }

31 +

])

32 +

})

33 + 34 +

test('should extend operators (multiple consecutive)', () => {

35 +

const ops = [{ add: '+', subtract: '-' }, { eq: '==' }]

36 + 37 +

expect(

38 +

extendOperators(ops, [

39 +

{ name: 'first', op: 'op1', before: '==' },

40 +

{ name: 'second', op: 'op2', before: 'op1' }

41 +

])

42 +

).toEqual([{ add: '+', subtract: '-' }, { second: 'op2' }, { first: 'op1' }, { eq: '==' }])

43 +

})

44 +

})

Original file line number Diff line number Diff line change

@@ -0,0 +1,46 @@

1 +

import { isArray } from './is'

2 +

import type { CustomOperator, OperatorGroup } from './types'

3 + 4 +

// operator precedence from highest to lowest

5 +

export const operators: OperatorGroup[] = [

6 +

{ pow: '^' },

7 +

{ multiply: '*', divide: '/', mod: '%' },

8 +

{ add: '+', subtract: '-' },

9 +

{ gt: '>', gte: '>=', lt: '<', lte: '<=', in: 'in', 'not in': 'not in' },

10 +

{ eq: '==', ne: '!=' },

11 +

{ and: 'and' },

12 +

{ or: 'or' },

13 +

{ pipe: '|' }

14 +

]

15 + 16 +

export const varargOperators = ['|', 'and', 'or']

17 +

export const leftAssociativeOperators = ['|', 'and', 'or', '*', '/', '%', '+', '-']

18 + 19 +

export function extendOperators(operators: OperatorGroup[], customOperators: CustomOperator[]) {

20 +

// backward compatibility error with v4 where `operators` was an object

21 +

if (!isArray(customOperators)) {

22 +

throw new Error('Invalid custom operators')

23 +

}

24 + 25 +

return customOperators.reduce(extendOperator, operators)

26 +

}

27 + 28 +

function extendOperator(

29 +

operators: OperatorGroup[],

30 +

// @ts-expect-error Inside the function we will check whether at, below, and above are defined

31 +

{ name, op, at, after, before }: CustomOperator

32 +

): OperatorGroup[] {

33 +

if (at) {

34 +

return operators.map((group) => {

35 +

return Object.values(group).includes(at) ? { ...group, [name]: op } : group

36 +

})

37 +

}

38 + 39 +

const searchOp = after ?? before

40 +

const index = operators.findIndex((group) => Object.values(group).includes(searchOp))

41 +

if (index !== -1) {

42 +

return operators.toSpliced(index + (after ? 1 : 0), 0, { [name]: op })

43 +

}

44 + 45 +

throw new Error('Invalid custom operator')

46 +

}

Original file line number Diff line number Diff line change

@@ -43,18 +43,70 @@ for (const [category, testGroups] of Object.entries(testsByCategory)) {

43 43

describe('customization', () => {

44 44

test('should parse a custom function', () => {

45 45

const options: JSONQueryParseOptions = {

46 -

functions: { customFn: true }

46 +

functions: { customFn: () => () => 42 }

47 47

}

48 48 49 49

expect(parse('customFn(.age, "desc")', options)).toEqual(['customFn', ['get', 'age'], 'desc'])

50 + 51 +

// built-in functions should still be available

52 +

expect(parse('add(2, 3)', options)).toEqual(['add', 2, 3])

50 53

})

51 54 52 -

test('should parse a custom operator', () => {

55 +

test('should parse a custom operator without vararg', () => {

53 56

const options: JSONQueryParseOptions = {

54 -

operators: { aboutEq: '~=' }

57 +

operators: [{ name: 'aboutEq', op: '~=', at: '==' }]

55 58

}

56 59 57 60

expect(parse('.score ~= 8', options)).toEqual(['aboutEq', ['get', 'score'], 8])

61 + 62 +

// built-in operators should still be available

63 +

expect(parse('.score == 8', options)).toEqual(['eq', ['get', 'score'], 8])

64 + 65 +

expect(() => parse('2 ~= 3 ~= 4', options)).toThrow("Unexpected part '~= 4'")

66 +

})

67 + 68 +

test('should parse a custom operator with vararg without leftAssociative', () => {

69 +

const options: JSONQueryParseOptions = {

70 +

operators: [{ name: 'aboutEq', op: '~=', at: '==', vararg: true }]

71 +

}

72 + 73 +

expect(parse('2 and 3 and 4', options)).toEqual(['and', 2, 3, 4])

74 +

expect(parse('2 ~= 3', options)).toEqual(['aboutEq', 2, 3])

75 +

expect(parse('2 ~= 3 and 4', options)).toEqual(['and', ['aboutEq', 2, 3], 4])

76 +

expect(parse('2 and 3 ~= 4', options)).toEqual(['and', 2, ['aboutEq', 3, 4]])

77 +

expect(parse('2 == 3 ~= 4', options)).toEqual(['aboutEq', ['eq', 2, 3], 4])

78 +

expect(parse('2 ~= 3 == 4', options)).toEqual(['eq', ['aboutEq', 2, 3], 4])

79 +

expect(() => parse('2 ~= 3 ~= 4', options)).toThrow("Unexpected part '~= 4'")

80 +

expect(() => parse('2 == 3 == 4', options)).toThrow("Unexpected part '== 4'")

81 +

})

82 + 83 +

test('should parse a custom operator with vararg with leftAssociative', () => {

84 +

const options: JSONQueryParseOptions = {

85 +

operators: [{ name: 'aboutEq', op: '~=', at: '==', vararg: true, leftAssociative: true }]

86 +

}

87 + 88 +

expect(parse('2 and 3 and 4', options)).toEqual(['and', 2, 3, 4])

89 +

expect(parse('2 ~= 3', options)).toEqual(['aboutEq', 2, 3])

90 +

expect(parse('2 ~= 3 ~= 4', options)).toEqual(['aboutEq', 2, 3, 4])

91 +

expect(() => parse('2 == 3 == 4', options)).toThrow("Unexpected part '== 4'")

92 +

})

93 + 94 +

test('should throw an error in case of an invalid custom operator', () => {

95 +

const options: JSONQueryParseOptions = {

96 +

// @ts-ignore

97 +

operators: [{}]

98 +

}

99 + 100 +

expect(() => parse('.score > 8', options)).toThrow('Invalid custom operator')

101 +

})

102 + 103 +

test('should throw an error in case of an invalid custom operator (2)', () => {

104 +

const options: JSONQueryParseOptions = {

105 +

// @ts-ignore

106 +

operators: {}

107 +

}

108 + 109 +

expect(() => parse('.score > 8', options)).toThrow('Invalid custom operators')

58 110

})

59 111

})

60 112

You can’t perform that action at this time.


RetroSearch is an open source project built by @garambo | Open a GitHub Issue

Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo

HTML: 3.2 | Encoding: UTF-8 | Version: 0.7.4