tidwall / sjson

Set JSON values very quickly in Go
MIT License
2.4k stars 165 forks source link

Insert value at array indice #75

Open wI2L opened 10 months ago

wI2L commented 10 months ago

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

wI2L commented 10 months ago

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

tidwall commented 10 months ago

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"]
tidwall commented 10 months ago

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.

wI2L commented 10 months ago

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:

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.