jsonpatch icon indicating copy to clipboard operation
jsonpatch copied to clipboard

Improvable patch

Open jpillora opened this issue 9 years ago • 8 comments

Thanks for making this project :) See program and output. It would nice if it instead produced a 2 operation patch: remove 0 and add 10 "..." instead of 10 replaces. (Side question: do removes need to contain the removed value?)

package main

import (
    "encoding/json"
    "log"

    "github.com/mattbaird/jsonpatch"
)

type Log struct {
    Lines []int
}

func main() {

    size := 10

    lines := make([]int, size)
    for i := 0; i < size; i++ {
        lines[i] = i + 1
    }

    a, _ := json.Marshal(&Log{Lines: lines})
    log.Printf("%s", a)

    //remove from the front
    lines = lines[1:]
    //add to the end
    lines = append(lines, size+1)

    b, _ := json.Marshal(&Log{Lines: lines})
    log.Printf("%s", b)

    ops, err := jsonpatch.CreatePatch(a, b)
    if err != nil {
        log.Fatal(err)
    }

    delta, _ := json.MarshalIndent(ops, "", "  ")
    log.Printf("%s", delta)
}
$ go run patch.go
2015/07/11 17:22:28 {"Lines":[1,2,3,4,5,6,7,8,9,10]}
2015/07/11 17:22:28 {"Lines":[2,3,4,5,6,7,8,9,10,11]}
2015/07/11 17:22:28 [
  {
    "op": "replace",
    "path": "/Lines/0",
    "value": 2
  },
  {
    "op": "replace",
    "path": "/Lines/1",
    "value": 3
  },
  {
    "op": "replace",
    "path": "/Lines/2",
    "value": 4
  },
  {
    "op": "replace",
    "path": "/Lines/3",
    "value": 5
  },
  {
    "op": "replace",
    "path": "/Lines/4",
    "value": 6
  },
  {
    "op": "replace",
    "path": "/Lines/5",
    "value": 7
  },
  {
    "op": "replace",
    "path": "/Lines/6",
    "value": 8
  },
  {
    "op": "replace",
    "path": "/Lines/7",
    "value": 9
  },
  {
    "op": "replace",
    "path": "/Lines/8",
    "value": 10
  },
  {
    "op": "replace",
    "path": "/Lines/9",
    "value": 11
  }
]

jpillora avatar Jul 11 '15 07:07 jpillora

When I use a map[string]int instead, it works as expected:

[
  {
    "op": "add",
    "path": "/Lines/10",
    "value": 11
  },
  {
    "op": "remove",
    "path": "/Lines/0"
  }
]

And it does leave out the value for remove :blush:

jpillora avatar Jul 11 '15 13:07 jpillora

hi @jpillora - Yah I think I can probably do this, however it would also be cool if you wanted to hack a bit :)

mattbaird avatar Jul 11 '15 15:07 mattbaird

I was thinking https://en.wikipedia.org/wiki/Longest_common_subsequence_problem could be used when comparing two arrays. Since it works on strings and characters, it should work on arrays of interfaces. Only issue is it might be slow. Let me know if you want to try implementing this otherwise I'll give it a try.

On Sunday, July 12, 2015, Matthew Baird [email protected] wrote:

hi @jpillora https://github.com/jpillora - Yah I think I can probably do this, however it would also be cool if you wanted to hack a bit :)

— Reply to this email directly or view it on GitHub https://github.com/mattbaird/jsonpatch/issues/1#issuecomment-120634495.

jpillora avatar Jul 11 '15 23:07 jpillora

@jpillora I'd love for you to take a crack at it. Sounds like an interesting algo, and I'd love to however my time is limited.

mattbaird avatar Jul 15 '15 22:07 mattbaird

hi @jpillora did you ever implement this?

mattbaird avatar Aug 31 '16 16:08 mattbaird

I have not yet. Though I would still find it very useful! I make heavy use of this package via https://github.com/jpillora/velox (forked to encode slashes - no longer adhears to the spec). Wow over a year old now, how time flies...

Edit: Didn't mean to close, though you can close if you like as I won't have time for this in the foreseeable future

jpillora avatar Aug 31 '16 16:08 jpillora

Oyea I just remembered, I took a crack at the algorithm 6 months ago https://github.com/jpillora/lcs though it currently only works on strings

jpillora avatar Aug 31 '16 16:08 jpillora

I found that it has a smarter behavior when the original array has a different size from the new array. In this example I remove the first user of the list (Joe), and add a new user to the end of the list (Thomas):

printPatch := func(oldList []string, newList []string) {
	oldListJson, _ := json.Marshal(oldList)
	newListJson, _ := json.Marshal(newList)

	patches, _ := jsonpatch.CreatePatch(oldListJson, newListJson)
	patchBytes, _ := json.MarshalIndent(patches, "", "  ")

	fmt.Printf("Result: %s\n", string(patchBytes))
}

originalList := []string{"Joe", "Alice", "Bob", "Charlie", "Daniel"}
newList := originalList[1:]         //Remove Joe
newList = append(newList, "Thomas") //Add Thomas
printPatch(originalList, newList)   // Print first result

newList = append(newList, "")     // Add an empty user
printPatch(originalList, newList) // Print second result
  • Result 1 (both arrays with the same size):
[
  {
    "op": "replace",
    "path": "/0",
    "value": "Alice"
  },
  {
    "op": "replace",
    "path": "/1",
    "value": "Bob"
  },
  {
    "op": "replace",
    "path": "/2",
    "value": "Charlie"
  },
  {
    "op": "replace",
    "path": "/3",
    "value": "Daniel"
  },
  {
    "op": "replace",
    "path": "/4",
    "value": "Thomas"
  }
]
  • Result 2 (the second array had an empty string appended newList = append(newList, "")):
[
  {
    "op": "remove",
    "path": "/0"
  },
  {
    "op": "add",
    "path": "/4",
    "value": "Thomas"
  },
  {
    "op": "add",
    "path": "/5",
    "value": ""
  }
]

It seems if I add an empty object to the array to force the new array to have a different size. Then the generated Patch is really smarter!

How can I force the smarter comparison without having to add an empty object?

gustavotrott avatar Apr 06 '24 14:04 gustavotrott