Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 30 additions & 7 deletions src/jsonpath.js
Original file line number Diff line number Diff line change
Expand Up @@ -707,7 +707,7 @@ class JSONPathClass {
return retObj;
} else if (loc === '$') { // root only
addRet(this._trace(x, val, path, null, null, callback, hasArrExpr));
} else if ((/^(-?\d*):(-?\d*):?(\d*)$/v).test(loc)) { // [start:end:step] Python slice syntax
} else if ((/^(-?\d*):(-?\d*):?(-?\d*)$/v).test(loc)) { // [start:end:step] Python slice syntax
const sliceResult = this._slice(
loc, x, val, path, parent, parentPropName, callback
);
Expand Down Expand Up @@ -984,14 +984,37 @@ class JSONPathClass {
return undefined;
}
const len = val.length, parts = loc.split(':'),
step = (parts[2] && Number(parts[2])) || 1;
let start = (parts[0] && Number(parts[0])) || 0,
end = parts[1] ? Number(parts[1]) : len;
start = (start < 0) ? Math.max(0, start + len) : Math.min(len, start);
end = (end < 0) ? Math.max(0, end + len) : Math.min(len, end);
step = parts[2] ? Number(parts[2]) : 1;
/** @type {ReturnObject[]} */
const ret = [];
for (let i = start; i < end; i += step) {
// A step of zero selects nothing (RFC 9535 section 2.3.4.2.2)
if (step === 0) {
return ret;
}
/**
* @param {number} index
* @returns {number}
*/
const normalize = (index) => (index < 0 ? index + len : index);
// With a negative step the defaults run backwards: from the last
// item to (and including) the first
const start = normalize(
parts[0] ? Number(parts[0]) : (step > 0 ? 0 : len - 1)
),
end = normalize(
parts[1] ? Number(parts[1]) : (step > 0 ? len : -len - 1)
);
// The first and last positions to visit, whichever way we are going
const [from, to] = step > 0
? [
Math.min(Math.max(start, 0), len),
Math.min(Math.max(end, 0), len)
]
: [
Math.min(Math.max(start, -1), len - 1),
Math.min(Math.max(end, -1), len - 1)
];
for (let i = from; step > 0 ? i < to : i > to; i += step) {
const tmp = this._trace(
unshift(i, expr),
val,
Expand Down
46 changes: 46 additions & 0 deletions test/test.slice.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,49 @@ describe('JSONPath - slice', function () {
}), []);
});
});

describe('JSONPath - slice with a negative step', function () {
const small = [0, 1, 2, 3];
const digits = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

// From RFC 9535 section 2.3.4 and the JSONPath compliance test suite
/** @type {[string, number[], number[]][]} */
const cases = [
['$[::-1]', small, [3, 2, 1, 0]],
['$[:0:-1]', small, [3, 2, 1]],
['$[2::-1]', small, [2, 1, 0]],
['$[::-2]', small, [3, 1]],
['$[-1:-3:-1]', digits, [9, 8]],
['$[-1:-6:-2]', digits, [9, 7, 5]],
['$[-1:-7:-2]', digits, [9, 7, 5]],
['$[-1:1:-1]', digits, [9, 8, 7, 6, 5, 4, 3, 2]],
['$[7:-5:-1]', digits, [7, 6]],
['$[9:0:-1]', digits, [9, 8, 7, 6, 5, 4, 3, 2, 1]],
['$[113667776004:0:-1]', digits, [9, 8, 7, 6, 5, 4, 3, 2, 1]],
['$[3:-113667776004:-1]', digits, [3, 2, 1, 0]],
['$[-1:-10:-113667776004]', digits, [9]],
['$[::-1]', [], []]
];

for (const [path, json, expected] of cases) {
it(`should evaluate ${path} on ${JSON.stringify(json)}`, function () {
assert.deepEqual(jsonpath({json, path}), expected);
});
}

it('should reverse objects and report their paths', function () {
const json = {children: [{a: 1}, {a: 2}, {a: 3}]};
assert.deepEqual(
jsonpath({json, path: '$.children[::-1].a'}), [3, 2, 1]
);
assert.deepEqual(
jsonpath({json, path: '$.children[::-2]', resultType: 'path'}),
["$['children'][2]", "$['children'][0]"]
);
});

it('should select nothing for a step of zero', function () {
assert.deepEqual(jsonpath({json: digits, path: '$[1:2:0]'}), []);
assert.deepEqual(jsonpath({json: digits, path: '$[::0]'}), []);
});
});
Loading