1
+ var _ = require ( 'lodash' ) ;
2
+
3
+ module . exports = diff ;
4
+
5
+ function diff ( original , modified ) {
6
+ var omnom = new Omnom ( {
7
+ cautious : true ,
8
+ orderedArrays : true
9
+ } ) ;
10
+
11
+ omnom . diff ( original , modified ) ;
12
+
13
+ return omnom . changes ;
14
+ }
15
+
16
+ function Omnom ( options ) {
17
+ this . options = options ;
18
+ this . changes = { } ;
19
+ }
20
+
21
+ Omnom . prototype . diff = function ( original , modified ) {
22
+ this . onObject ( original , modified ) ;
23
+ } ;
24
+
25
+ Omnom . prototype . onObject = function ( original , modified , changePath ) {
26
+ if ( original === undefined || original === null )
27
+ return ( original !== modified ) && this . set ( changePath , modified ) ;
28
+
29
+ if ( typeof original == 'number' && typeof modified == 'number' && original !== modified )
30
+ return this . inc ( changePath , modified - original ) ;
31
+
32
+ if ( Array . isArray ( original ) && Array . isArray ( modified ) )
33
+ return this . onArray ( original , modified , changePath ) ;
34
+
35
+ if ( ! _ . isPlainObject ( original ) || ! _ . isPlainObject ( modified ) )
36
+ return ( original != modified ) && this . set ( changePath , modified ) ;
37
+
38
+ _ . each ( modified , function ( value , key ) {
39
+ // Handle array diffs in their own special way
40
+ if ( Array . isArray ( value ) && Array . isArray ( original [ key ] ) ) this . onArray ( original [ key ] , value , resolve ( changePath , key ) ) ;
41
+
42
+ // Otherwise, just keep going
43
+ else this . onObject ( original [ key ] , value , resolve ( changePath , key ) ) ;
44
+ } , this ) ;
45
+
46
+ // Unset removed properties
47
+ _ . each ( original , function ( value , key ) {
48
+ if ( modified [ key ] === undefined || modified [ key ] === null ) return this . unset ( resolve ( changePath , key ) ) ;
49
+ } , this ) ;
50
+ } ;
51
+
52
+ Omnom . prototype . onArray = function ( original , modified , changePath ) {
53
+ var i , j ;
54
+
55
+ // Check if we can get from original => modified using just pulls
56
+ if ( original . length > modified . length ) {
57
+ var pulls = [ ] ;
58
+ for ( i = 0 , j = 0 ; i < original . length && j < modified . length ; i ++ ) {
59
+ if ( almostEqual ( original [ i ] , modified [ j ] ) ) j ++ ;
60
+ else pulls . push ( original [ i ] ) ;
61
+ }
62
+
63
+ for ( ; i < original . length ; i ++ )
64
+ pulls . push ( original [ i ] ) ;
65
+
66
+ if ( j === modified . length ) {
67
+ if ( pulls . length === 1 ) return this . pull ( changePath , pulls [ 0 ] ) ;
68
+ // We can complete using just pulls
69
+ return this . pullAll ( changePath , pulls ) ;
70
+ }
71
+
72
+ // If we have a smaller target array than our source, we will need to re-create it
73
+ // regardless (if we want to do so in a single operation anyway)
74
+ else return this . set ( changePath , modified ) ;
75
+ }
76
+
77
+ // Check if we can get from original => modified using just pushes
78
+ if ( original . length < modified . length ) {
79
+ var canPush = true ;
80
+ for ( i = 0 ; i < original . length ; i ++ )
81
+ if ( almostEqual ( original [ i ] , modified [ i ] ) < 1 ) {
82
+ canPush = false ;
83
+ break ;
84
+ }
85
+
86
+ if ( canPush ) {
87
+ for ( i = original . length ; i < modified . length ; i ++ )
88
+ this . push ( changePath , modified [ i ] ) ;
89
+ return ;
90
+ }
91
+ }
92
+
93
+ // Otherwise, we need to use $set to generate the new array
94
+
95
+ // Check how many manipulations would need to be performed, if it's more than half the array size
96
+ // then rather re-create the array
97
+
98
+ var sets = [ ] ;
99
+ var partials = [ ] ;
100
+ for ( i = 0 ; i < modified . length ; i ++ ) {
101
+ var equality = almostEqual ( original [ i ] , modified [ i ] ) ;
102
+ if ( equality === 0 ) sets . push ( i ) ;
103
+ else if ( equality < 1 ) partials . push ( i ) ;
104
+ }
105
+
106
+ if ( sets . length > modified . length / 2 )
107
+ return this . set ( changePath , modified ) ;
108
+
109
+ for ( i = 0 ; i < sets . length ; i ++ )
110
+ this . set ( resolve ( changePath , sets [ i ] . toString ( ) ) , modified [ sets [ i ] ] ) ;
111
+
112
+ for ( i = 0 ; i < partials . length ; i ++ )
113
+ this . onObject ( original [ partials [ i ] ] , modified [ partials [ i ] ] , resolve ( changePath , partials [ i ] . toString ( ) ) ) ;
114
+ } ;
115
+
116
+ Omnom . prototype . set = function ( path , value ) {
117
+ if ( ! this . changes . $set )
118
+ this . changes . $set = { } ;
119
+
120
+ this . changes . $set [ path ] = value ;
121
+ } ;
122
+
123
+ Omnom . prototype . unset = function ( path , value ) {
124
+ if ( ! this . changes . $unset )
125
+ this . changes . $unset = { } ;
126
+
127
+ this . changes . $unset [ path ] = 1 ;
128
+ } ;
129
+
130
+ Omnom . prototype . inc = function ( path , value ) {
131
+ if ( ! this . changes . $inc )
132
+ this . changes . $inc = { } ;
133
+
134
+ this . changes . $inc [ path ] = value ;
135
+ } ;
136
+
137
+ Omnom . prototype . push = function ( path , value ) {
138
+ if ( ! this . changes . $push )
139
+ this . changes . $push = { } ;
140
+
141
+ if ( this . changes . $push [ path ] ) {
142
+ if ( this . changes . $push [ path ] . $each )
143
+ this . changes . $push [ path ] . $each . push ( value ) ;
144
+ else
145
+ this . changes . $push [ path ] = { $each : [ this . changes . $push [ path ] , value ] } ;
146
+ } else this . changes . $push [ path ] = value ;
147
+ } ;
148
+
149
+ Omnom . prototype . pull = function ( path , value ) {
150
+ if ( ! this . changes . $pull )
151
+ this . changes . $pull = { } ;
152
+
153
+ if ( this . changes . $pullAll && this . changes . $pullAll [ path ] ) {
154
+ return this . changes . $pullAll [ path ] . push ( value ) ;
155
+ }
156
+
157
+ if ( this . changes . $pull [ path ] ) {
158
+ this . pullAll ( path , [ this . changes . $pull [ path ] , value ] ) ;
159
+ delete this . changes . $pull [ path ] ;
160
+ return ;
161
+ }
162
+
163
+ this . changes . $pull [ path ] = value ;
164
+ } ;
165
+
166
+ Omnom . prototype . pullAll = function ( path , values ) {
167
+ if ( ! this . changes . $pullAll )
168
+ this . changes . $pullAll = { } ;
169
+
170
+ this . changes . $pullAll [ path ] = values ;
171
+ } ;
172
+
173
+ function resolve ( ) {
174
+ var validArguments = [ ] ;
175
+ Array . prototype . forEach . call ( arguments , function ( arg ) {
176
+ if ( arg ) validArguments . push ( arg ) ;
177
+ } ) ;
178
+ return validArguments . join ( '.' ) ;
179
+ }
180
+
181
+ var almostEqual = function ( o1 , o2 ) {
182
+ if ( ! _ . isPlainObject ( o1 ) || ! _ . isPlainObject ( o2 ) ) return o1 == o2 ? 1 : 0 ;
183
+
184
+ var o1i , o1k = Object . keys ( o1 ) ;
185
+ var o2i , o2k = Object . keys ( o2 ) ;
186
+
187
+ var commonKeys = [ ] ;
188
+ for ( o1i = 0 ; o1i < o1k . length ; o1i ++ )
189
+ if ( ~ o2k . indexOf ( o1k [ o1i ] ) ) commonKeys . push ( o1k [ o1i ] ) ;
190
+
191
+ var totalKeys = o1k . length + o2k . length - commonKeys . length ;
192
+ var keysDifference = totalKeys - commonKeys . length ;
193
+
194
+ var requiredChanges = 0 ;
195
+ for ( var i = 0 ; i < commonKeys . length ; i ++ )
196
+ if ( almostEqual ( o1 [ commonKeys [ i ] ] , o2 [ commonKeys [ i ] ] ) < 1 ) requiredChanges ++ ;
197
+
198
+ return 1 - ( keysDifference / totalKeys ) - ( requiredChanges / commonKeys . length ) ;
199
+ } ;
0 commit comments