codekingpro/portable-devtools
115k
1/*
2 const obj1 = {a: 4, b: 5};
3 const obj2 = {a: 3, b: 5};
4 const obj3 = {a: 4, c: 5};
5
6 diff(obj1, obj2);
7 [
8 { "op": "replace", "path": ['a'], "value": 3 }
9 ]
10
11 diff(obj2, obj3);
12 [
13 { "op": "remove", "path": ['b'] },
14 { "op": "replace", "path": ['a'], "value": 4 }
15 { "op": "add", "path": ['c'], "value": 5 }
16 ]
17
18 // using converter to generate jsPatch standard paths
19 // see http://jsonpatch.com
20 import {diff, jsonPatchPathConverter} from 'just-diff'
21 diff(obj1, obj2, jsonPatchPathConverter);
22 [
23 { "op": "replace", "path": '/a', "value": 3 }
24 ]
25
26 diff(obj2, obj3, jsonPatchPathConverter);
27 [
28 { "op": "remove", "path": '/b' },
29 { "op": "replace", "path": '/a', "value": 4 }
30 { "op": "add", "path": '/c', "value": 5 }
31 ]
32
33 // arrays
34 const obj4 = {a: 4, b: [1, 2, 3]};
35 const obj5 = {a: 3, b: [1, 2, 4]};
36 const obj6 = {a: 3, b: [1, 2, 4, 5]};
37
38 diff(obj4, obj5);
39 [
40 { "op": "replace", "path": ['a'], "value": 3 }
41 { "op": "replace", "path": ['b', 2], "value": 4 }
42 ]
43
44 diff(obj5, obj6);
45 [
46 { "op": "add", "path": ['b', 3], "value": 5 }
47 ]
48
49 // nested paths
50 const obj7 = {a: 4, b: {c: 3}};
51 const obj8 = {a: 4, b: {c: 4}};
52 const obj9 = {a: 5, b: {d: 4}};
53
54 diff(obj7, obj8);
55 [
56 { "op": "replace", "path": ['b', 'c'], "value": 4 }
57 ]
58
59 diff(obj8, obj9);
60 [
61 { "op": "replace", "path": ['a'], "value": 5 }
62 { "op": "remove", "path": ['b', 'c']}
63 { "op": "add", "path": ['b', 'd'], "value": 4 }
64 ]
65*/
66
67function diff(obj1, obj2, pathConverter) {
68 if (!obj1 || typeof obj1 != 'object' || !obj2 || typeof obj2 != 'object') {
69 throw new Error('both arguments must be objects or arrays');
70 }
71
72 pathConverter ||
73 (pathConverter = function(arr) {
74 return arr;
75 });
76
77 function getDiff({obj1, obj2, basePath, basePathForRemoves, diffs}) {
78 var obj1Keys = Object.keys(obj1);
79 var obj1KeysLength = obj1Keys.length;
80 var obj2Keys = Object.keys(obj2);
81 var obj2KeysLength = obj2Keys.length;
82 var path;
83
84 var lengthDelta = obj1.length - obj2.length;
85
86 if (trimFromRight(obj1, obj2)) {
87 for (var i = 0; i < obj1KeysLength; i++) {
88 var key = Array.isArray(obj1) ? Number(obj1Keys[i]) : obj1Keys[i];
89 if (!(key in obj2)) {
90 path = basePathForRemoves.concat(key);
91 diffs.remove.push({
92 op: 'remove',
93 path: pathConverter(path),
94 });
95 }
96 }
97
98 for (var i = 0; i < obj2KeysLength; i++) {
99 var key = Array.isArray(obj2) ? Number(obj2Keys[i]) : obj2Keys[i];
100 pushReplaces({
101 key,
102 obj1,
103 obj2,
104 path: basePath.concat(key),
105 pathForRemoves: basePath.concat(key),
106 diffs,
107 });
108 }
109 } else {
110 // trim from left, objects are both arrays
111 for (var i = 0; i < lengthDelta; i++) {
112 path = basePathForRemoves.concat(i);
113 diffs.remove.push({
114 op: 'remove',
115 path: pathConverter(path),
116 });
117 }
118
119 // now make a copy of obj1 with excess elements left trimmed and see if there any replaces
120 var obj1Trimmed = obj1.slice(lengthDelta);;
121 for (var i = 0; i < obj2KeysLength; i++) {
122 pushReplaces({
123 key: i,
124 obj1: obj1Trimmed,
125 obj2,
126 path: basePath.concat(i),
127 // since list of removes are reversed before presenting result,
128 // we need to ignore existing parent removes when doing nested removes
129 pathForRemoves: basePath.concat(i + lengthDelta),
130 diffs,
131 });
132 }
133 }
134 }
135
136 var diffs = {remove: [], replace: [], add: []};
137 getDiff({
138 obj1,
139 obj2,
140 basePath: [],
141 basePathForRemoves: [],
142 diffs,
143 });
144
145 // reverse removes since we want to maintain indexes
146 return diffs.remove
147 .reverse()
148 .concat(diffs.replace)
149 .concat(diffs.add);
150
151 function pushReplaces({key, obj1, obj2, path, pathForRemoves, diffs}) {
152 var obj1AtKey = obj1[key];
153 var obj2AtKey = obj2[key];
154
155 if(!(key in obj1) && (key in obj2)) {
156 var obj2Value = obj2AtKey;
157 diffs.add.push({
158 op: 'add',
159 path: pathConverter(path),
160 value: obj2Value,
161 });
162 } else if(obj1AtKey !== obj2AtKey) {
163 if(Object(obj1AtKey) !== obj1AtKey ||
164 Object(obj2AtKey) !== obj2AtKey || differentTypes(obj1AtKey, obj2AtKey)
165 ) {
166 pushReplace(path, diffs, obj2AtKey);
167 } else {
168 if(!Object.keys(obj1AtKey).length &&
169 !Object.keys(obj2AtKey).length &&
170 String(obj1AtKey) != String(obj2AtKey)) {
171 pushReplace(path, diffs, obj2AtKey);
172 } else {
173 getDiff({
174 obj1: obj1[key],
175 obj2: obj2[key],
176 basePath: path,
177 basePathForRemoves: pathForRemoves,
178 diffs});
179 }
180 }
181 }
182 }
183
184 function pushReplace(path, diffs, newValue) {
185 diffs.replace.push({
186 op: 'replace',
187 path: pathConverter(path),
188 value: newValue,
189 });
190 }
191}
192
193function jsonPatchPathConverter(arrayPath) {
194 return [''].concat(arrayPath).join('/');
195}
196
197function differentTypes(a, b) {
198 return Object.prototype.toString.call(a) != Object.prototype.toString.call(b);
199}
200
201function trimFromRight(obj1, obj2) {
202 var lengthDelta = obj1.length - obj2.length;
203 if (Array.isArray(obj1) && Array.isArray(obj2) && lengthDelta > 0) {
204 var leftMatches = 0;
205 var rightMatches = 0;
206 for (var i = 0; i < obj2.length; i++) {
207 if (String(obj1[i]) === String(obj2[i])) {
208 leftMatches++;
209 } else {
210 break;
211 }
212 }
213 for (var j = obj2.length; j > 0; j--) {
214 if (String(obj1[j + lengthDelta]) === String(obj2[j])) {
215 rightMatches++;
216 } else {
217 break;
218 }
219 }
220
221 // bias to trim right becase it requires less index shifting
222 return leftMatches >= rightMatches;
223 }
224 return true;
225}
226
227export {diff, jsonPatchPathConverter};
228 