Skip to content

Commit 93da20d

Browse files
committed
Fix array comparison (#2)
Also uses edit distance to generate optimal seq for simple arrays. Fixes #1
1 parent 81af803 commit 93da20d

9 files changed

+210
-91
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,5 @@ _testmain.go
2222
*.exe
2323
*.test
2424
*.prof
25+
26+
.idea/

jsonpatch.go

+127-55
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,20 @@ import (
88
"strings"
99
)
1010

11-
var errBadJSONDoc = fmt.Errorf("Invalid JSON Document")
11+
var errBadJSONDoc = fmt.Errorf("invalid JSON Document")
1212

13-
type JsonPatchOperation struct {
13+
type Operation struct {
1414
Operation string `json:"op"`
1515
Path string `json:"path"`
1616
Value interface{} `json:"value,omitempty"`
1717
}
1818

19-
func (j *JsonPatchOperation) Json() string {
19+
func (j *Operation) Json() string {
2020
b, _ := json.Marshal(j)
2121
return string(b)
2222
}
2323

24-
func (j *JsonPatchOperation) MarshalJSON() ([]byte, error) {
24+
func (j *Operation) MarshalJSON() ([]byte, error) {
2525
var b bytes.Buffer
2626
b.WriteString("{")
2727
b.WriteString(fmt.Sprintf(`"op":"%s"`, j.Operation))
@@ -39,14 +39,14 @@ func (j *JsonPatchOperation) MarshalJSON() ([]byte, error) {
3939
return b.Bytes(), nil
4040
}
4141

42-
type ByPath []JsonPatchOperation
42+
type ByPath []Operation
4343

4444
func (a ByPath) Len() int { return len(a) }
4545
func (a ByPath) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
4646
func (a ByPath) Less(i, j int) bool { return a[i].Path < a[j].Path }
4747

48-
func NewPatch(operation, path string, value interface{}) JsonPatchOperation {
49-
return JsonPatchOperation{Operation: operation, Path: path, Value: value}
48+
func NewPatch(operation, path string, value interface{}) Operation {
49+
return Operation{Operation: operation, Path: path, Value: value}
5050
}
5151

5252
// CreatePatch creates a patch as specified in http://jsonpatch.com/
@@ -55,7 +55,7 @@ func NewPatch(operation, path string, value interface{}) JsonPatchOperation {
5555
// The function will return an array of JsonPatchOperations
5656
//
5757
// An error will be returned if any of the two documents are invalid.
58-
func CreatePatch(a, b []byte) ([]JsonPatchOperation, error) {
58+
func CreatePatch(a, b []byte) ([]Operation, error) {
5959
aI := map[string]interface{}{}
6060
bI := map[string]interface{}{}
6161
err := json.Unmarshal(a, &aI)
@@ -66,7 +66,7 @@ func CreatePatch(a, b []byte) ([]JsonPatchOperation, error) {
6666
if err != nil {
6767
return nil, errBadJSONDoc
6868
}
69-
return diff(aI, bI, "", []JsonPatchOperation{})
69+
return diff(aI, bI, "", []Operation{})
7070
}
7171

7272
// Returns true if the values matches (must be json types)
@@ -78,22 +78,25 @@ func matchesValue(av, bv interface{}) bool {
7878
}
7979
switch at := av.(type) {
8080
case string:
81-
bt := bv.(string)
82-
if bt == at {
81+
bt, ok := bv.(string)
82+
if ok && bt == at {
8383
return true
8484
}
8585
case float64:
86-
bt := bv.(float64)
87-
if bt == at {
86+
bt, ok := bv.(float64)
87+
if ok && bt == at {
8888
return true
8989
}
9090
case bool:
91-
bt := bv.(bool)
92-
if bt == at {
91+
bt, ok := bv.(bool)
92+
if ok && bt == at {
9393
return true
9494
}
9595
case map[string]interface{}:
96-
bt := bv.(map[string]interface{})
96+
bt, ok := bv.(map[string]interface{})
97+
if !ok {
98+
return false
99+
}
97100
for key := range at {
98101
if !matchesValue(at[key], bt[key]) {
99102
return false
@@ -106,7 +109,10 @@ func matchesValue(av, bv interface{}) bool {
106109
}
107110
return true
108111
case []interface{}:
109-
bt := bv.([]interface{})
112+
bt, ok := bv.([]interface{})
113+
if !ok {
114+
return false
115+
}
110116
if len(bt) != len(at) {
111117
return false
112118
}
@@ -148,7 +154,7 @@ func makePath(path string, newPart interface{}) string {
148154
}
149155

150156
// diff returns the (recursive) difference between a and b as an array of JsonPatchOperations.
151-
func diff(a, b map[string]interface{}, path string, patch []JsonPatchOperation) ([]JsonPatchOperation, error) {
157+
func diff(a, b map[string]interface{}, path string, patch []Operation) ([]Operation, error) {
152158
for key, bv := range b {
153159
p := makePath(path, key)
154160
av, ok := a[key]
@@ -157,11 +163,6 @@ func diff(a, b map[string]interface{}, path string, patch []JsonPatchOperation)
157163
patch = append(patch, NewPatch("add", p, bv))
158164
continue
159165
}
160-
// If types have changed, replace completely
161-
if reflect.TypeOf(av) != reflect.TypeOf(bv) {
162-
patch = append(patch, NewPatch("replace", p, bv))
163-
continue
164-
}
165166
// Types are the same, compare values
166167
var err error
167168
patch, err = handleValues(av, bv, p, patch)
@@ -181,7 +182,12 @@ func diff(a, b map[string]interface{}, path string, patch []JsonPatchOperation)
181182
return patch, nil
182183
}
183184

184-
func handleValues(av, bv interface{}, p string, patch []JsonPatchOperation) ([]JsonPatchOperation, error) {
185+
func handleValues(av, bv interface{}, p string, patch []Operation) ([]Operation, error) {
186+
// If types have changed, replace completely
187+
if reflect.TypeOf(av) != reflect.TypeOf(bv) {
188+
return append(patch, NewPatch("replace", p, bv)), nil
189+
}
190+
185191
var err error
186192
switch at := av.(type) {
187193
case map[string]interface{}:
@@ -195,16 +201,19 @@ func handleValues(av, bv interface{}, p string, patch []JsonPatchOperation) ([]J
195201
patch = append(patch, NewPatch("replace", p, bv))
196202
}
197203
case []interface{}:
198-
bt, ok := bv.([]interface{})
199-
if !ok {
200-
// array replaced by non-array
201-
patch = append(patch, NewPatch("replace", p, bv))
202-
} else if len(at) != len(bt) {
203-
// arrays are not the same length
204-
patch = append(patch, compareArray(at, bt, p)...)
205-
204+
bt := bv.([]interface{})
205+
if isSimpleArray(at) && isSimpleArray(bt) {
206+
patch = append(patch, compareEditDistance(at, bt, p)...)
206207
} else {
207-
for i := range bt {
208+
n := min(len(at), len(bt))
209+
for i := len(at) - 1; i >= n; i-- {
210+
patch = append(patch, NewPatch("remove", makePath(p, i), nil))
211+
}
212+
for i := n; i < len(bt); i++ {
213+
patch = append(patch, NewPatch("add", makePath(p, i), bt[i]))
214+
}
215+
for i := 0; i < n; i++ {
216+
var err error
208217
patch, err = handleValues(at[i], bt[i], makePath(p, i), patch)
209218
if err != nil {
210219
return nil, err
@@ -224,34 +233,97 @@ func handleValues(av, bv interface{}, p string, patch []JsonPatchOperation) ([]J
224233
return patch, nil
225234
}
226235

227-
func compareArray(av, bv []interface{}, p string) []JsonPatchOperation {
228-
retval := []JsonPatchOperation{}
229-
// var err error
230-
for i, v := range av {
231-
found := false
232-
for _, v2 := range bv {
233-
if reflect.DeepEqual(v, v2) {
234-
found = true
235-
break
236+
func isBasicType(a interface{}) bool {
237+
switch a.(type) {
238+
case string, float64, bool:
239+
default:
240+
return false
241+
}
242+
return true
243+
}
244+
245+
func isSimpleArray(a []interface{}) bool {
246+
for i := range a {
247+
switch a[i].(type) {
248+
case string, float64, bool:
249+
default:
250+
val := reflect.ValueOf(a[i])
251+
if val.Kind() == reflect.Map {
252+
for _, k := range val.MapKeys() {
253+
av := val.MapIndex(k)
254+
if av.Kind() == reflect.Ptr || av.Kind() == reflect.Interface {
255+
if av.IsNil() {
256+
continue
257+
}
258+
av = av.Elem()
259+
}
260+
if av.Kind() != reflect.String && av.Kind() != reflect.Float64 && av.Kind() != reflect.Bool {
261+
return false
262+
}
263+
}
264+
return true
236265
}
237-
}
238-
if !found {
239-
retval = append(retval, NewPatch("remove", makePath(p, i), nil))
266+
return false
240267
}
241268
}
269+
return true
270+
}
271+
272+
// https://en.wikipedia.org/wiki/Wagner%E2%80%93Fischer_algorithm
273+
// Adapted from https://github.com/texttheater/golang-levenshtein
274+
func compareEditDistance(s, t []interface{}, p string) []Operation {
275+
m := len(s)
276+
n := len(t)
277+
278+
d := make([][]int, m+1)
279+
for i := 0; i <= m; i++ {
280+
d[i] = make([]int, n+1)
281+
d[i][0] = i
282+
}
283+
for j := 0; j <= n; j++ {
284+
d[0][j] = j
285+
}
242286

243-
for i, v := range bv {
244-
found := false
245-
for _, v2 := range av {
246-
if reflect.DeepEqual(v, v2) {
247-
found = true
248-
break
287+
for j := 1; j <= n; j++ {
288+
for i := 1; i <= m; i++ {
289+
if reflect.DeepEqual(s[i-1], t[j-1]) {
290+
d[i][j] = d[i-1][j-1] // no op required
291+
} else {
292+
del := d[i-1][j] + 1
293+
add := d[i][j-1] + 1
294+
rep := d[i-1][j-1] + 1
295+
d[i][j] = min(rep, min(add, del))
249296
}
250297
}
251-
if !found {
252-
retval = append(retval, NewPatch("add", makePath(p, i), v))
253-
}
254298
}
255299

256-
return retval
300+
return backtrace(s, t, p, m, n, d)
301+
}
302+
303+
func min(x int, y int) int {
304+
if y < x {
305+
return y
306+
}
307+
return x
308+
}
309+
310+
func backtrace(s, t []interface{}, p string, i int, j int, matrix [][]int) []Operation {
311+
if i > 0 && matrix[i-1][j]+1 == matrix[i][j] {
312+
return append(backtrace(s, t, p, i-1, j, matrix), NewPatch("remove", makePath(p, i-1), nil))
313+
}
314+
if j > 0 && matrix[i][j-1]+1 == matrix[i][j] {
315+
return append(backtrace(s, t, p, i, j-1, matrix), NewPatch("add", makePath(p, i), t[j-1]))
316+
}
317+
if i > 0 && j > 0 && matrix[i-1][j-1]+1 == matrix[i][j] {
318+
if isBasicType(s[0]) {
319+
return append(backtrace(s, t, p, i-1, j-1, matrix), NewPatch("replace", makePath(p, i-1), t[j-1]))
320+
}
321+
322+
p2, _ := handleValues(s[j-1], t[j-1], makePath(p, i-1), []Operation{})
323+
return append(backtrace(s, t, p, i-1, j-1, matrix), p2...)
324+
}
325+
if i > 0 && j > 0 && matrix[i-1][j-1] == matrix[i][j] {
326+
return backtrace(s, t, p, i-1, j-1, matrix)
327+
}
328+
return []Operation{}
257329
}

jsonpatch_array_test.go

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package jsonpatch
2+
3+
import (
4+
"github.com/stretchr/testify/assert"
5+
"sort"
6+
"testing"
7+
)
8+
9+
var arraySrc = `
10+
{
11+
"spec": {
12+
"loadBalancerSourceRanges": [
13+
"192.101.0.0/16",
14+
"192.0.0.0/24"
15+
]
16+
}
17+
}
18+
`
19+
20+
var arrayDst = `
21+
{
22+
"spec": {
23+
"loadBalancerSourceRanges": [
24+
"192.101.0.0/24"
25+
]
26+
}
27+
}
28+
`
29+
30+
func TestArraySame(t *testing.T) {
31+
patch, e := CreatePatch([]byte(arraySrc), []byte(arraySrc))
32+
assert.NoError(t, e)
33+
assert.Equal(t, len(patch), 0, "they should be equal")
34+
}
35+
36+
func TestArrayBoolReplace(t *testing.T) {
37+
patch, e := CreatePatch([]byte(arraySrc), []byte(arrayDst))
38+
assert.NoError(t, e)
39+
assert.Equal(t, 2, len(patch), "they should be equal")
40+
sort.Sort(ByPath(patch))
41+
42+
change := patch[0]
43+
assert.Equal(t, "replace", change.Operation, "they should be equal")
44+
assert.Equal(t, "/spec/loadBalancerSourceRanges/0", change.Path, "they should be equal")
45+
assert.Equal(t, "192.101.0.0/24", change.Value, "they should be equal")
46+
change = patch[1]
47+
assert.Equal(t, change.Operation, "remove", "they should be equal")
48+
assert.Equal(t, change.Path, "/spec/loadBalancerSourceRanges/1", "they should be equal")
49+
assert.Equal(t, nil, change.Value, "they should be equal")
50+
}
51+
52+
func TestArrayAlmostSame(t *testing.T) {
53+
src := `{"Lines":[1,2,3,4,5,6,7,8,9,10]}`
54+
to := `{"Lines":[2,3,4,5,6,7,8,9,10,11]}`
55+
patch, e := CreatePatch([]byte(src), []byte(to))
56+
assert.NoError(t, e)
57+
assert.Equal(t, 2, len(patch), "they should be equal")
58+
sort.Sort(ByPath(patch))
59+
60+
change := patch[0]
61+
assert.Equal(t, "remove", change.Operation, "they should be equal")
62+
assert.Equal(t, "/Lines/0", change.Path, "they should be equal")
63+
assert.Equal(t, nil, change.Value, "they should be equal")
64+
change = patch[1]
65+
assert.Equal(t, change.Operation, "add", "they should be equal")
66+
assert.Equal(t, change.Path, "/Lines/10", "they should be equal")
67+
assert.Equal(t, float64(11), change.Value, "they should be equal")
68+
}

jsonpatch_complex_test.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
package jsonpatch
22

33
import (
4-
"github.com/stretchr/testify/assert"
54
"sort"
65
"testing"
6+
7+
"github.com/stretchr/testify/assert"
78
)
89

910
var complexBase = `{"a":100, "b":[{"c1":"hello", "d1":"foo"},{"c2":"hello2", "d2":"foo2"} ], "e":{"f":200, "g":"h", "i":"j"}}`

jsonpatch_geojson_test.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
package jsonpatch
22

33
import (
4-
"github.com/stretchr/testify/assert"
54
"sort"
65
"testing"
6+
7+
"github.com/stretchr/testify/assert"
78
)
89

910
var point = `{"type":"Point", "coordinates":[0.0, 1.0]}`

0 commit comments

Comments
 (0)