Skip to content

Commit ae6464f

Browse files
committed
Add array diff
1 parent a4a53a4 commit ae6464f

File tree

2 files changed

+241
-0
lines changed

2 files changed

+241
-0
lines changed

src/arrayDiff/index.js

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { isDate, isEmpty, isObject, properObject } from '../utils';
2+
3+
const diff = (lhs, rhs) => {
4+
if (lhs === rhs) return {}; // equal return no diff
5+
6+
if (!isObject(lhs) || !isObject(rhs)) return rhs; // return updated rhs
7+
8+
const l = properObject(lhs);
9+
const r = properObject(rhs);
10+
11+
const deletedValues = Object.keys(l).reduce((acc, key) => {
12+
return r.hasOwnProperty(key) ? acc : { ...acc, [key]: undefined };
13+
}, {});
14+
15+
if (isDate(l) || isDate(r)) {
16+
if (l.valueOf() == r.valueOf()) return {};
17+
return r;
18+
}
19+
20+
if (Array.isArray(r) && Array.isArray(l)) {
21+
const deletedValues = l.reduce((acc, item, index) => {
22+
return r.hasOwnProperty(index) ? acc.concat(item) : acc.concat(undefined);
23+
}, []);
24+
25+
return r.reduce((acc, rightItem, index) => {
26+
if (!deletedValues.hasOwnProperty(index)) {
27+
return acc.concat(rightItem);
28+
}
29+
30+
const leftItem = l[index];
31+
const difference = diff(rightItem, leftItem);
32+
33+
if (isObject(difference) && isEmpty(difference) && !isDate(difference)) {
34+
delete acc[index];
35+
return acc; // return no diff
36+
}
37+
38+
return acc.slice(0, index).concat(rightItem).concat(acc.slice(index + 1)); // return updated key
39+
}, deletedValues);
40+
}
41+
42+
return Object.keys(r).reduce((acc, key) => {
43+
if (!l.hasOwnProperty(key)) return { ...acc, [key]: r[key] }; // return added r key
44+
45+
const difference = diff(l[key], r[key]);
46+
47+
if (isObject(difference) && isEmpty(difference) && !isDate(difference)) return acc; // return no diff
48+
49+
return { ...acc, [key]: difference }; // return updated key
50+
}, deletedValues);
51+
};
52+
53+
export default diff;

src/arrayDiff/index.test.js

