tidwall / sjson

Set JSON values very quickly in Go

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Insert value at array indice

wI2L opened this issue · comments

Hello @tidwall,

I have a use case where I need to "insert" a value at a specific indice in an array. However, the current behavior with SetBytes is a replacement if a value already exist at the given indice.

Consider the following example (which show the current behavior):

package main

import (
	"fmt"

	"github.com/tidwall/sjson"
)

func main() {
	var a = `["a","b","c"]`
	var b = `["a","b","c"]`

	a2, err := sjson.SetBytes([]byte(a), "0", "d")
	if err != nil {
		fmt.Println(err)
	}
	b2, err := sjson.SetBytes([]byte(b), "1", "d")
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println(string(a2))
	fmt.Println(string(b2))
}

The output is:

["d","b","c"]
["a","d","c"]

Instead, I'd like to be able to insert the values, effectively shifting all the other elements of the array to the right. The output would then be:

["d","a","b","c"]
["a","d","b","c"]

I searched for alternatives using sjson or gjson, but haven't found a way to do it with the current version of the packages.

Do you have any idea to achieve that, or would you be open to add this behavior as a feature (perhaps via a path modifier) ?

Thanks

Note that I'd be willing to implement this feature if we can agree on the right implementation.

Here's one idea.
First use gjson to get substring information about the original item.
Then create a new json string by adding a "null" element in place of where the new item will exist.
Finally use sjson to do the replacement.

package main

import (
	"fmt"

	"github.com/tidwall/gjson"
	"github.com/tidwall/sjson"
)

func main() {
	var a = `["a","b","c"]`
	var b = `["a","b","c"]`

	a2, err := sjson.SetBytes([]byte(a), "0", "d")
	if err != nil {
		fmt.Println(err)
	}
	b2, err := sjson.SetBytes([]byte(b), "1", "d")
	if err != nil {
		fmt.Println(err)
	}

	fmt.Println(string(a2))
	fmt.Println(string(b2))

	// Here we'll insert a "dummy" null element
	res := gjson.Get(b, "1")
	if res.Index > 0 {
		b = b[:res.Index] + "null," + b[res.Index:]
	}

	// And now it works
	b2, err = sjson.SetBytes([]byte(b), "1", "d")
	if err != nil {
		fmt.Println(err)
	}

	fmt.Println(string(b2))
}
["d","b","c"]
["a","d","c"]
["a","d","b","c"]

Probably the less hacky way to do it would be to extract the values into a Go array then modify that array by adding the adding the new element(s) at the index. Then reserializing the array into json.

This seems to effectively be the way the splice function in Javascript works.

Maybe adding a splice feature to sjson or gjson would make sense.

Thanks for your answer, and the idea.

It actually isn't very different to how I'd imagined an implementation of that feature in sjson directly:

  • fetch the position index of the item using gjson
  • if the index is > 0, add the new item in the string/byte slice directly before that position
  • otherwise, the current behavior is applied, setting the value at the index (with null eventually preprended before it)

This still require to know whether to add a , after the inserted element, if it's the last element of the array or not. I guess we could simply check if the next char is a ] signaling the end of the array. Actually, if r.Index > 0, then the array item exists, and we prepend a new item before it, so the comma must be inserted every time.

The "insert" behavior could be enabled using a new field in the sjson.Options struct.

Regarding you latter comment, this would inherently produce an allocation to hold the deserialized elements of the array, which I would avoid if I could, but that's debatable.