123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426 |
- // Package gojsondiff implements "Diff" that compares two JSON objects and
- // generates Deltas that describes differences between them. The package also
- // provides "Patch" that apply Deltas to a JSON object.
- package gojsondiff
- import (
- "container/list"
- "encoding/json"
- "reflect"
- "sort"
- dmp "github.com/sergi/go-diff/diffmatchpatch"
- "github.com/yudai/golcs"
- )
- // A Diff holds deltas generated by a Differ
- type Diff interface {
- // Deltas returns Deltas that describe differences between two JSON objects
- Deltas() []Delta
- // Modified returnes true if Diff has at least one Delta.
- Modified() bool
- }
- type diff struct {
- deltas []Delta
- }
- func (diff *diff) Deltas() []Delta {
- return diff.deltas
- }
- func (diff *diff) Modified() bool {
- return len(diff.deltas) > 0
- }
- // A Differ conmapres JSON objects and apply patches
- type Differ struct {
- textDiffMinimumLength int
- }
- // New returns new Differ with default configuration
- func New() *Differ {
- return &Differ{
- textDiffMinimumLength: 30,
- }
- }
- // Compare compares two JSON strings as []bytes and return a Diff object.
- func (differ *Differ) Compare(
- left []byte,
- right []byte,
- ) (Diff, error) {
- var leftMap, rightMap map[string]interface{}
- err := json.Unmarshal(left, &leftMap)
- if err != nil {
- return nil, err
- }
- err = json.Unmarshal(right, &rightMap)
- if err != nil {
- return nil, err
- }
- return differ.CompareObjects(leftMap, rightMap), nil
- }
- // CompareObjects compares two JSON object as map[string]interface{}
- // and return a Diff object.
- func (differ *Differ) CompareObjects(
- left map[string]interface{},
- right map[string]interface{},
- ) Diff {
- deltas := differ.compareMaps(left, right)
- return &diff{deltas: deltas}
- }
- // CompareArrays compares two JSON arrays as []interface{}
- // and return a Diff object.
- func (differ *Differ) CompareArrays(
- left []interface{},
- right []interface{},
- ) Diff {
- deltas := differ.compareArrays(left, right)
- return &diff{deltas: deltas}
- }
- func (differ *Differ) compareMaps(
- left map[string]interface{},
- right map[string]interface{},
- ) (deltas []Delta) {
- deltas = make([]Delta, 0)
- names := sortedKeys(left) // stabilize delta order
- for _, name := range names {
- if rightValue, ok := right[name]; ok {
- same, delta := differ.compareValues(Name(name), left[name], rightValue)
- if !same {
- deltas = append(deltas, delta)
- }
- } else {
- deltas = append(deltas, NewDeleted(Name(name), left[name]))
- }
- }
- names = sortedKeys(right) // stabilize delta order
- for _, name := range names {
- if _, ok := left[name]; !ok {
- deltas = append(deltas, NewAdded(Name(name), right[name]))
- }
- }
- return deltas
- }
- // ApplyPatch applies a Diff to an JSON object. This method is destructive.
- func (differ *Differ) ApplyPatch(json map[string]interface{}, patch Diff) {
- applyDeltas(patch.Deltas(), json)
- }
- type maybe struct {
- index int
- lcsIndex int
- item interface{}
- }
- func (differ *Differ) compareArrays(
- left []interface{},
- right []interface{},
- ) (deltas []Delta) {
- deltas = make([]Delta, 0)
- // LCS index pairs
- lcsPairs := lcs.New(left, right).IndexPairs()
- // list up items not in LCS, they are maybe deleted
- maybeDeleted := list.New() // but maybe moved or modified
- lcsI := 0
- for i, leftValue := range left {
- if lcsI < len(lcsPairs) && lcsPairs[lcsI].Left == i {
- lcsI++
- } else {
- maybeDeleted.PushBack(maybe{index: i, lcsIndex: lcsI, item: leftValue})
- }
- }
- // list up items not in LCS, they are maybe Added
- maybeAdded := list.New() // but maybe moved or modified
- lcsI = 0
- for i, rightValue := range right {
- if lcsI < len(lcsPairs) && lcsPairs[lcsI].Right == i {
- lcsI++
- } else {
- maybeAdded.PushBack(maybe{index: i, lcsIndex: lcsI, item: rightValue})
- }
- }
- // find moved items
- var delNext *list.Element // for prefetch to remove item in iteration
- for delCandidate := maybeDeleted.Front(); delCandidate != nil; delCandidate = delNext {
- delCan := delCandidate.Value.(maybe)
- delNext = delCandidate.Next()
- for addCandidate := maybeAdded.Front(); addCandidate != nil; addCandidate = addCandidate.Next() {
- addCan := addCandidate.Value.(maybe)
- if reflect.DeepEqual(delCan.item, addCan.item) {
- deltas = append(deltas, NewMoved(Index(delCan.index), Index(addCan.index), delCan.item, nil))
- maybeAdded.Remove(addCandidate)
- maybeDeleted.Remove(delCandidate)
- break
- }
- }
- }
- // find modified or add+del
- prevIndexDel := 0
- prevIndexAdd := 0
- delElement := maybeDeleted.Front()
- addElement := maybeAdded.Front()
- for i := 0; i <= len(lcsPairs); i++ { // not "< len(lcsPairs)"
- var lcsPair lcs.IndexPair
- var delSize, addSize int
- if i < len(lcsPairs) {
- lcsPair = lcsPairs[i]
- delSize = lcsPair.Left - prevIndexDel - 1
- addSize = lcsPair.Right - prevIndexAdd - 1
- prevIndexDel = lcsPair.Left
- prevIndexAdd = lcsPair.Right
- }
- var delSlice []maybe
- if delSize > 0 {
- delSlice = make([]maybe, 0, delSize)
- } else {
- delSlice = make([]maybe, 0, maybeDeleted.Len())
- }
- for ; delElement != nil; delElement = delElement.Next() {
- d := delElement.Value.(maybe)
- if d.lcsIndex != i {
- break
- }
- delSlice = append(delSlice, d)
- }
- var addSlice []maybe
- if addSize > 0 {
- addSlice = make([]maybe, 0, addSize)
- } else {
- addSlice = make([]maybe, 0, maybeAdded.Len())
- }
- for ; addElement != nil; addElement = addElement.Next() {
- a := addElement.Value.(maybe)
- if a.lcsIndex != i {
- break
- }
- addSlice = append(addSlice, a)
- }
- if len(delSlice) > 0 && len(addSlice) > 0 {
- var bestDeltas []Delta
- bestDeltas, delSlice, addSlice = differ.maximizeSimilarities(delSlice, addSlice)
- for _, delta := range bestDeltas {
- deltas = append(deltas, delta)
- }
- }
- for _, del := range delSlice {
- deltas = append(deltas, NewDeleted(Index(del.index), del.item))
- }
- for _, add := range addSlice {
- deltas = append(deltas, NewAdded(Index(add.index), add.item))
- }
- }
- return deltas
- }
- func (differ *Differ) compareValues(
- position Position,
- left interface{},
- right interface{},
- ) (same bool, delta Delta) {
- if reflect.TypeOf(left) != reflect.TypeOf(right) {
- return false, NewModified(position, left, right)
- }
- switch left.(type) {
- case map[string]interface{}:
- l := left.(map[string]interface{})
- childDeltas := differ.compareMaps(l, right.(map[string]interface{}))
- if len(childDeltas) > 0 {
- return false, NewObject(position, childDeltas)
- }
- case []interface{}:
- l := left.([]interface{})
- childDeltas := differ.compareArrays(l, right.([]interface{}))
- if len(childDeltas) > 0 {
- return false, NewArray(position, childDeltas)
- }
- default:
- if !reflect.DeepEqual(left, right) {
- if reflect.ValueOf(left).Kind() == reflect.String &&
- reflect.ValueOf(right).Kind() == reflect.String &&
- differ.textDiffMinimumLength <= len(left.(string)) {
- textDiff := dmp.New()
- patchs := textDiff.PatchMake(left.(string), right.(string))
- return false, NewTextDiff(position, patchs, left, right)
- } else {
- return false, NewModified(position, left, right)
- }
- }
- }
- return true, nil
- }
- func applyDeltas(deltas []Delta, object interface{}) interface{} {
- preDeltas := make(preDeltas, 0)
- for _, delta := range deltas {
- switch delta.(type) {
- case PreDelta:
- preDeltas = append(preDeltas, delta.(PreDelta))
- }
- }
- sort.Sort(preDeltas)
- for _, delta := range preDeltas {
- object = delta.PreApply(object)
- }
- postDeltas := make(postDeltas, 0, len(deltas)-len(preDeltas))
- for _, delta := range deltas {
- switch delta.(type) {
- case PostDelta:
- postDeltas = append(postDeltas, delta.(PostDelta))
- }
- }
- sort.Sort(postDeltas)
- for _, delta := range postDeltas {
- object = delta.PostApply(object)
- }
- return object
- }
- func (differ *Differ) maximizeSimilarities(left []maybe, right []maybe) (resultDeltas []Delta, freeLeft, freeRight []maybe) {
- deltaTable := make([][]Delta, len(left))
- for i := 0; i < len(left); i++ {
- deltaTable[i] = make([]Delta, len(right))
- }
- for i, leftValue := range left {
- for j, rightValue := range right {
- _, delta := differ.compareValues(Index(rightValue.index), leftValue.item, rightValue.item)
- deltaTable[i][j] = delta
- }
- }
- sizeX := len(left) + 1 // margins for both sides
- sizeY := len(right) + 1
- // fill out with similarities
- dpTable := make([][]float64, sizeX)
- for i := 0; i < sizeX; i++ {
- dpTable[i] = make([]float64, sizeY)
- }
- for x := sizeX - 2; x >= 0; x-- {
- for y := sizeY - 2; y >= 0; y-- {
- prevX := dpTable[x+1][y]
- prevY := dpTable[x][y+1]
- score := deltaTable[x][y].Similarity() + dpTable[x+1][y+1]
- dpTable[x][y] = max(prevX, prevY, score)
- }
- }
- minLength := len(left)
- if minLength > len(right) {
- minLength = len(right)
- }
- maxInvalidLength := minLength - 1
- freeLeft = make([]maybe, 0, len(left)-minLength)
- freeRight = make([]maybe, 0, len(right)-minLength)
- resultDeltas = make([]Delta, 0, minLength)
- var x, y int
- for x, y = 0, 0; x <= sizeX-2 && y <= sizeY-2; {
- current := dpTable[x][y]
- nextX := dpTable[x+1][y]
- nextY := dpTable[x][y+1]
- xValidLength := len(left) - maxInvalidLength + y
- yValidLength := len(right) - maxInvalidLength + x
- if x+1 < xValidLength && current == nextX {
- freeLeft = append(freeLeft, left[x])
- x++
- } else if y+1 < yValidLength && current == nextY {
- freeRight = append(freeRight, right[y])
- y++
- } else {
- resultDeltas = append(resultDeltas, deltaTable[x][y])
- x++
- y++
- }
- }
- for ; x < sizeX-1; x++ {
- freeLeft = append(freeLeft, left[x-1])
- }
- for ; y < sizeY-1; y++ {
- freeRight = append(freeRight, right[y-1])
- }
- return resultDeltas, freeLeft, freeRight
- }
- func deltasSimilarity(deltas []Delta) (similarity float64) {
- for _, delta := range deltas {
- similarity += delta.Similarity()
- }
- similarity = similarity / float64(len(deltas))
- return
- }
- func stringSimilarity(left, right string) (similarity float64) {
- matchingLength := float64(
- lcs.New(
- stringToInterfaceSlice(left),
- stringToInterfaceSlice(right),
- ).Length(),
- )
- similarity =
- (matchingLength / float64(len(left))) * (matchingLength / float64(len(right)))
- return
- }
- func stringToInterfaceSlice(str string) []interface{} {
- s := make([]interface{}, len(str))
- for i, v := range str {
- s[i] = v
- }
- return s
- }
- func sortedKeys(m map[string]interface{}) (keys []string) {
- keys = make([]string, 0, len(m))
- for key, _ := range m {
- keys = append(keys, key)
- }
- sort.Strings(keys)
- return
- }
- func max(first float64, rest ...float64) (max float64) {
- max = first
- for _, value := range rest {
- if max < value {
- max = value
- }
- }
- return max
- }
|