Skip to content

Commit 7e2a7a1

Browse files
committed
Added difference() function to compute difference between two objects
1 parent c9cacd6 commit 7e2a7a1

File tree

3 files changed

+105
-2
lines changed

3 files changed

+105
-2
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "lup-utils",
3-
"version": "1.1.0",
3+
"version": "1.2.0",
44
"description": "NodeJS library providing utility functions.",
55
"main": "./lib/index",
66
"types": "./lib/index.d.ts",

src/__tests__/object.test.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { deepEqual } from '../object';
1+
import { deepEqual, difference } from '../object';
22

33
test('deepEqual()', async () => {
44
// Primitive values (true)
@@ -56,6 +56,7 @@ test('deepEqual()', async () => {
5656
expect(deepEqual({ a: [1, 2], b: 3 }, { a: [1, 2], b: 3 }, true)).toBe(true);
5757

5858
// Object strict order (false)
59+
expect(deepEqual({ a: 1 }, { b: 2 }, true)).toBe(false);
5960
expect(deepEqual({ a: 1, b: 2 }, { b: 2, a: 1 }, true)).toBe(false);
6061
expect(deepEqual({ a: 1, b: { c: 3 } }, { b: { c: 3 }, a: 1 }, true)).toBe(false);
6162
expect(deepEqual({ a: [1, 2], b: 3 }, { b: 3, a: [1, 2] }, true)).toBe(false);
@@ -68,7 +69,54 @@ test('deepEqual()', async () => {
6869
expect(deepEqual({ a: [1, 2], b: 3 }, { b: 3, a: [1, 2] }, false)).toBe(true);
6970

7071
// Object unordered (false)
72+
expect(deepEqual({ b: 2 }, { b: 3 }, false)).toBe(false);
7173
expect(deepEqual({ a: 1, b: 2 }, { a: 1, b: 3 }, false)).toBe(false);
7274
expect(deepEqual({ a: 1, b: 2 }, { a: 1 })).toBe(false);
7375
expect(deepEqual({ a: 1 }, { a: 1, b: 2 })).toBe(false);
7476
});
77+
78+
test('difference()', async () => {
79+
expect(difference({}, {}, { recursive: false, ignoreOrder: false })).toEqual({});
80+
expect(difference({ a: 1 }, { a: 1 }, { recursive: false, ignoreOrder: false })).toEqual({});
81+
expect(difference({ a: 1 }, { a: 2 }, { recursive: false, ignoreOrder: false })).toEqual({ a: 2 });
82+
expect(difference({ a: 1, b: 2 }, { a: 1 }, { recursive: false, ignoreOrder: false })).toEqual({ b: undefined });
83+
expect(difference({ a: 1 }, { a: 1, b: 2 }, { recursive: false, ignoreOrder: false })).toEqual({ b: 2 });
84+
expect(
85+
difference(
86+
{ a: { b: 2, c: { d: 3, e: 4 } } },
87+
{ a: { b: 2, c: { d: 3, e: 4 } } },
88+
{ recursive: false, ignoreOrder: false },
89+
),
90+
).toEqual({});
91+
expect(
92+
difference(
93+
{ a: { b: 2, c: { d: 3, e: 4 } } },
94+
{ a: { b: 3, c: { d: 3, e: 4 } } },
95+
{ recursive: false, ignoreOrder: false },
96+
),
97+
).toEqual({ a: { b: 3, c: { d: 3, e: 4 } } });
98+
expect(
99+
difference(
100+
{ a: { b: 2, c: { d: 3, e: 4 } } },
101+
{ a: { b: 2, c: { d: 3, e: 5 } } },
102+
{ recursive: false, ignoreOrder: false },
103+
),
104+
).toEqual({ a: { b: 2, c: { d: 3, e: 5 } } });
105+
expect(difference({ a: [1, 2, 3] }, { a: [1, 2, 3] }, { recursive: false, ignoreOrder: false })).toEqual({});
106+
expect(difference({ a: [1, 2, 3] }, { a: [3, 2, 1] }, { recursive: false, ignoreOrder: false })).toEqual({
107+
a: [3, 2, 1],
108+
});
109+
expect(difference({ a: [1, 2, 3] }, { a: [4, 5, 6] }, { recursive: false, ignoreOrder: false })).toEqual({
110+
a: [4, 5, 6],
111+
});
112+
113+
expect(difference({ a: [1, 2, 3] }, { a: [3, 2, 1] }, { recursive: false, ignoreOrder: true })).toEqual({});
114+
115+
expect(
116+
difference(
117+
{ a: { b: 2, c: { d: 3, e: 4 } } },
118+
{ a: { b: 2, c: { d: 3, e: 5 } } },
119+
{ recursive: true, ignoreOrder: false },
120+
),
121+
).toEqual({ a: { c: { e: 5 } } });
122+
});

src/object.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,58 @@ export function deepEqual(a: any, b: any, strictOrder: boolean = false): boolean
3636
}
3737
return false;
3838
}
39+
40+
/**
41+
* Compares two objects and returns a new object containing all keys that have different values.
42+
* If two arrays are different, the entire new array is included in the result.
43+
* @param oldObj Old object to compare to.
44+
* @param newObj New object to compare from and from which to take values.
45+
* @param options Additional options for comparison.
46+
* @returns An object containing all keys that have different values (if value is missing in newObj, value is set to undefined).
47+
*/
48+
export function difference<O, N>(
49+
oldObj: O,
50+
newObj: N,
51+
options?: {
52+
/** Whether to ignore the order of items in arrays. */
53+
ignoreOrder?: boolean;
54+
55+
/** Whether to perform a deep comparison of nested objects (does not apply to arrays). */
56+
recursive?: boolean;
57+
},
58+
): Partial<O & N> {
59+
if (typeof oldObj !== 'object' || oldObj === null || typeof newObj !== 'object' || newObj === null) {
60+
return deepEqual(oldObj, newObj, !options?.ignoreOrder) ? (oldObj as any) : (newObj as any);
61+
}
62+
const diff: Partial<O & N> = {};
63+
const oldKeys = new Set<string>(Object.keys(oldObj));
64+
const newKeys = new Set<string>(Object.keys(newObj));
65+
66+
// iterate old object
67+
for (const key of oldKeys) {
68+
newKeys.delete(key);
69+
const oldValue: any = (oldObj as any)[key]!;
70+
const newValue: any = (newObj as any)[key];
71+
72+
// recursive difference
73+
if (options?.recursive && typeof oldValue === 'object' && oldValue !== null) {
74+
if (typeof newValue !== 'object' || newValue === null) {
75+
(diff as any)[key] = newValue;
76+
continue;
77+
}
78+
(diff as any)[key] = difference(oldValue, newValue, options);
79+
continue;
80+
}
81+
82+
// flat difference
83+
if (!deepEqual(oldValue, newValue, !options?.ignoreOrder)) (diff as any)[key] = newValue;
84+
}
85+
86+
// iterate new object
87+
for (const key of newKeys) {
88+
const newValue: any = (newObj as any)[key]!;
89+
(diff as any)[key] = newValue;
90+
}
91+
92+
return diff;
93+
}

0 commit comments

Comments
 (0)