Skip to content

Commit

Permalink
Add array diff
Browse files Browse the repository at this point in the history
  • Loading branch information
mattphillips committed Dec 29, 2017
1 parent a4a53a4 commit ae6464f
Show file tree
Hide file tree
Showing 2 changed files with 241 additions and 0 deletions.
53 changes: 53 additions & 0 deletions src/arrayDiff/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { isDate, isEmpty, isObject, properObject } from '../utils';

const diff = (lhs, rhs) => {
if (lhs === rhs) return {}; // equal return no diff

if (!isObject(lhs) || !isObject(rhs)) return rhs; // return updated rhs

const l = properObject(lhs);
const r = properObject(rhs);

const deletedValues = Object.keys(l).reduce((acc, key) => {
return r.hasOwnProperty(key) ? acc : { ...acc, [key]: undefined };
}, {});

if (isDate(l) || isDate(r)) {
if (l.valueOf() == r.valueOf()) return {};
return r;
}

if (Array.isArray(r) && Array.isArray(l)) {
const deletedValues = l.reduce((acc, item, index) => {
return r.hasOwnProperty(index) ? acc.concat(item) : acc.concat(undefined);
}, []);

return r.reduce((acc, rightItem, index) => {
if (!deletedValues.hasOwnProperty(index)) {
return acc.concat(rightItem);
}

const leftItem = l[index];
const difference = diff(rightItem, leftItem);

if (isObject(difference) && isEmpty(difference) && !isDate(difference)) {
delete acc[index];
return acc; // return no diff
}

return acc.slice(0, index).concat(rightItem).concat(acc.slice(index + 1)); // return updated key
}, deletedValues);
}

return Object.keys(r).reduce((acc, key) => {
if (!l.hasOwnProperty(key)) return { ...acc, [key]: r[key] }; // return added r key

const difference = diff(l[key], r[key]);

if (isObject(difference) && isEmpty(difference) && !isDate(difference)) return acc; // return no diff

return { ...acc, [key]: difference }; // return updated key
}, deletedValues);
};

export default diff;
188 changes: 188 additions & 0 deletions src/arrayDiff/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import forEach from 'jest-each';

import diff from './';

