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