Skip to content

Commit f9f5106

Browse files
committed
feat: function stringify only wraps an operator in parentheses when needed
1 parent a3aa78a commit f9f5106

File tree

3 files changed

+62
-40
lines changed

3 files changed

+62
-40
lines changed

src/stringify.ts

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,19 @@ const DEFAULT_INDENTATION = ' '
3333
*/
3434
export const stringify = (query: JSONQuery, options?: JSONQueryStringifyOptions) => {
3535
const space = options?.indentation ?? DEFAULT_INDENTATION
36-
const allOperators = Object.assign({}, ...(options?.operators ?? operators))
37-
38-
const _stringify = (query: JSONQuery, indent: string) =>
39-
isArray(query) ? stringifyFunction(query as JSONQueryFunction, indent) : JSON.stringify(query) // value (string, number, boolean, null)
40-
41-
const stringifyFunction = (query: JSONQueryFunction, indent: string) => {
36+
const allOperators = options?.operators ?? operators
37+
const allOperatorsMap = Object.assign({}, ...allOperators)
38+
39+
const _stringify = (query: JSONQuery, indent: string, operatorPrecedence = 0) =>
40+
isArray(query)
41+
? stringifyFunction(query as JSONQueryFunction, indent, operatorPrecedence)
42+
: JSON.stringify(query) // value (string, number, boolean, null)
43+
44+
const stringifyFunction = (
45+
query: JSONQueryFunction,
46+
indent: string,
47+
parentPrecedence: number
48+
) => {
4249
const [name, ...args] = query
4350

4451
if (name === 'get' && args.length > 0) {
@@ -65,27 +72,28 @@ export const stringify = (query: JSONQuery, options?: JSONQueryStringifyOptions)
6572
}
6673

6774
// operator like ".age >= 18"
68-
const op = allOperators[name]
75+
const op = allOperatorsMap[name]
6976
if (op && args.length === 2) {
77+
const precedence = allOperators.findIndex((group) => name in group)
7078
const [left, right] = args
71-
const leftStr = _stringify(left, indent)
72-
const rightStr = _stringify(right, indent)
73-
// FIXME: do not wrap in parenthesis when not needed: reckon with the operator precedence
74-
return `(${leftStr} ${op} ${rightStr})`
79+
const leftStr = _stringify(left, indent, precedence)
80+
const rightStr = _stringify(right, indent, precedence)
81+
82+
return parentPrecedence > precedence
83+
? `(${leftStr} ${op} ${rightStr})`
84+
: `${leftStr} ${op} ${rightStr}`
7585
}
7686

7787
// regular function like sort(.age)
7888
const childIndent = args.length === 1 ? indent : indent + space
7989
const argsStr = args.map((arg) => _stringify(arg, childIndent))
80-
return args.length === 1 && argsStr[0][0] === '('
81-
? `${name}${argsStr[0]}`
82-
: join(
83-
argsStr,
84-
[`${name}(`, ', ', ')'],
85-
args.length === 1
86-
? [`${name}(`, `,\n${indent}`, ')']
87-
: [`${name}(\n${childIndent}`, `,\n${childIndent}`, `\n${indent})`]
88-
)
90+
return join(
91+
argsStr,
92+
[`${name}(`, ', ', ')'],
93+
args.length === 1
94+
? [`${name}(`, `,\n${indent}`, ')']
95+
: [`${name}(\n${childIndent}`, `,\n${childIndent}`, `\n${indent})`]
96+
)
8997
}
9098

9199
const stringifyObject = (query: JSONQueryObject, indent: string) => {

test-suite/parse.test.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,10 @@
273273
{ "input": " { a : 1 } ", "output": ["object", { "a": 1 }] },
274274
{ "input": "{a:1,b:2}", "output": ["object", { "a": 1, "b": 2 }] },
275275
{ "input": "{ a : 1 , b : 2 }", "output": ["object", { "a": 1, "b": 2 }] },
276+
{
277+
"input": "{ ok: .error == null }",
278+
"output": ["object", { "ok": ["eq", ["get", "error"], null] }]
279+
},
276280
{ "input": "{ \"a\" : 1 , \"b\" : 2 }", "output": ["object", { "a": 1, "b": 2 }] },
277281
{ "input": "{ 2: \"two\" }", "output": ["object", { "2": "two" }] },
278282
{ "input": "{ 0: \"zero\" }", "output": ["object", { "0": "zero" }] },

test-suite/stringify.test.json

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,41 +20,51 @@
2020
"category": "operator",
2121
"description": "should stringify all operators",
2222
"tests": [
23-
{ "input": ["eq", ["get", "score"], 8], "output": "(.score == 8)" },
24-
{ "input": ["lt", ["get", "score"], 8], "output": "(.score < 8)" },
25-
{ "input": ["lte", ["get", "score"], 8], "output": "(.score <= 8)" },
26-
{ "input": ["gt", ["get", "score"], 8], "output": "(.score > 8)" },
27-
{ "input": ["gte", ["get", "score"], 8], "output": "(.score >= 8)" },
28-
{ "input": ["ne", ["get", "score"], 8], "output": "(.score != 8)" },
29-
{ "input": ["add", ["get", "score"], 8], "output": "(.score + 8)" },
30-
{ "input": ["subtract", ["get", "score"], 8], "output": "(.score - 8)" },
31-
{ "input": ["multiply", ["get", "score"], 8], "output": "(.score * 8)" },
32-
{ "input": ["divide", ["get", "score"], 8], "output": "(.score / 8)" },
33-
{ "input": ["pow", ["get", "score"], 8], "output": "(.score ^ 8)" },
34-
{ "input": ["mod", ["get", "score"], 8], "output": "(.score % 8)" },
35-
{ "input": ["and", ["get", "score"], 8], "output": "(.score and 8)" },
36-
{ "input": ["or", ["get", "score"], 8], "output": "(.score or 8)" },
23+
{ "input": ["eq", ["get", "score"], 8], "output": ".score == 8" },
24+
{ "input": ["lt", ["get", "score"], 8], "output": ".score < 8" },
25+
{ "input": ["lte", ["get", "score"], 8], "output": ".score <= 8" },
26+
{ "input": ["gt", ["get", "score"], 8], "output": ".score > 8" },
27+
{ "input": ["gte", ["get", "score"], 8], "output": ".score >= 8" },
28+
{ "input": ["ne", ["get", "score"], 8], "output": ".score != 8" },
29+
{ "input": ["add", ["get", "score"], 8], "output": ".score + 8" },
30+
{ "input": ["subtract", ["get", "score"], 8], "output": ".score - 8" },
31+
{ "input": ["multiply", ["get", "score"], 8], "output": ".score * 8" },
32+
{ "input": ["divide", ["get", "score"], 8], "output": ".score / 8" },
33+
{ "input": ["pow", ["get", "score"], 8], "output": ".score ^ 8" },
34+
{ "input": ["mod", ["get", "score"], 8], "output": ".score % 8" },
35+
{ "input": ["and", ["get", "score"], 8], "output": ".score and 8" },
36+
{ "input": ["or", ["get", "score"], 8], "output": ".score or 8" },
3737
{
3838
"input": ["in", ["get", "score"], ["array", 8, 9, 10]],
39-
"output": "(.score in [8, 9, 10])"
39+
"output": ".score in [8, 9, 10]"
4040
},
4141
{
4242
"input": ["not in", ["get", "score"], ["array", 8, 9, 10]],
43-
"output": "(.score not in [8, 9, 10])"
43+
"output": ".score not in [8, 9, 10]"
4444
}
4545
]
4646
},
47+
{
48+
"category": "operator",
49+
"description": "should wrap operators in parenthesis when needed",
50+
"tests": [
51+
{ "input": ["add", 2, 3], "output": "2 + 3" },
52+
{ "input": ["multiply", ["add", 1, 2], 3], "output": "(1 + 2) * 3" },
53+
{ "input": ["add", ["add", 1, 2], 3], "output": "1 + 2 + 3" },
54+
{ "input": ["add", ["multiply", 1, 2], 3], "output": "1 * 2 + 3" }
55+
]
56+
},
4757
{
4858
"category": "operator",
4959
"description": "should stringify a custom operator",
5060
"options": {
5161
"operators": [{ "eq": "==", "aboutEq": "~=" }]
5262
},
5363
"tests": [
54-
{ "input": ["aboutEq", 2, 3], "output": "(2 ~= 3)" },
64+
{ "input": ["aboutEq", 2, 3], "output": "2 ~= 3" },
5565
{ "input": ["filter", ["aboutEq", 2, 3]], "output": "filter(2 ~= 3)" },
56-
{ "input": ["object", { "result": ["aboutEq", 2, 3] }], "output": "{ result: (2 ~= 3) }" },
57-
{ "input": ["eq", 2, 3], "output": "(2 == 3)" }
66+
{ "input": ["object", { "result": ["aboutEq", 2, 3] }], "output": "{ result: 2 ~= 3 }" },
67+
{ "input": ["eq", 2, 3], "output": "2 == 3" }
5868
]
5969
},
6070
{
@@ -246,7 +256,7 @@
246256
},
247257
{
248258
"input": ["filter", ["and", ["gte", ["get", "age"], 23], ["lte", ["get", "age"], 27]]],
249-
"output": "filter((.age >= 23) and (.age <= 27))"
259+
"output": "filter(.age >= 23 and .age <= 27)"
250260
},
251261
{
252262
"input": [

0 commit comments

Comments
 (0)