Skip to content

Commit

Permalink
feat(differ): support preserveKeyOrder (#27)
Browse files Browse the repository at this point in the history
Close #26
  • Loading branch information
RexSkz authored Jan 30, 2024
1 parent 2646d54 commit 763eeee
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 5 deletions.
22 changes: 22 additions & 0 deletions playground/playground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const Playground: React.FC<PlaygroundProps> = props => {
const [ignoreCase, setIgnoreCase] = React.useState(false);
const [ignoreCaseForKey, setIgnoreCaseForKey] = React.useState(false);
const [recursiveEqual, setRecursiveEqual] = React.useState(true);
const [preserveKeyOrder, setPreserveKeyOrder] = React.useState<DifferOptions['preserveKeyOrder']>(undefined);

// viewer props
const [indent, setIndent] = React.useState(4);
Expand All @@ -43,6 +44,7 @@ const Playground: React.FC<PlaygroundProps> = props => {
ignoreCase,
ignoreCaseForKey,
recursiveEqual,
preserveKeyOrder,
}), [
detectCircular,
maxDepth,
Expand All @@ -51,6 +53,7 @@ const Playground: React.FC<PlaygroundProps> = props => {
ignoreCase,
ignoreCaseForKey,
recursiveEqual,
preserveKeyOrder,
]);
const differ = React.useMemo(() => new Differ(differOptions), [differOptions]);
const [diff, setDiff] = React.useState(differ.diff("", ""));
Expand Down Expand Up @@ -297,6 +300,25 @@ return (
onChange={e => setRecursiveEqual(e.target.checked)}
/>
</label>
<label htmlFor="preserve-key-order">
<Label
title="Preserve key order"
tip={
<>
Sometimes you do not want the keys in result be sorted, for example <code>start_time</code> and <code>end_time</code> will be swapped by default. You can set this option to let the differ preserve the key order according to <code>before</code> or <code>after</code>.
</>
}
/>
<select
id="preserve-key-order"
value={preserveKeyOrder}
onChange={e => setPreserveKeyOrder((e.target.value === 'sort' ? undefined : e.target.value) as DifferOptions['preserveKeyOrder'])}
>
<option value="sort">sort (default)</option>
<option value="before">by "before"</option>
<option value="after">by "after"</option>
</select>
</label>
</form>
</div>
<div className="config">
Expand Down
7 changes: 7 additions & 0 deletions src/differ.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@ export interface DifferOptions {
* This comparation process is slow in huge objects.
*/
recursiveEqual?: boolean;
/**
* If the value is set, differ will make sure the key order of results is the same as inputs
* ("before" or "after"). Otherwise, differ will sort the keys of results.
*/
preserveKeyOrder?: 'before' | 'after';
}

interface Token {
Expand Down Expand Up @@ -151,6 +156,7 @@ class Differ {
ignoreCase = false,
ignoreCaseForKey = false,
recursiveEqual = false,
preserveKeyOrder,
}: DifferOptions = {}) {
this.options = {
detectCircular,
Expand All @@ -160,6 +166,7 @@ class Differ {
ignoreCase,
ignoreCaseForKey,
recursiveEqual,
preserveKeyOrder,
};
this.arrayDiffFunc = arrayDiffMethod === 'lcs' || arrayDiffMethod === 'unorder-lcs'
? diffArrayLCS
Expand Down
9 changes: 8 additions & 1 deletion src/utils/cmp.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
interface CmpOptions {
ignoreCase?: boolean;
keyOrdersMap?: Map<string, number>;
}

const getOrderByType = (value: any) => {
Expand Down Expand Up @@ -32,6 +33,12 @@ const getOrderByType = (value: any) => {
* - For array and object: preserve the original order (or do we have a better idea?)
*/
const cmp = (a: any, b: any, options: CmpOptions) => {
const orderByMapA = options.keyOrdersMap?.get(a);
const orderByMapB = options.keyOrdersMap?.get(b);
if (orderByMapA !== undefined && orderByMapB !== undefined) {
return orderByMapA - orderByMapB;
}

const orderByTypeA = getOrderByType(a);
const orderByTypeB = getOrderByType(b);

Expand All @@ -47,7 +54,7 @@ const cmp = (a: any, b: any, options: CmpOptions) => {
case 'number':
return a - b;
case 'string':
if (options?.ignoreCase) {
if (options.ignoreCase) {
a = a.toLowerCase();
b = b.toLowerCase();
}
Expand Down
33 changes: 29 additions & 4 deletions src/utils/diff-object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,36 @@ const diffObject = (

const keysLeft = Object.keys(lhs);
const keysRight = Object.keys(rhs);
sortKeys(keysLeft, options);
sortKeys(keysRight, options);
const keyOrdersMap = new Map<string, number>();
if (!options.preserveKeyOrder) {
sortKeys(keysLeft, options);
sortKeys(keysRight, options);
} else if (options.preserveKeyOrder === 'before') {
for (let i = 0; i < keysLeft.length; i++) {
keyOrdersMap.set(keysLeft[i], i);
}
for (let i = 0; i < keysRight.length; i++) {
if (!keyOrdersMap.has(keysRight[i])) {
keyOrdersMap.set(keysRight[i], keysLeft.length + i);
}
}
keysRight.sort((a, b) => keyOrdersMap.get(a)! - keyOrdersMap.get(b)!);
} else if (options.preserveKeyOrder === 'after') {
for (let i = 0; i < keysRight.length; i++) {
keyOrdersMap.set(keysRight[i], i);
}
for (let i = 0; i < keysLeft.length; i++) {
if (!keyOrdersMap.has(keysLeft[i])) {
keyOrdersMap.set(keysLeft[i], keysRight.length + i);
}
}
keysLeft.sort((a, b) => keyOrdersMap.get(a)! - keyOrdersMap.get(b)!);
}

const keysCmpOptions = { ignoreCase: options.ignoreCaseForKey };
const valueCmpOptions = { ignoreCase: options.ignoreCase };
const keysCmpOptions = {
ignoreCase: options.ignoreCaseForKey,
keyOrdersMap,
};
while (keysLeft.length || keysRight.length) {
const keyLeft = keysLeft[0];
const keyRight = keysRight[0];
Expand Down

0 comments on commit 763eeee

Please sign in to comment.