+188
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import forEach from 'jest-each';
2+
3+
import diff from './';
4+
5+
describe('.arrayDiff', () => {
6+
7+
describe('base case', () => {
8+
describe('equal', () => {
9+
forEach([
10+
['int', 1],
11+
['string', 'a'],
12+
['boolean', true],
13+
['null', null],
14+
['undefined', undefined],
15+
['object', { a: 1 }],
16+
['array', [1]],
17+
['function', () => ({})],
18+
['date', new Date()],
19+
['date with milliseconds', new Date('2017-01-01T00:00:00.637Z')],
20+
]).test('returns empty object when given values of type %s are equal', (type, value) => {
21+
expect(diff(value, value)).toEqual({});
22+
});
23+
});
24+
25+
describe('not equal and not object', () => {
26+
forEach([
27+
[1, 2],
28+
['a', 'b'],
29+
[true, false],
30+
['hello', null],
31+
['hello', undefined],
32+
[null, undefined],
33+
[undefined, null],
34+
[null, { a: 1 }],
35+
['872983', { areaCode: '+44', number: '872983' }],
36+
[100, () => ({})],
37+
[() => ({}), 100],
38+
[new Date('2017-01-01'), new Date('2017-01-02')],
39+
[new Date('2017-01-01T00:00:00.636Z'), new Date('2017-01-01T00:00:00.637Z')],
40+
]).test('returns right hand side value when different to left hand side value (%s, %s)', (lhs, rhs) => {
41+
expect(diff(lhs, rhs)).toEqual(rhs);
42+
});
43+
});
44+
});
45+
46+
describe('recursive case', () => {
47+
describe('object', () => {
48+
test('returns right hand side value when given objects are different', () => {
49+
expect(diff({ a: 1 }, { a: 2 })).toEqual({ a: 2 });
50+
});
51+
52+
test('returns right hand side value when right hand side value is null', () => {
53+
expect(diff({ a: 1 }, { a: null })).toEqual({ a: null });
54+
});
55+
56+
test('returns subset of right hand side value when sibling objects differ', () => {
57+
expect(diff({ a: { b: 1 }, c: 2 }, { a: { b: 1 }, c: 3 })).toEqual({ c: 3 });
58+
});
59+
60+
test('returns subset of right hand side value when nested values differ', () => {
61+
expect(diff({ a: { b: 1, c: 2} }, { a: { b: 1, c: 3 } })).toEqual({ a: { c: 3 } });
62+
});
63+
64+
test('returns subset of right hand side value when nested values differ at multiple paths', () => {
65+
expect(diff({ a: { b: 1 }, c: 2, d: { e: 100 } }, { a: { b: 99 }, c: 3, d: { e: 100 } })).toEqual({ a: { b: 99 }, c: 3 });
66+
});
67+
68+
test('returns subset of right hand side value when a key value has been deleted', () => {
69+
expect(diff({ a: { b: 1 }, c: 2, d: { e: 100 } }, { a: { b: 1 }, c: 2, d: {} })).toEqual({ d: { e: undefined } });
70+
});
71+
72+
test('returns subset of right hand side value when a key value has been added', () => {
73+
expect(diff({ a: 1 }, { a: 1, b: 2 })).toEqual({ b: 2 });
74+
});
75+
76+
test('returns keys as undefined when deleted from right hand side', () => {
77+
expect(diff({ a: 1, b: { c: 2 }}, { a: 1 })).toEqual({ b: undefined });
78+
});
79+
});
80+
81+
describe('arrays', () => {
82+
test('returns right hand side value as object of indices to value when arrays are different', () => {
83+
expect(diff([1], [2])).toEqual([2]);
84+
});
85+
86+
test('returns subset of right hand side array as object of indices to value when arrays differs at multiple indicies', () => {
87+
const expected = [9, 8, 3];
88+
delete expected['2'];
89+
expect(diff([1, 2, 3], [9, 8, 3])).toEqual(expected);
90+
});
91+
92+
test('returns subset of right hand side array as object of indices to value when right hand side array has deletions', () => {
93+
const expected = [1, 3, undefined];
94+
delete expected['0'];
95+
expect(diff([1, 2, 3], [1, 3])).toEqual(expected);
96+
});
97+
98+
test('returns subset of right hand side array as object of indices to value when right hand side array has additions', () => {
99+
const expected = [1, 2, 3, 9];
100+
delete expected['0'];
101+
delete expected['1'];
102+
delete expected['2'];
103+
// expected.forEach(console.log)
104+
expect(diff([1, 2, 3], [1, 2, 3, 9])).toEqual(expected);
105+
});
106+
});
107+
108+
describe('date', () => {
109+
const lhs = new Date('2016');
110+
const rhs = new Date('2017');
111+
112+
test('returns empty object when dates are equal', () => {
113+
expect(diff(new Date('2016'), new Date('2016'))).toEqual({});
114+
});
115+
116+
test('returns right hand side date when updated', () => {
117+
expect(diff({ date: lhs }, { date: rhs })).toEqual({ date: rhs });
118+
expect(diff([lhs], [rhs])).toEqual([rhs]);
119+
});
120+
121+
test('returns undefined when date deleted', () => {
122+
expect(diff({ date: lhs }, {})).toEqual({ date: undefined });
123+
expect(diff([lhs], [])).toEqual([undefined]);
124+
});
125+
126+
test('returns right hand side when date is added', () => {
127+
expect(diff({}, { date: rhs })).toEqual({ date: rhs });
128+
expect(diff([], [rhs])).toEqual([rhs]);
129+
});
130+
});
131+
132+
describe('object create null', () => {
133+
test('returns right hand side value when given objects are different', () => {
134+
const lhs = Object.create(null);
135+
lhs.a = 1;
136+
const rhs = Object.create(null);
137+
rhs.a = 2;
138+
expect(diff(lhs, rhs)).toEqual({ a: 2 });
139+
});
140+
141+
test('returns subset of right hand side value when sibling objects differ', () => {
142+
const lhs = Object.create(null);
143+
lhs.a = { b: 1 };
144+
lhs.c = 2;
145+
const rhs = Object.create(null);
146+
rhs.a = { b: 1 };
147+
rhs.c = 3;
148+
expect(diff(lhs, rhs)).toEqual({ c: 3 });
149+
});
150+
151+
test('returns subset of right hand side value when nested values differ', () => {
152+
const lhs = Object.create(null);
153+
lhs.a = { b: 1, c: 2};
154+
const rhs = Object.create(null);
155+
rhs.a = { b: 1, c: 3 };
156+
expect(diff(lhs, rhs)).toEqual({ a: { c: 3 } });
157+
});
158+
159+
test('returns subset of right hand side value when nested values differ at multiple paths', () => {
160+
const lhs = Object.create(null);
161+
lhs.a = { b: 1 };
162+
lhs.c = 2;
163+
const rhs = Object.create(null);
164+
rhs.a = { b: 99 };
165+
rhs.c = 3;
166+
expect(diff(lhs, rhs)).toEqual({ a: { b: 99 }, c: 3 });
167+
});
168+
169+
test('returns subset of right hand side value when a key value has been deleted', () => {
170+
const lhs = Object.create(null);
171+
lhs.a = { b: 1 };
172+
lhs.c = 2;
173+
const rhs = Object.create(null);
174+
rhs.a = { b: 1 };
175+
expect(diff(lhs, rhs)).toEqual({ c: undefined });
176+
});
177+
178+
test('returns subset of right hand side value when a key value has been added', () => {
179+
const lhs = Object.create(null);
180+
lhs.a = 1;
181+
const rhs = Object.create(null);
182+
rhs.a = 1;
183+
rhs.b = 2;
184+
expect(diff(lhs, rhs)).toEqual({ b: 2 });
185+
});
186+
});
187+
});
188+
});

0 commit comments

Comments
 (0)