Skip to content

Commit 1598d51

Browse files
utkuakyuzutku.akyuz
andauthored
fet: add compare-key method for array diffing (#50)
* feat: compare by matching sent key * fix: compare array of non object elements correction in compare by key method * fix: array bracket utils created to prevent code duplication * fix: improved recursive comparison on compare key method --------- Co-authored-by: utku.akyuz <utku.akyuz@ekinokssoftware.com>
1 parent c3ca37b commit 1598d51

File tree

8 files changed

+481
-34
lines changed

8 files changed

+481
-34
lines changed

playground/docs.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const Docs: React.FC<PropTypes> = props => {
2626
const [ignoreCase, setIgnoreCase] = React.useState(false);
2727
const [recursiveEqual, setRecursiveEqual] = React.useState(true);
2828
const [preserveKeyOrder, setPreserveKeyOrder] = React.useState<DifferOptions['preserveKeyOrder']>(undefined);
29+
const [compareKey, setCompareKey] = React.useState<string>('');
2930

3031
// viewer props
3132
const [indent, setIndent] = React.useState(4);
@@ -44,6 +45,7 @@ const Docs: React.FC<PropTypes> = props => {
4445
ignoreCase,
4546
recursiveEqual,
4647
preserveKeyOrder,
48+
compareKey
4749
}), [
4850
detectCircular,
4951
maxDepth,
@@ -52,6 +54,7 @@ const Docs: React.FC<PropTypes> = props => {
5254
ignoreCase,
5355
recursiveEqual,
5456
preserveKeyOrder,
57+
compareKey,
5558
]);
5659
const differ = React.useMemo(() => new Differ(differOptions), [differOptions]);
5760

@@ -231,9 +234,10 @@ const Docs: React.FC<PropTypes> = props => {
231234
<option value="lcs">lcs</option>
232235
<option value="unorder-normal">unorder-normal</option>
233236
<option value="unorder-lcs">unorder-lcs</option>
237+
<option value="compare-key">compare-key</option>
234238
</select>
235239
</label>
236-
<blockquote>The way to diff arrays, default is <code>"normal"</code>, using <code>"lcs"</code> may get a better result but much slower. <code>"unorder-normal"</code> and <code>"unorder-lcs"</code> are for unordered arrays (the order of elements in the array doesn't matter).</blockquote>
240+
<blockquote>The way to diff arrays, default is <code>"normal"</code>, using <code>"lcs"</code> may get a better result but much slower. <code>"unorder-normal"</code> and <code>"unorder-lcs"</code> are for unordered arrays (the order of elements in the array doesn't matter). <code>"compare-key"</code> matches objects by a specific key property.</blockquote>
237241
<label htmlFor="ignore-case">
238242
Ignore case:
239243
<input

playground/playground.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const Playground: React.FC<PlaygroundProps> = props => {
2828
const [ignoreCaseForKey, setIgnoreCaseForKey] = React.useState(false);
2929
const [recursiveEqual, setRecursiveEqual] = React.useState(true);
3030
const [preserveKeyOrder, setPreserveKeyOrder] = React.useState<DifferOptions['preserveKeyOrder']>(undefined);
31+
const [compareKey, setCompareKey] = React.useState<string>('');
3132

3233
// viewer props
3334
const [indent, setIndent] = React.useState(4);
@@ -47,6 +48,7 @@ const Playground: React.FC<PlaygroundProps> = props => {
4748
ignoreCaseForKey,
4849
recursiveEqual,
4950
preserveKeyOrder,
51+
compareKey: compareKey || undefined,
5052
}), [
5153
detectCircular,
5254
maxDepth,
@@ -56,6 +58,7 @@ const Playground: React.FC<PlaygroundProps> = props => {
5658
ignoreCaseForKey,
5759
recursiveEqual,
5860
preserveKeyOrder,
61+
compareKey,
5962
]);
6063
const differ = React.useMemo(() => new Differ(differOptions), [differOptions]);
6164
const [diff, setDiff] = React.useState(differ.diff('', ''));
@@ -251,7 +254,7 @@ return (
251254
title="Array diff method"
252255
tip={
253256
<>
254-
The way to diff arrays, default is <code>"normal"</code>, using <code>"lcs"</code> may get a better result but much slower. <code>"unorder-normal"</code> and <code>"unorder-lcs"</code> are for unordered arrays (the order of elements in the array doesn't matter).
257+
The way to diff arrays, default is <code>"normal"</code>, using <code>"lcs"</code> may get a better result but much slower. <code>"unorder-normal"</code> and <code>"unorder-lcs"</code> are for unordered arrays (the order of elements in the array doesn't matter). <code>"compare-key"</code> matches objects by a specific key property.
255258
</>
256259
}
257260
/>
@@ -264,8 +267,24 @@ return (
264267
<option value="lcs">lcs</option>
265268
<option value="unorder-normal">unorder-normal</option>
266269
<option value="unorder-lcs">unorder-lcs</option>
270+
<option value="compare-key">compare-key</option>
267271
</select>
268272
</label>
273+
{arrayDiffMethod === 'compare-key' && (
274+
<label htmlFor="compare-key">
275+
<Label
276+
title="Compare key"
277+
tip="The key to use for matching objects in arrays. Objects with the same value for this key will be matched and compared, regardless of their position in the array."
278+
/>
279+
<input
280+
type="text"
281+
id="compare-key"
282+
value={compareKey}
283+
onChange={e => setCompareKey(e.target.value)}
284+
placeholder="e.g., oid, userId, id"
285+
/>
286+
</label>
287+
)}
269288
<label htmlFor="ignore-case">
270289
<Label
271290
title="Ignore case"

src/differ.ts

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import concat from './utils/concat';
33
import detectCircular from './utils/detect-circular';
44
import diffArrayLCS from './utils/diff-array-lcs';
55
import diffArrayNormal from './utils/diff-array-normal';
6+
import diffArrayCompareKey from './utils/diff-array-compare-key';
67
import diffObject from './utils/diff-object';
78
import getType from './utils/get-type';
89
import sortInnerArrays from './utils/sort-inner-arrays';
@@ -81,8 +82,26 @@ export interface DifferOptions {
8182
* 3 3
8283
* + 4
8384
* ```
85+
*
86+
* When using `compare-key`, the differ will match objects in arrays by a specific key
87+
* property (specified by `compareKey` option). This is useful when comparing arrays of
88+
* objects where the order doesn't matter but you want to match related objects.
89+
* The output will be:
90+
*
91+
* ```diff
92+
* a b
93+
* 1 1
94+
* - 2
95+
* + 3
96+
* 4 4
97+
* ```
8498
*/
85-
arrayDiffMethod?: 'normal' | 'lcs' | 'unorder-normal' | 'unorder-lcs';
99+
arrayDiffMethod?:
100+
| 'normal'
101+
| 'lcs'
102+
| 'unorder-normal'
103+
| 'unorder-lcs'
104+
| 'compare-key';
86105
/**
87106
* Whether to ignore the case when comparing strings, default `false`.
88107
*/
@@ -116,6 +135,12 @@ export interface DifferOptions {
116135
* ("before" or "after"). Otherwise, differ will sort the keys of results.
117136
*/
118137
preserveKeyOrder?: 'before' | 'after';
138+
/**
139+
* The key to use for matching objects in arrays when using `compare-key` array diff method.
140+
* Objects with the same value for this key will be matched and compared, regardless of their
141+
* position in the array.
142+
*/
143+
compareKey?: string;
119144
/**
120145
* The behavior when encountering values that are not part of the JSON spec, e.g. `undefined`, `NaN`, `Infinity`, `123n`, `() => alert(1)`, `Symbol.iterator`.
121146
*
@@ -169,6 +194,7 @@ class Differ {
169194
ignoreCaseForKey = false,
170195
recursiveEqual = false,
171196
preserveKeyOrder,
197+
compareKey,
172198
undefinedBehavior = UndefinedBehavior.stringify,
173199
}: DifferOptions = {}) {
174200
this.options = {
@@ -180,11 +206,17 @@ class Differ {
180206
ignoreCaseForKey,
181207
recursiveEqual,
182208
preserveKeyOrder,
209+
compareKey,
183210
undefinedBehavior,
184211
};
185-
this.arrayDiffFunc = arrayDiffMethod === 'lcs' || arrayDiffMethod === 'unorder-lcs'
186-
? diffArrayLCS
187-
: diffArrayNormal;
212+
213+
if (arrayDiffMethod === 'compare-key') {
214+
this.arrayDiffFunc = diffArrayCompareKey;
215+
} else if (arrayDiffMethod === 'lcs' || arrayDiffMethod === 'unorder-lcs') {
216+
this.arrayDiffFunc = diffArrayLCS;
217+
} else {
218+
this.arrayDiffFunc = diffArrayNormal;
219+
}
188220
}
189221

190222
private detectCircular(source: any) {

src/utils/array-bracket-utils.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { DiffResult } from '../differ';
2+
3+
// Shared utility for array diff
4+
export const addArrayOpeningBrackets = (
5+
linesLeft: DiffResult[],
6+
linesRight: DiffResult[],
7+
keyLeft: string,
8+
keyRight: string,
9+
level: number
10+
) => {
11+
if (keyLeft && keyRight) {
12+
linesLeft.push({ level, type: 'equal', text: `"${keyLeft}": [` });
13+
linesRight.push({ level, type: 'equal', text: `"${keyRight}": [` });
14+
} else {
15+
linesLeft.push({ level, type: 'equal', text: '[' });
16+
linesRight.push({ level, type: 'equal', text: '[' });
17+
}
18+
};
19+
20+
export const addArrayClosingBrackets = (
21+
linesLeft: DiffResult[],
22+
linesRight: DiffResult[],
23+
level: number
24+
) => {
25+
linesLeft.push({ level, type: 'equal', text: ']' });
26+
linesRight.push({ level, type: 'equal', text: ']' });
27+
};
28+
29+
export const addMaxDepthPlaceholder = (
30+
linesLeft: DiffResult[],
31+
linesRight: DiffResult[],
32+
level: number
33+
) => {
34+
linesLeft.push({ level: level + 1, type: 'equal', text: '...' });
35+
linesRight.push({ level: level + 1, type: 'equal', text: '...' });
36+
};

0 commit comments

Comments
 (0)