Skip to content

Commit 970112f

Browse files
committed
feat: implement vararg operators (WIP)
1 parent 3fd94de commit 970112f

File tree

8 files changed

+111
-130
lines changed

8 files changed

+111
-130
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ filter(.age >= 18 and .age <= 65)
210210

211211
The operators have the following precedence, from highest to lowest:
212212

213+
- `|`
213214
- `^`
214215
- `*`, `/`, `%`
215216
- `+`, `-`

src/functions.ts

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,20 @@ export function buildFunction(fn: (...args: unknown[]) => unknown): FunctionBuil
2727
}
2828
}
2929

30+
const validateMaxArgs = (fn: (...args: unknown[]) => unknown) => {
31+
return (...args: unknown[]) => {
32+
if (args.length > fn.length) {
33+
throw new Error('Too many arguments')
34+
}
35+
36+
return fn(...args)
37+
}
38+
}
39+
40+
const reduceArgs = (fn: (...args: unknown[]) => unknown) => {
41+
return (...args: unknown[]) => args.reduce(fn)
42+
}
43+
3044
export const functions: FunctionBuildersMap = {
3145
pipe: (...entries: JSONQuery[]) => {
3246
const _entries = entries.map((entry) => compile(entry))
@@ -247,8 +261,8 @@ export const functions: FunctionBuildersMap = {
247261

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

250-
and: buildFunction((a, b) => !!(a && b)),
251-
or: buildFunction((a, b) => !!(a || b)),
264+
and: buildFunction(reduceArgs((a, b) => !!(a && b))),
265+
or: buildFunction(reduceArgs((a, b) => !!(a || b))),
252266
not: buildFunction((a: unknown) => !a),
253267

254268
exists: (queryGet: JSONQueryFunction) => {
@@ -286,19 +300,20 @@ export const functions: FunctionBuildersMap = {
286300
return (data: unknown) => regex.test(getter(data) as string)
287301
},
288302

289-
eq: buildFunction((a, b) => a === b),
290-
gt: buildFunction((a, b) => a > b),
291-
gte: buildFunction((a, b) => a >= b),
292-
lt: buildFunction((a, b) => a < b),
293-
lte: buildFunction((a, b) => a <= b),
294-
ne: buildFunction((a, b) => a !== b),
295-
296-
add: buildFunction((a: number, b: number) => a + b),
297-
subtract: buildFunction((a: number, b: number) => a - b),
298-
multiply: buildFunction((a: number, b: number) => a * b),
299-
divide: buildFunction((a: number, b: number) => a / b),
300-
pow: buildFunction((a: number, b: number) => a ** b),
301-
mod: buildFunction((a: number, b: number) => a % b),
303+
eq: buildFunction(validateMaxArgs((a, b) => a === b)),
304+
gt: buildFunction(validateMaxArgs((a, b) => a > b)),
305+
gte: buildFunction(validateMaxArgs((a, b) => a >= b)),
306+
lt: buildFunction(validateMaxArgs((a, b) => a < b)),
307+
lte: buildFunction(validateMaxArgs((a, b) => a <= b)),
308+
ne: buildFunction(validateMaxArgs((a, b) => a !== b)),
309+
310+
add: buildFunction(reduceArgs((a: number, b: number) => a + b)),
311+
subtract: buildFunction(reduceArgs((a: number, b: number) => a - b)),
312+
multiply: buildFunction(reduceArgs((a: number, b: number) => a * b)),
313+
divide: buildFunction(reduceArgs((a: number, b: number) => a / b)),
314+
pow: buildFunction(validateMaxArgs((a: number, b: number) => a ** b)),
315+
mod: buildFunction(validateMaxArgs((a: number, b: number) => a % b)),
316+
302317
abs: buildFunction(Math.abs),
303318
round: buildFunction((value: number, digits = 0) => {
304319
const num = Math.round(Number(`${value}e${digits}`))

src/operators.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { CustomOperator, OperatorGroup } from './types'
33

44
// operator precedence from highest to lowest
55
export const operators: OperatorGroup[] = [
6+
{ pipe: '|' },
67
{ pow: '^' },
78
{ multiply: '*', divide: '/', mod: '%' },
89
{ add: '+', subtract: '-' },

src/parse.ts

Lines changed: 11 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -29,27 +29,6 @@ export function parse(query: string, options?: JSONQueryParseOptions): JSONQuery
2929
const allFunctions = options?.functions ?? functions
3030
const allOperators = extendOperators(operators, options?.operators ?? [])
3131

32-
const parsePipe = () => {
33-
skipWhitespace()
34-
const first = parseOperator(allOperators.length - 1)
35-
skipWhitespace()
36-
37-
if (query[i] === '|') {
38-
const pipe = [first]
39-
40-
while (query[i] === '|') {
41-
i++
42-
skipWhitespace()
43-
44-
pipe.push(parseOperator(allOperators.length - 1))
45-
}
46-
47-
return ['pipe', ...pipe]
48-
}
49-
50-
return first
51-
}
52-
5332
const parseOperator = (precedenceLevel: number) => {
5433
const currentOperators = allOperators[precedenceLevel]
5534
if (!currentOperators) {
@@ -58,16 +37,16 @@ export function parse(query: string, options?: JSONQueryParseOptions): JSONQuery
5837

5938
let left = parseOperator(precedenceLevel - 1)
6039

61-
skipWhitespace()
62-
6340
while (true) {
41+
skipWhitespace()
42+
6443
const name = parseOperatorName(currentOperators)
6544
if (!name) {
6645
break
6746
}
6847

6948
const right = parseOperator(precedenceLevel - 1)
70-
left = [name, left, right]
49+
left = name === left[0] ? [...left, right] : [name, left, right]
7150
}
7251

7352
return left
@@ -92,9 +71,11 @@ export function parse(query: string, options?: JSONQueryParseOptions): JSONQuery
9271
}
9372

9473
const parseParenthesis = () => {
74+
skipWhitespace()
75+
9576
if (query[i] === '(') {
9677
i++
97-
const inner = parsePipe()
78+
const inner = parseOperator(allOperators.length - 1)
9879
eatChar(')')
9980
return inner
10081
}
@@ -139,11 +120,11 @@ export function parse(query: string, options?: JSONQueryParseOptions): JSONQuery
139120

140121
skipWhitespace()
141122

142-
const args = query[i] !== ')' ? [parsePipe()] : []
123+
const args = query[i] !== ')' ? [parseOperator(allOperators.length - 1)] : []
143124
while (i < query.length && query[i] !== ')') {
144125
skipWhitespace()
145126
eatChar(',')
146-
args.push(parsePipe())
127+
args.push(parseOperator(allOperators.length - 1))
147128
}
148129

149130
eatChar(')')
@@ -172,7 +153,7 @@ export function parse(query: string, options?: JSONQueryParseOptions): JSONQuery
172153
skipWhitespace()
173154
eatChar(':')
174155

175-
object[key] = parsePipe()
156+
object[key] = parseOperator(allOperators.length - 1)
176157
}
177158

178159
eatChar('}')
@@ -199,7 +180,7 @@ export function parse(query: string, options?: JSONQueryParseOptions): JSONQuery
199180
skipWhitespace()
200181
}
201182

202-
array.push(parsePipe())
183+
array.push(parseOperator(allOperators.length - 1))
203184
}
204185

205186
eatChar(']')
@@ -258,7 +239,7 @@ export function parse(query: string, options?: JSONQueryParseOptions): JSONQuery
258239
}
259240

260241
let i = 0
261-
const output = parsePipe()
242+
const output = parseOperator(allOperators.length - 1)
262243
parseEnd()
263244

264245
return output

src/stringify.ts

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,10 @@ export const stringify = (query: JSONQuery, options?: JSONQueryStringifyOptions)
4040
const _stringify = (
4141
query: JSONQuery,
4242
indent: string,
43-
operatorPrecedence = allOperators.length - 1
43+
parentPrecedence = allOperators.length - 1
4444
) =>
4545
isArray(query)
46-
? stringifyFunction(query as JSONQueryFunction, indent, operatorPrecedence)
46+
? stringifyFunction(query as JSONQueryFunction, indent, parentPrecedence)
4747
: JSON.stringify(query) // value (string, number, boolean, null)
4848

4949
const stringifyFunction = (
@@ -57,12 +57,6 @@ export const stringify = (query: JSONQuery, options?: JSONQueryStringifyOptions)
5757
return stringifyPath(args as JSONPath)
5858
}
5959

60-
if (name === 'pipe') {
61-
const argsStr = args.map((arg) => _stringify(arg, indent + space))
62-
63-
return join(argsStr, ['', ' | ', ''], ['', `\n${indent + space}| `, ''])
64-
}
65-
6660
if (name === 'object') {
6761
return stringifyObject(args[0] as JSONQueryObject, indent)
6862
}
@@ -78,15 +72,13 @@ export const stringify = (query: JSONQuery, options?: JSONQueryStringifyOptions)
7872

7973
// operator like ".age >= 18"
8074
const op = allOperatorsMap[name]
81-
if (op && args.length === 2) {
75+
if (op) {
8276
const precedence = allOperators.findIndex((group) => name in group)
83-
const [left, right] = args
84-
const leftStr = _stringify(left, indent, precedence)
85-
const rightStr = _stringify(right, indent, precedence)
77+
const start = parentPrecedence < precedence ? '(' : ''
78+
const end = parentPrecedence < precedence ? ')' : ''
79+
const argsStr = args.map((arg) => _stringify(arg, indent + space, precedence))
8680

87-
return parentPrecedence < precedence
88-
? `(${leftStr} ${op} ${rightStr})`
89-
: `${leftStr} ${op} ${rightStr}`
81+
return join(argsStr, [start, ` ${op} `, end], [start, `\n${indent + space}${op} `, end])
9082
}
9183

9284
// regular function like sort(.age)

test-suite/compile.test.json

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -948,6 +948,13 @@
948948
"query": ["and", true, true],
949949
"output": true
950950
},
951+
{
952+
"category": "and",
953+
"description": "should calculate and with more than two arguments",
954+
"input": null,
955+
"query": ["and", true, true, false],
956+
"output": false
957+
},
951958

952959
{
953960
"category": "or",
@@ -998,6 +1005,13 @@
9981005
"query": ["or", false, true],
9991006
"output": true
10001007
},
1008+
{
1009+
"category": "or",
1010+
"description": "should calculate or with more than two arguments",
1011+
"input": null,
1012+
"query": ["or", false, false, true],
1013+
"output": true
1014+
},
10011015

10021016
{
10031017
"category": "not",
@@ -1268,6 +1282,13 @@
12681282
"query": ["add", "is:", true],
12691283
"output": "is:true"
12701284
},
1285+
{
1286+
"category": "add",
1287+
"description": "should add more than two arguments",
1288+
"input": null,
1289+
"query": ["add", 6, 2, 3],
1290+
"output": 11
1291+
},
12711292

12721293
{
12731294
"category": "subtract",
@@ -1283,6 +1304,13 @@
12831304
"query": ["subtract", 6, 2],
12841305
"output": 4
12851306
},
1307+
{
1308+
"category": "subtract",
1309+
"description": "should subtract more than two arguments",
1310+
"input": null,
1311+
"query": ["subtract", 6, 2, 1],
1312+
"output": 3
1313+
},
12861314

12871315
{
12881316
"category": "multiply",
@@ -1298,6 +1326,13 @@
12981326
"query": ["multiply", 6, 2],
12991327
"output": 12
13001328
},
1329+
{
1330+
"category": "multiply",
1331+
"description": "should multiply more than two arguments",
1332+
"input": null,
1333+
"query": ["multiply", 6, 2, 3],
1334+
"output": 36
1335+
},
13011336

13021337
{
13031338
"category": "divide",
@@ -1313,6 +1348,13 @@
13131348
"query": ["divide", 6, 2],
13141349
"output": 3
13151350
},
1351+
{
1352+
"category": "divide",
1353+
"description": "should divide more than two arguments",
1354+
"input": null,
1355+
"query": ["divide", 6, 2, 3],
1356+
"output": 1
1357+
},
13161358

13171359
{
13181360
"category": "pow",

test-suite/parse.test.json

Lines changed: 9 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -165,60 +165,26 @@
165165
"category": "operator",
166166
"description": "should parse operators and and or with more than two arguments",
167167
"tests": [
168-
{
169-
"input": "1 and 2 and 3",
170-
"output": ["and", ["and", 1, 2], 3]
171-
},
172-
{
173-
"input": "1 or 2 or 3",
174-
"output": ["or", ["or", 1, 2], 3]
175-
},
176-
{
177-
"input": "1 or 2 or 3 or 4",
178-
"output": ["or", ["or", ["or", 1, 2], 3], 4]
179-
}
168+
{ "input": "1 and 2 and 3", "output": ["and", 1, 2, 3] },
169+
{ "input": "1 or 2 or 3", "output": ["or", 1, 2, 3] },
170+
{ "input": "1 ^ 2 ^ 3", "output": ["pow", 1, 2, 3] },
171+
{ "input": "1 * 2 * 3", "output": ["multiply", 1, 2, 3] },
172+
{ "input": "1 / 2 / 3", "output": ["divide", 1, 2, 3] },
173+
{ "input": "1 + 2 + 3", "output": ["add", 1, 2, 3] },
174+
{ "input": "1 - 2 - 3", "output": ["subtract", 1, 2, 3] },
175+
{ "input": "1 % 2 % 3", "output": ["mod", 1, 2, 3] }
180176
]
181177
},
182178
{
183179
"category": "operator",
184180
"description": "should parse operators with the same precedence",
185181
"tests": [
186-
{ "input": "2 * 3 * 4", "output": ["multiply", ["multiply", 2, 3], 4] },
187-
{ "input": "2 / 3 / 4", "output": ["divide", ["divide", 2, 3], 4] },
188182
{ "input": "2 * 3 / 4", "output": ["divide", ["multiply", 2, 3], 4] },
189183
{ "input": "2 / 3 * 4", "output": ["multiply", ["divide", 2, 3], 4] },
190184
{ "input": "2 * 3 % 4", "output": ["mod", ["multiply", 2, 3], 4] },
191185
{ "input": "2 % 3 * 4", "output": ["multiply", ["mod", 2, 3], 4] },
192-
{ "input": "2 + 3 + 4", "output": ["add", ["add", 2, 3], 4] },
193-
{ "input": "2 - 3 - 4", "output": ["subtract", ["subtract", 2, 3], 4] },
194186
{ "input": "2 + 3 - 4", "output": ["subtract", ["add", 2, 3], 4] },
195-
{ "input": "2 - 3 + 4", "output": ["add", ["subtract", 2, 3], 4] },
196-
{ "input": "2 > 3 > 4", "output": ["gt", ["gt", 2, 3], 4] },
197-
{ "input": "2 >= 3 >= 4", "output": ["gte", ["gte", 2, 3], 4] },
198-
{ "input": "2 < 3 < 4", "output": ["lt", ["lt", 2, 3], 4] },
199-
{ "input": "2 <= 3 <= 4", "output": ["lte", ["lte", 2, 3], 4] },
200-
{ "input": "2 in 3 in 4", "output": ["in", ["in", 2, 3], 4] },
201-
{ "input": "2 not in 3 not in 4", "output": ["not in", ["not in", 2, 3], 4] },
202-
{ "input": "2 > 3 >= 4", "output": ["gte", ["gt", 2, 3], 4] },
203-
{ "input": "2 >= 3 > 4", "output": ["gt", ["gte", 2, 3], 4] },
204-
{ "input": "2 > 3 < 4", "output": ["lt", ["gt", 2, 3], 4] },
205-
{ "input": "2 < 3 > 4", "output": ["gt", ["lt", 2, 3], 4] },
206-
{ "input": "2 > 3 <= 4", "output": ["lte", ["gt", 2, 3], 4] },
207-
{ "input": "2 <= 3 > 4", "output": ["gt", ["lte", 2, 3], 4] },
208-
{ "input": "2 > .a in .b", "output": ["in", ["gt", 2, ["get", "a"]], ["get", "b"]] },
209-
{ "input": ".a in .b > 2", "output": ["gt", ["in", ["get", "a"], ["get", "b"]], 2] },
210-
{
211-
"input": "2 > .a not in .b",
212-
"output": ["not in", ["gt", 2, ["get", "a"]], ["get", "b"]]
213-
},
214-
{
215-
"input": ".a not in .b > 2",
216-
"output": ["gt", ["not in", ["get", "a"], ["get", "b"]], 2]
217-
},
218-
{ "input": "2 == 3 == 4", "output": ["eq", ["eq", 2, 3], 4] },
219-
{ "input": "2 != 3 != 4", "output": ["ne", ["ne", 2, 3], 4] },
220-
{ "input": "2 == 3 != 4", "output": ["ne", ["eq", 2, 3], 4] },
221-
{ "input": "2 != 3 == 4", "output": ["eq", ["ne", 2, 3], 4] }
187+
{ "input": "2 - 3 + 4", "output": ["add", ["subtract", 2, 3], 4] }
222188
]
223189
},
224190
{

0 commit comments

Comments
 (0)