describe('.arrayDiff', () => {

describe('base case', () => {
describe('equal', () => {
forEach([
['int', 1],
['string', 'a'],
['boolean', true],
['null', null],
['undefined', undefined],
['object', { a: 1 }],
['array', [1]],
['function', () => ({})],
['date', new Date()],
['date with milliseconds', new Date('2017-01-01T00:00:00.637Z')],
]).test('returns empty object when given values of type %s are equal', (type, value) => {
expect(diff(value, value)).toEqual({});
});
});

describe('not equal and not object', () => {
forEach([
[1, 2],
['a', 'b'],
[true, false],
['hello', null],
['hello', undefined],
[null, undefined],
[undefined, null],
[null, { a: 1 }],
['872983', { areaCode: '+44', number: '872983' }],
[100, () => ({})],
[() => ({}), 100],
[new Date('2017-01-01'), new Date('2017-01-02')],
[new Date('2017-01-01T00:00:00.636Z'), new Date('2017-01-01T00:00:00.637Z')],
]).test('returns right hand side value when different to left hand side value (%s, %s)', (lhs, rhs) => {
expect(diff(lhs, rhs)).toEqual(rhs);
});
});
});

describe('recursive case', () => {
describe('object', () => {
test('returns right hand side value when given objects are different', () => {
expect(diff({ a: 1 }, { a: 2 })).toEqual({ a: 2 });
});

test('returns right hand side value when right hand side value is null', () => {
expect(diff({ a: 1 }, { a: null })).toEqual({ a: null });
});

test('returns subset of right hand side value when sibling objects differ', () => {
expect(diff({ a: { b: 1 }, c: 2 }, { a: { b: 1 }, c: 3 })).toEqual({ c: 3 });
});

test('returns subset of right hand side value when nested values differ', () => {
expect(diff({ a: { b: 1, c: 2} }, { a: { b: 1, c: 3 } })).toEqual({ a: { c: 3 } });
});

test('returns subset of right hand side value when nested values differ at multiple paths', () => {
expect(diff({ a: { b: 1 }, c: 2, d: { e: 100 } }, { a: { b: 99 }, c: 3, d: { e: 100 } })).toEqual({ a: { b: 99 }, c: 3 });
});

test('returns subset of right hand side value when a key value has been deleted', () => {
expect(diff({ a: { b: 1 }, c: 2, d: { e: 100 } }, { a: { b: 1 }, c: 2, d: {} })).toEqual({ d: { e: undefined } });
});

test('returns subset of right hand side value when a key value has been added', () => {
expect(diff({ a: 1 }, { a: 1, b: 2 })).toEqual({ b: 2 });
});

test('returns keys as undefined when deleted from right hand side', () => {
expect(diff({ a: 1, b: { c: 2 }}, { a: 1 })).toEqual({ b: undefined });
});
});

describe('arrays', () => {
test('returns right hand side value as object of indices to value when arrays are different', () => {
expect(diff([1], [2])).toEqual([2]);
});

test('returns subset of right hand side array as object of indices to value when arrays differs at multiple indicies', () => {
const expected = [9, 8, 3];
delete expected['2'];
expect(diff([1, 2, 3], [9, 8, 3])).toEqual(expected);
});

test('returns subset of right hand side array as object of indices to value when right hand side array has deletions', () => {
const expected = [1, 3, undefined];
delete expected['0'];
expect(diff([1, 2, 3], [1, 3])).toEqual(expected);
});

test('returns subset of right hand side array as object of indices to value when right hand side array has additions', () => {
const expected = [1, 2, 3, 9];
delete expected['0'];
delete expected['1'];
delete expected['2'];
// expected.forEach(console.log)
expect(diff([1, 2, 3], [1, 2, 3, 9])).toEqual(expected);
});
});

describe('date', () => {
const lhs = new Date('2016');
const rhs = new Date('2017');

test('returns empty object when dates are equal', () => {
expect(diff(new Date('2016'), new Date('2016'))).toEqual({});
});

test('returns right hand side date when updated', () => {
expect(diff({ date: lhs }, { date: rhs })).toEqual({ date: rhs });
expect(diff([lhs], [rhs])).toEqual([rhs]);
});

test('returns undefined when date deleted', () => {
expect(diff({ date: lhs }, {})).toEqual({ date: undefined });
expect(diff([lhs], [])).toEqual([undefined]);
});

test('returns right hand side when date is added', () => {
expect(diff({}, { date: rhs })).toEqual({ date: rhs });
expect(diff([], [rhs])).toEqual([rhs]);
});
});

describe('object create null', () => {
test('returns right hand side value when given objects are different', () => {
const lhs = Object.create(null);
lhs.a = 1;
const rhs = Object.create(null);
rhs.a = 2;
expect(diff(lhs, rhs)).toEqual({ a: 2 });
});

test('returns subset of right hand side value when sibling objects differ', () => {
const lhs = Object.create(null);
lhs.a = { b: 1 };
lhs.c = 2;
const rhs = Object.create(null);
rhs.a = { b: 1 };
rhs.c = 3;
expect(diff(lhs, rhs)).toEqual({ c: 3 });
});

test('returns subset of right hand side value when nested values differ', () => {
const lhs = Object.create(null);
lhs.a = { b: 1, c: 2};
const rhs = Object.create(null);
rhs.a = { b: 1, c: 3 };
expect(diff(lhs, rhs)).toEqual({ a: { c: 3 } });
});

test('returns subset of right hand side value when nested values differ at multiple paths', () => {
const lhs = Object.create(null);
lhs.a = { b: 1 };
lhs.c = 2;
const rhs = Object.create(null);
rhs.a = { b: 99 };
rhs.c = 3;
expect(diff(lhs, rhs)).toEqual({ a: { b: 99 }, c: 3 });
});

test('returns subset of right hand side value when a key value has been deleted', () => {
const lhs = Object.create(null);
lhs.a = { b: 1 };
lhs.c = 2;
const rhs = Object.create(null);
rhs.a = { b: 1 };
expect(diff(lhs, rhs)).toEqual({ c: undefined });
});

test('returns subset of right hand side value when a key value has been added', () => {
const lhs = Object.create(null);
lhs.a = 1;
const rhs = Object.create(null);
rhs.a = 1;
rhs.b = 2;
expect(diff(lhs, rhs)).toEqual({ b: 2 });
});
});
});
});

0 comments on commit ae6464f

Please sign in to comment.