Skip to content

Commit 36678da

Browse files
authored
feat: deep equal support (#20)
1 parent 800f1ee commit 36678da

File tree

5 files changed

+568
-26
lines changed

5 files changed

+568
-26
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Try it out on the online playground: <https://jsonquerylang.org>
1010

1111
## Features
1212

13-
- Small: just `3.7 kB` when minified and gzipped! The JSON query engine without parse/stringify is only `1.7 kB`.
13+
- Small: just `3.9 kB` when minified and gzipped! The JSON query engine without parse/stringify is only `1.9 kB`.
1414
- Feature rich (50+ powerful functions and operators)
1515
- Easy to interoperate with thanks to the intermediate JSON format.
1616
- Expressive

src/compile.test.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -109,16 +109,6 @@ describe('error handling', () => {
109109
])
110110
})
111111

112-
test('should do nothing when sorting objects without a getter', () => {
113-
const data = [{ a: 1 }, { c: 3 }, { b: 2 }]
114-
expect(go(data, ['sort'])).toEqual(data)
115-
})
116-
117-
test('should not crash when sorting a list with nested arrays', () => {
118-
expect(go([[3], [7], [4]], ['sort'])).toEqual([[3], [4], [7]])
119-
expect(go([[], [], []], ['sort'])).toEqual([[], [], []])
120-
})
121-
122112
test('should throw an error when calculating the sum of an empty array', () => {
123113
expect(() => go([], ['sum'])).toThrow('Reduce of empty array with no initial value')
124114
})

src/functions.ts

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { compile } from './compile'
2-
import { isArray } from './is'
2+
import { isArray, isEqual } from './is'
33
import type {
44
Entry,
55
FunctionBuilder,
@@ -27,6 +27,20 @@ export function buildFunction(fn: (...args: unknown[]) => unknown): FunctionBuil
2727
}
2828
}
2929

30+
const gt = (a: unknown, b: unknown) => {
31+
if (
32+
(typeof a === 'number' && typeof b === 'number') ||
33+
(typeof a === 'string' && typeof b === 'string')
34+
) {
35+
return a > b
36+
}
37+
38+
throw new TypeError('Two numbers or two strings expected')
39+
}
40+
const gte = (a: unknown, b: unknown) => isEqual(a, b) || gt(a, b)
41+
const lt = (a: unknown, b: unknown) => gt(b, a)
42+
const lte = (a: unknown, b: unknown) => gte(b, a)
43+
3044
export const functions: FunctionBuildersMap = {
3145
pipe: (...entries: JSONQuery[]) => {
3246
const _entries = entries.map((entry) => compile(entry))
@@ -130,7 +144,7 @@ export const functions: FunctionBuildersMap = {
130144
function compare(itemA: unknown, itemB: unknown) {
131145
const a = getter(itemA)
132146
const b = getter(itemB)
133-
return a > b ? sign : a < b ? -sign : 0
147+
return gt(a, b) ? sign : lt(a, b) ? -sign : 0
134148
}
135149

136150
return (data: T[]) => data.slice().sort(compare)
@@ -216,7 +230,17 @@ export const functions: FunctionBuildersMap = {
216230

217231
uniq:
218232
() =>
219-
<T>(data: T[]) => [...new Set(data)],
233+
<T>(data: T[]) => {
234+
const res: T[] = []
235+
236+
for (const item of data) {
237+
if (!res.find((resItem) => isEqual(resItem, item))) {
238+
res.push(item)
239+
}
240+
}
241+
242+
return res
243+
},
220244

221245
uniqBy:
222246
<T>(path: JSONQueryProperty) =>
@@ -268,11 +292,16 @@ export const functions: FunctionBuildersMap = {
268292

269293
return (data: unknown) => (truthy(_condition(data)) ? _valueIfTrue(data) : _valueIfFalse(data))
270294
},
271-
in: (path: string, values: JSONQuery) => {
272-
const getter = compile(path)
273-
const _values = compile(values)
295+
in: (value: JSONQuery, values: JSONQuery) => {
296+
const getValue = compile(value)
297+
const getValues = compile(values)
274298

275-
return (data: unknown) => (_values(data) as string[]).includes(getter(data) as string)
299+
return (data: unknown) => {
300+
const _value = getValue(data)
301+
const _values = getValues(data) as unknown[]
302+
303+
return !!_values.find((item) => isEqual(item, _value))
304+
}
276305
},
277306
'not in': (path: string, values: JSONQuery) => {
278307
const _in = functions.in(path, values)
@@ -286,12 +315,12 @@ export const functions: FunctionBuildersMap = {
286315
return (data: unknown) => regex.test(getter(data) as string)
287316
},
288317

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),
318+
eq: buildFunction(isEqual),
319+
gt: buildFunction(gt),
320+
gte: buildFunction(gte),
321+
lt: buildFunction(lt),
322+
lte: buildFunction(lte),
323+
ne: buildFunction((a, b) => !isEqual(a, b)),
295324

296325
add: buildFunction((a: number, b: number) => a + b),
297326
subtract: buildFunction((a: number, b: number) => a - b),

src/is.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,18 @@ export const isObject = (value: unknown): value is object =>
44
value && typeof value === 'object' && !isArray(value)
55

66
export const isString = (value: unknown): value is string => typeof value === 'string'
7+
8+
// source: https://stackoverflow.com/a/77278013/1262753
9+
export const isEqual = <T>(a: T, b: T): boolean => {
10+
if (a === b) {
11+
return true
12+
}
13+
14+
const bothObject = a && b && typeof a === 'object' && typeof b === 'object'
15+
16+
return (
17+
bothObject &&
18+
Object.keys(a).length === Object.keys(b).length &&
19+
Object.entries(a).every(([k, v]) => isEqual(v, b[k as keyof T]))
20+
)
21+
}

0 commit comments

Comments
 (0)