Skip to content

Commit

Permalink
Allow negative indexes in json operators (#7849)
Browse files Browse the repository at this point in the history
* Add unit tests for negative indexes in json ops

* Allow negative indexes in JSON operators

Negative indexes will be treated as counting from the end, so -1 means
last item of the array, -2 means next-to-last item, and so on.

* Add documentation for negative indexes
  • Loading branch information
rmunn committed Nov 21, 2023
1 parent bf8b3cf commit ab72cc7
Show file tree
Hide file tree
Showing 6 changed files with 72 additions and 5 deletions.
24 changes: 19 additions & 5 deletions core/modules/filters/json-ops.js
Expand Up @@ -213,6 +213,18 @@ function getDataItemType(data,indexes) {
}
}

function getItemAtIndex(item,index) {
if($tw.utils.hop(item,index)) {
return item[index];
} else if($tw.utils.isArray(item)) {
index = $tw.utils.parseInt(index);
if(index < 0) { index = index + item.length };
return item[index]; // Will be undefined if index was out-of-bounds
} else {
return undefined;
}
}

/*
Given a JSON data structure and an array of index strings, return the value at the end of the index chain, or "undefined" if any of the index strings are invalid
*/
Expand All @@ -225,7 +237,7 @@ function getDataItem(data,indexes) {
for(var i=0; i<indexes.length; i++) {
if(item !== undefined) {
if(item !== null && ["number","string","boolean"].indexOf(typeof item) === -1) {
item = item[indexes[i]];
item = getItemAtIndex(item,indexes[i]);
} else {
item = undefined;
}
Expand All @@ -249,16 +261,18 @@ function setDataItem(data,indexes,value) {
// Traverse the JSON data structure using the index chain
var current = data;
for(var i = 0; i < indexes.length - 1; i++) {
var index = indexes[i];
if($tw.utils.hop(current,index)) {
current = current[index];
} else {
current = getItemAtIndex(current,indexes[i]);
if(current === undefined) {
// Return the original JSON data structure if any of the index strings are invalid
return data;
}
}
// Add the value to the end of the index chain
var lastIndex = indexes[indexes.length - 1];
if($tw.utils.isArray(current)) {
lastIndex = $tw.utils.parseInt(lastIndex);
if(lastIndex < 0) { lastIndex = lastIndex + current.length };
}
current[lastIndex] = value;
return data;
}
Expand Down
21 changes: 21 additions & 0 deletions editions/test/tiddlers/tests/test-json-filters.js
Expand Up @@ -53,6 +53,11 @@ describe("json filter tests", function() {
expect(wiki.filterTiddlers("[{First}jsonget[d],[f],[2]]")).toEqual(["true"]);
expect(wiki.filterTiddlers("[{First}jsonget[d],[f],[3]]")).toEqual(["false"]);
expect(wiki.filterTiddlers("[{First}jsonget[d],[f],[4]]")).toEqual(["null"]);
expect(wiki.filterTiddlers("[{First}jsonget[d],[f],[-5]]")).toEqual(["five"]);
expect(wiki.filterTiddlers("[{First}jsonget[d],[f],[-4]]")).toEqual(["six"]);
expect(wiki.filterTiddlers("[{First}jsonget[d],[f],[-3]]")).toEqual(["true"]);
expect(wiki.filterTiddlers("[{First}jsonget[d],[f],[-2]]")).toEqual(["false"]);
expect(wiki.filterTiddlers("[{First}jsonget[d],[f],[-1]]")).toEqual(["null"]);
});

it("should support the jsonextract operator", function() {
Expand All @@ -70,6 +75,11 @@ describe("json filter tests", function() {
expect(wiki.filterTiddlers("[{First}jsonextract[d],[f],[2]]")).toEqual(["true"]);
expect(wiki.filterTiddlers("[{First}jsonextract[d],[f],[3]]")).toEqual(["false"]);
expect(wiki.filterTiddlers("[{First}jsonextract[d],[f],[4]]")).toEqual(["null"]);
expect(wiki.filterTiddlers("[{First}jsonextract[d],[f],[-5]]")).toEqual(['"five"']);
expect(wiki.filterTiddlers("[{First}jsonextract[d],[f],[-4]]")).toEqual(['"six"']);
expect(wiki.filterTiddlers("[{First}jsonextract[d],[f],[-3]]")).toEqual(["true"]);
expect(wiki.filterTiddlers("[{First}jsonextract[d],[f],[-2]]")).toEqual(["false"]);
expect(wiki.filterTiddlers("[{First}jsonextract[d],[f],[-1]]")).toEqual(["null"]);
});

it("should support the jsonindexes operator", function() {
Expand All @@ -85,6 +95,11 @@ describe("json filter tests", function() {
expect(wiki.filterTiddlers("[{First}jsonindexes[d],[f],[2]]")).toEqual([]);
expect(wiki.filterTiddlers("[{First}jsonindexes[d],[f],[3]]")).toEqual([]);
expect(wiki.filterTiddlers("[{First}jsonindexes[d],[f],[4]]")).toEqual([]);
expect(wiki.filterTiddlers("[{First}jsonindexes[d],[f],[-5]]")).toEqual([]);
expect(wiki.filterTiddlers("[{First}jsonindexes[d],[f],[-4]]")).toEqual([]);
expect(wiki.filterTiddlers("[{First}jsonindexes[d],[f],[-3]]")).toEqual([]);
expect(wiki.filterTiddlers("[{First}jsonindexes[d],[f],[-2]]")).toEqual([]);
expect(wiki.filterTiddlers("[{First}jsonindexes[d],[f],[-1]]")).toEqual([]);
});

it("should support the jsontype operator", function() {
Expand All @@ -101,6 +116,11 @@ describe("json filter tests", function() {
expect(wiki.filterTiddlers("[{First}jsontype[d],[f],[2]]")).toEqual(["boolean"]);
expect(wiki.filterTiddlers("[{First}jsontype[d],[f],[3]]")).toEqual(["boolean"]);
expect(wiki.filterTiddlers("[{First}jsontype[d],[f],[4]]")).toEqual(["null"]);
expect(wiki.filterTiddlers("[{First}jsontype[d],[f],[-5]]")).toEqual(["string"]);
expect(wiki.filterTiddlers("[{First}jsontype[d],[f],[-4]]")).toEqual(["string"]);
expect(wiki.filterTiddlers("[{First}jsontype[d],[f],[-3]]")).toEqual(["boolean"]);
expect(wiki.filterTiddlers("[{First}jsontype[d],[f],[-2]]")).toEqual(["boolean"]);
expect(wiki.filterTiddlers("[{First}jsontype[d],[f],[-1]]")).toEqual(["null"]);
});

it("should support the jsonset operator", function() {
Expand All @@ -115,6 +135,7 @@ describe("json filter tests", function() {
expect(wiki.filterTiddlers("[{First}jsonset:null[id]]")).toEqual(['{"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null]},"id":null}']);
expect(wiki.filterTiddlers("[{First}jsonset:array[d],[f],[5]]")).toEqual(['{"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null,[]]}}']);
expect(wiki.filterTiddlers("[{First}jsonset:object[d],[f],[5]]")).toEqual(['{"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null,{}]}}']);
expect(wiki.filterTiddlers("[{First}jsonset:number[d],[f],[-1],[42]]")).toEqual(['{"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,42]}}']);
expect(wiki.filterTiddlers("[{First}jsonset[missing],[id],[Antelope]]")).toEqual(['{"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null]}}']);
expect(wiki.filterTiddlers("[{First}jsonset:json[\"Antelope\"]]")).toEqual(['"Antelope"']);
expect(wiki.filterTiddlers("[{First}jsonset:json[id],[{\"a\":313}]]")).toEqual(['{"a":"one","b":"","c":1.618,"d":{"e":"four","f":["five","six",true,false,null]},"id":{"a":313}}']);
Expand Down
8 changes: 8 additions & 0 deletions editions/tw5.com/tiddlers/filters/jsonextract.tid
Expand Up @@ -53,6 +53,14 @@ The <<.op jsonextract>> operator uses multiple operands to specify the indexes o
[<jsondata>jsonextract[d],[g]] --> {"x":"max","y":"may","z":"maize"}
```

<<.from-version "5.3.2">> Negative indexes into an array are counted from the end, so -1 means the last item, -2 the next-to-last item, and so on:

```
[<jsondata>jsonextract[d],[f],[-1]] --> null
[<jsondata>jsonextract[d],[f],[-2]] --> false
[<jsondata>jsonextract[d],[f],[-4]] --> "six"
```

Indexes can be dynamically composed from variables and transclusions:

```
Expand Down
8 changes: 8 additions & 0 deletions editions/tw5.com/tiddlers/filters/jsonget.tid
Expand Up @@ -51,6 +51,14 @@ The <<.op jsonget>> operator uses multiple operands to specify the indexes of th
[<jsondata>jsonget[d],[f],[0]] --> "five"
```

<<.from-version "5.3.2">> Negative indexes into an array are counted from the end, so -1 means the last item, -2 the next-to-last item, and so on:

```
[<jsondata>jsonget[d],[f],[-1]] --> null
[<jsondata>jsonget[d],[f],[-2]] --> false
[<jsondata>jsonget[d],[f],[-4]] --> "six"
```

Indexes can be dynamically composed from variables and transclusions:

```
Expand Down
8 changes: 8 additions & 0 deletions editions/tw5.com/tiddlers/filters/jsonset.tid
Expand Up @@ -51,6 +51,14 @@ The <<.op jsonset>> operator uses multiple operands to specify the indexes of th
[<jsondata>jsonset[d],[f],[Panther]] --> {"a": "one","b": "","c": "three","d": "{"e": "four","f": "Panther","g": {"x": "max","y": "may","z": "maize"}}"}
```

Negative indexes into an array are counted from the end, so -1 means the last item, -2 the next-to-last item, and so on:

```
[<jsondata>jsonset[d],[f],[-1],[Elephant]] --> {"a": "one","b": "","c": "three","d": "{"e": "four","f": ["five","six",true,false,"Elephant"],"g": {"x": "max","y": "may","z": "maize"}}"}
[<jsondata>jsonset[d],[f],[-2],[Elephant]] --> {"a": "one","b": "","c": "three","d": "{"e": "four","f": ["five","six",true,"Elephant",null],"g": {"x": "max","y": "may","z": "maize"}}"}
[<jsondata>jsonset[d],[f],[-4],[Elephant]] --> {"a": "one","b": "","c": "three","d": "{"e": "four","f": ["five","Elephant",true,false,null],"g": {"x": "max","y": "may","z": "maize"}}"}
```

Indexes can be dynamically composed from variables and transclusions:

```
Expand Down
8 changes: 8 additions & 0 deletions editions/tw5.com/tiddlers/filters/jsontype.tid
Expand Up @@ -61,6 +61,14 @@ The <<.op jsontype>> operator uses multiple operands to specify the indexes of t
[<jsondata>jsontype[d],[f],[2]] --> "boolean"
```

<<.from-version "5.3.2">> Negative indexes into an array are counted from the end, so -1 means the last item, -2 the next-to-last item, and so on:

```
[<jsondata>jsontype[d],[f],[-1]] --> "null"
[<jsondata>jsontype[d],[f],[-2]] --> "boolean"
[<jsondata>jsontype[d],[f],[-4]] --> "string"
```

Indexes can be dynamically composed from variables and transclusions:

```
Expand Down

1 comment on commit ab72cc7

@vercel
Copy link

@vercel vercel bot commented on ab72cc7 Nov 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

tiddlywiki5 – ./

tiddlywiki5-git-master-jermolene.vercel.app
tiddlywiki5-jermolene.vercel.app
tiddlywiki5.vercel.app

Please sign in to comment.