• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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