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