From a83622274f073fbad36763d0e1264cd715636a74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=8B=87?= Date: Tue, 1 Jul 2025 01:44:38 +0800 Subject: [PATCH 1/4] perf: Add comprehensive tests and optimize diffArrayCompareKey Added a new test suite for diffArrayCompareKey covering various scenarios, including nested structures, edge cases, and option handling. Refactored diffArrayCompareKey for better performance and maintainability by introducing helper functions, optimizing object matching with compareKey, and improving validation logic. The changes enhance correctness, efficiency, and test coverage. --- src/utils/diff-array-compare-key.spec.ts | 415 +++++++++++++++++++++++ src/utils/diff-array-compare-key.ts | 390 ++++++++++++--------- 2 files changed, 649 insertions(+), 156 deletions(-) create mode 100644 src/utils/diff-array-compare-key.spec.ts diff --git a/src/utils/diff-array-compare-key.spec.ts b/src/utils/diff-array-compare-key.spec.ts new file mode 100644 index 0000000..3b8efd9 --- /dev/null +++ b/src/utils/diff-array-compare-key.spec.ts @@ -0,0 +1,415 @@ +import { DifferOptions, UndefinedBehavior } from '../differ'; +import diffArrayCompareKey, { allObjectsHaveCompareKey } from './diff-array-compare-key'; + +describe('Utility function: diff-array-compare-key', () => { + const defaultOptions: DifferOptions = { + compareKey: 'id', + showModifications: true, + undefinedBehavior: UndefinedBehavior.stringify, + }; + + const createOptions = (overrides: Partial = {}): DifferOptions => ({ + ...defaultOptions, + ...overrides, + }); + + describe('allObjectsHaveCompareKey', () => { + it('should return true when all objects have the compare key', () => { + const arr = [ + { id: 1, name: 'Alice' }, + { id: 2, name: 'Bob' }, + { id: 3, name: 'Charlie' }, + ]; + expect(allObjectsHaveCompareKey(arr, 'id')).toBe(true); + }); + + it('should return false when some objects are missing the compare key', () => { + const arr = [ + { id: 1, name: 'Alice' }, + { name: 'Bob' }, // missing id + { id: 3, name: 'Charlie' }, + ]; + expect(allObjectsHaveCompareKey(arr, 'id')).toBe(false); + }); + + it('should return true for nested arrays with compare key', () => { + const arr = [ + { id: 1, items: [{ id: 11, value: 'a' }] }, + { id: 2, items: [{ id: 22, value: 'b' }] }, + ]; + expect(allObjectsHaveCompareKey(arr, 'id')).toBe(true); + }); + + it('should return false for nested arrays missing compare key', () => { + const arr = [ + { id: 1, items: [{ value: 'a' }] }, // nested object missing id + { id: 2, items: [{ id: 22, value: 'b' }] }, + ]; + expect(allObjectsHaveCompareKey(arr, 'id')).toBe(false); + }); + + it('should handle empty arrays', () => { + expect(allObjectsHaveCompareKey([], 'id')).toBe(true); + }); + + it('should handle arrays with non-object items', () => { + const arr = [{ id: 1, name: 'Alice' }, 'string item', 123]; + expect(allObjectsHaveCompareKey(arr, 'id')).toBe(true); + }); + }); + + describe('diffArrayCompareKey - Basic functionality', () => { + it('should handle identical arrays', () => { + const left = [ + { id: 1, name: 'Alice' }, + { id: 2, name: 'Bob' }, + ]; + const right = [ + { id: 1, name: 'Alice' }, + { id: 2, name: 'Bob' }, + ]; + + const [linesLeft] = diffArrayCompareKey(left, right, '', '', 0, createOptions()); + + expect(linesLeft).toEqual([ + { level: 0, type: 'equal', text: '[' }, + { level: 1, type: 'equal', text: '{' }, + { level: 2, type: 'equal', text: '"id": 1' }, + { level: 2, type: 'equal', text: '"name": "Alice"' }, + { level: 1, type: 'equal', text: '}' }, + { level: 1, type: 'equal', text: '{' }, + { level: 2, type: 'equal', text: '"id": 2' }, + { level: 2, type: 'equal', text: '"name": "Bob"' }, + { level: 1, type: 'equal', text: '}' }, + { level: 0, type: 'equal', text: ']' }, + ]); + }); + + it('should match objects by compareKey regardless of order', () => { + const left = [ + { id: 1, name: 'Alice' }, + { id: 2, name: 'Bob' }, + ]; + const right = [ + { id: 2, name: 'Bob' }, + { id: 1, name: 'Alice' }, + ]; + + const [linesLeft, linesRight] = diffArrayCompareKey(left, right, '', '', 0, createOptions()); + + // Should match id=1 with id=1 and id=2 with id=2, regardless of position + expect(linesLeft.filter((line) => line.type === 'equal')).toHaveLength(linesLeft.length); + expect(linesRight.filter((line) => line.type === 'equal')).toHaveLength(linesRight.length); + }); + + it('should detect modifications in matched objects', () => { + const left = [{ id: 1, name: 'Alice', age: 25 }]; + const right = [{ id: 1, name: 'Alice', age: 26 }]; + + const [linesLeft, linesRight] = diffArrayCompareKey(left, right, '', '', 0, createOptions()); + + // Should have equal parts for id and name, but modified age + expect(linesLeft.some((line) => line.text === '"age": 25' && line.type === 'modify')).toBe(true); + expect(linesRight.some((line) => line.text === '"age": 26' && line.type === 'modify')).toBe(true); + }); + }); + + describe('diffArrayCompareKey - Add/Remove operations', () => { + it('should handle added items', () => { + const left = [{ id: 1, name: 'Alice' }]; + const right = [ + { id: 1, name: 'Alice' }, + { id: 2, name: 'Bob' }, + ]; + + const [, linesRight] = diffArrayCompareKey(left, right, '', '', 0, createOptions()); + + // Should have added content for Bob + expect(linesRight.some((line) => line.text.includes('Bob') && line.type === 'add')).toBe(true); + }); + + it('should handle removed items', () => { + const left = [ + { id: 1, name: 'Alice' }, + { id: 2, name: 'Bob' }, + ]; + const right = [{ id: 1, name: 'Alice' }]; + + const [linesLeft] = diffArrayCompareKey(left, right, '', '', 0, createOptions()); + + // Should have removed content for Bob + expect(linesLeft.some((line) => line.text.includes('Bob') && line.type === 'remove')).toBe(true); + }); + + it('should handle completely different arrays', () => { + const left = [ + { id: 1, name: 'Alice' }, + { id: 2, name: 'Bob' }, + ]; + const right = [ + { id: 3, name: 'Charlie' }, + { id: 4, name: 'Dave' }, + ]; + + const [linesLeft, linesRight] = diffArrayCompareKey(left, right, '', '', 0, createOptions()); + + // Should have all items as removed from left and added to right + expect(linesLeft.some((line) => line.text.includes('Alice') && line.type === 'remove')).toBe(true); + expect(linesLeft.some((line) => line.text.includes('Bob') && line.type === 'remove')).toBe(true); + expect(linesRight.some((line) => line.text.includes('Charlie') && line.type === 'add')).toBe(true); + expect(linesRight.some((line) => line.text.includes('Dave') && line.type === 'add')).toBe(true); + }); + }); + + describe('diffArrayCompareKey - Nested structures', () => { + it('should handle nested arrays in objects', () => { + const left = [{ id: 1, items: [1, 2, 3] }]; + const right = [{ id: 1, items: [1, 3, 4] }]; + + const [linesLeft, linesRight] = diffArrayCompareKey(left, right, '', '', 0, createOptions()); + + // Should recursively diff the nested arrays - verify array structure + expect(linesLeft).toContainEqual(expect.objectContaining({ text: '[' })); + expect(linesRight).toContainEqual(expect.objectContaining({ text: '[' })); + + // Should show specific changes in the nested array content + // 数组 [1, 2, 3] vs [1, 3, 4] 按位置比较: + // - 位置0: 1 vs 1 → equal + // - 位置1: 2 vs 3 → modify + // - 位置2: 3 vs 4 → modify + + // Verify equal element + expect(linesLeft.some((line) => line.text === '1' && line.type === 'equal')).toBe(true); + expect(linesRight.some((line) => line.text === '1' && line.type === 'equal')).toBe(true); + + // Verify modifications (position-based comparison) + expect(linesLeft.some((line) => line.text === '2' && line.type === 'modify')).toBe(true); + expect(linesRight.some((line) => line.text === '3' && line.type === 'modify')).toBe(true); + expect(linesLeft.some((line) => line.text === '3' && line.type === 'modify')).toBe(true); + expect(linesRight.some((line) => line.text === '4' && line.type === 'modify')).toBe(true); + + // Verify we have both equal and modify types (showing it did process the nested array) + const leftTypes = new Set(linesLeft.map((line) => line.type)); + const rightTypes = new Set(linesRight.map((line) => line.type)); + expect(leftTypes.has('equal')).toBe(true); + expect(leftTypes.has('modify')).toBe(true); + expect(rightTypes.has('equal')).toBe(true); + expect(rightTypes.has('modify')).toBe(true); + }); + + it('should handle nested objects in arrays', () => { + const left = [{ id: 1, profile: { name: 'Alice', age: 25 } }]; + const right = [{ id: 1, profile: { name: 'Alice', age: 26 } }]; + + const [linesLeft, linesRight] = diffArrayCompareKey(left, right, '', '', 0, createOptions()); + + // Should show modification in nested object - check for age field modification + expect(linesLeft.some((line) => line.text.includes('25') && line.type === 'modify')).toBe(true); + expect(linesRight.some((line) => line.text.includes('26') && line.type === 'modify')).toBe(true); + }); + + it('should handle mixed types in array values', () => { + const left = [{ id: 1, data: [1, 2] }]; + const right = [{ id: 1, data: { count: 2 } }]; + + const [linesLeft, linesRight] = diffArrayCompareKey(left, right, '', '', 0, createOptions()); + + // Should handle type change from array to object + expect(linesLeft.some((line) => line.type === 'modify' || line.type === 'remove')).toBe(true); + expect(linesRight.some((line) => line.type === 'modify' || line.type === 'add')).toBe(true); + }); + }); + + describe('diffArrayCompareKey - Options handling', () => { + it('should respect showModifications option', () => { + const left = [{ id: 1, name: 'Alice' }]; + const right = [{ id: 1, name: 'Bob' }]; + + // With showModifications: true + const [linesLeft1, linesRight1] = diffArrayCompareKey( + left, + right, + '', + '', + 0, + createOptions({ showModifications: true }) + ); + expect(linesLeft1.some((line) => line.type === 'modify')).toBe(true); + expect(linesRight1.some((line) => line.type === 'modify')).toBe(true); + + // With showModifications: false + const [linesLeft2, linesRight2] = diffArrayCompareKey( + left, + right, + '', + '', + 0, + createOptions({ showModifications: false }) + ); + expect(linesLeft2.some((line) => line.type === 'remove')).toBe(true); + expect(linesRight2.some((line) => line.type === 'add')).toBe(true); + }); + + it('should respect maxDepth option', () => { + const left = [{ id: 1, deep: { nested: { value: 1 } } }]; + const right = [{ id: 1, deep: { nested: { value: 2 } } }]; + + const [linesLeft, linesRight] = diffArrayCompareKey(left, right, '', '', 0, createOptions({ maxDepth: 2 })); + + // Should stop at maxDepth and show placeholder + expect(linesLeft.some((line) => line.text === '...')).toBe(true); + expect(linesRight.some((line) => line.text === '...')).toBe(true); + }); + + it('should handle missing compareKey option', () => { + const left = [{ id: 1, name: 'Alice' }]; + const right = [{ id: 1, name: 'Bob' }]; + + // Should fallback to diffArrayNormal when compareKey is not provided + const [linesLeft, linesRight] = diffArrayCompareKey( + left, + right, + '', + '', + 0, + createOptions({ compareKey: undefined }) + ); + + // Should still produce valid output (though using different algorithm) + expect(linesLeft).toHaveLength(linesRight.length); + expect(linesLeft[0]).toEqual({ level: 0, type: 'equal', text: '[' }); + }); + }); + + describe('diffArrayCompareKey - Edge cases', () => { + it('should handle empty arrays', () => { + const [linesLeft, linesRight] = diffArrayCompareKey([], [], '', '', 0, createOptions()); + + expect(linesLeft).toEqual([ + { level: 0, type: 'equal', text: '[' }, + { level: 0, type: 'equal', text: ']' }, + ]); + expect(linesRight).toEqual([ + { level: 0, type: 'equal', text: '[' }, + { level: 0, type: 'equal', text: ']' }, + ]); + }); + + it('should handle arrays with primitive values', () => { + const left = [1, 2, 3]; + const right = [1, 3, 4]; + + // Should fallback to diffArrayNormal for primitive arrays + const [linesLeft, linesRight] = diffArrayCompareKey(left, right, '', '', 0, createOptions()); + + expect(linesLeft).toHaveLength(linesRight.length); + }); + + it('should handle arrays with mixed object and non-object items', () => { + const left = [{ id: 1, name: 'Alice' }, 'string', 123]; + const right = [{ id: 1, name: 'Alice' }, 'string', 456]; + + // Should fallback to diffArrayNormal for mixed arrays + const [linesLeft, linesRight] = diffArrayCompareKey(left, right, '', '', 0, createOptions()); + + expect(linesLeft).toHaveLength(linesRight.length); + }); + + it('should handle objects without compareKey', () => { + const left = [{ name: 'Alice' }, { id: 2, name: 'Bob' }]; + const right = [{ name: 'Alice' }, { id: 2, name: 'Bob' }]; + + // Should fallback to diffArrayNormal when objects missing compareKey + const [linesLeft, linesRight] = diffArrayCompareKey(left, right, '', '', 0, createOptions()); + + expect(linesLeft).toHaveLength(linesRight.length); + }); + + it('should handle duplicate compareKey values', () => { + const left = [ + { id: 1, name: 'Alice' }, + { id: 1, name: 'Alice2' }, + ]; + const right = [{ id: 1, name: 'Alice' }]; + + const [linesLeft] = diffArrayCompareKey(left, right, '', '', 0, createOptions()); + + // Should match first occurrence and mark second as removed + expect(linesLeft.some((line) => line.type === 'remove')).toBe(true); + }); + + it('should handle complex nested structures', () => { + const left = [ + { + id: 1, + profile: { + personal: { name: 'Alice', age: 25 }, + hobbies: ['reading', 'coding'], + friends: [{ id: 10, name: 'Bob' }], + }, + }, + ]; + + const right = [ + { + id: 1, + profile: { + personal: { name: 'Alice', age: 26 }, + hobbies: ['reading', 'gaming'], + friends: [{ id: 10, name: 'Robert' }], + }, + }, + ]; + + const [linesLeft, linesRight] = diffArrayCompareKey(left, right, '', '', 0, createOptions()); + + // Should handle deep nested changes + expect(linesLeft.some((line) => line.text.includes('25') && line.type === 'modify')).toBe(true); + expect(linesRight.some((line) => line.text.includes('26') && line.type === 'modify')).toBe(true); + }); + }); + + describe('diffArrayCompareKey - Array keys handling', () => { + it('should handle arrays with keyLeft and keyRight', () => { + const left = [{ id: 1, name: 'Alice' }]; + const right = [{ id: 1, name: 'Alice' }]; + + const [linesLeft, linesRight] = diffArrayCompareKey(left, right, 'users', 'users', 0, createOptions()); + + expect(linesLeft[0]).toEqual({ level: 0, type: 'equal', text: '"users": [' }); + expect(linesRight[0]).toEqual({ level: 0, type: 'equal', text: '"users": [' }); + }); + + it('should handle different array keys', () => { + const left = [{ id: 1, name: 'Alice' }]; + const right = [{ id: 1, name: 'Alice' }]; + + const [linesLeft, linesRight] = diffArrayCompareKey(left, right, 'oldUsers', 'newUsers', 0, createOptions()); + + expect(linesLeft[0]).toEqual({ level: 0, type: 'equal', text: '"oldUsers": [' }); + expect(linesRight[0]).toEqual({ level: 0, type: 'equal', text: '"newUsers": [' }); + }); + }); + + describe('diffArrayCompareKey - Type safety', () => { + it('should handle null and undefined values', () => { + const left = [{ id: 1, value: null }]; + const right = [{ id: 1, value: undefined }]; + + const [linesLeft, linesRight] = diffArrayCompareKey(left, right, '', '', 0, createOptions()); + + expect(linesLeft.some((line) => line.text.includes('null'))).toBe(true); + expect(linesRight.some((line) => line.text.includes('undefined'))).toBe(true); + }); + + it('should handle special numeric values', () => { + const left = [{ id: 1, value: NaN }]; + const right = [{ id: 1, value: Infinity }]; + + const [linesLeft, linesRight] = diffArrayCompareKey(left, right, '', '', 0, createOptions()); + + expect(linesLeft.some((line) => line.text.includes('NaN'))).toBe(true); + expect(linesRight.some((line) => line.text.includes('Infinity'))).toBe(true); + }); + }); +}); diff --git a/src/utils/diff-array-compare-key.ts b/src/utils/diff-array-compare-key.ts index 5b678af..095be71 100644 --- a/src/utils/diff-array-compare-key.ts +++ b/src/utils/diff-array-compare-key.ts @@ -9,6 +9,120 @@ import stringify from './stringify'; import diffArrayNormal from './diff-array-normal'; import { addArrayClosingBrackets, addArrayOpeningBrackets, addMaxDepthPlaceholder } from './array-bracket-utils'; +// Helper function to check if an item is an object with the required compare key +function isObjectWithCompareKey(item: any, compareKey: string): boolean { + return getType(item) === 'object' && compareKey in item; +} + +// Helper function to create formatted result lines +function createFormattedLine( + level: number, + type: 'equal' | 'add' | 'remove' | 'modify', + item: any, + options: DifferOptions +): DiffResult { + return { + level, + type, + text: formatValue(item, undefined, undefined, options.undefinedBehavior), + }; +} + +// Helper function to process matched objects +function processMatchedObjects( + leftItem: any, + rightItem: any, + level: number, + options: DifferOptions, + linesLeft: DiffResult[], + linesRight: DiffResult[] +): [DiffResult[], DiffResult[]] { + const leftType = getType(leftItem); + const rightType = getType(rightItem); + + if (leftType !== rightType) { + prettyAppendLines(linesLeft, linesRight, '', '', leftItem, rightItem, level + 1, options); + } else if (leftType === 'object') { + // Always recurse into diffObject for aligned objects + linesLeft.push({ level: level + 1, type: 'equal', text: '{' }); + linesRight.push({ level: level + 1, type: 'equal', text: '{' }); + + const keys = Array.from(new Set([...Object.keys(leftItem), ...Object.keys(rightItem)])); + for (const key of keys) { + const lVal = leftItem[key]; + const rVal = rightItem[key]; + if (Array.isArray(lVal) && Array.isArray(rVal)) { + // Recursively diff arrays + const [arrL, arrR] = diffArrayRecursive(lVal, rVal, '', '', level + 2, options, [], []); + linesLeft = concat(linesLeft, arrL); + linesRight = concat(linesRight, arrR); + } else if (Array.isArray(lVal) || Array.isArray(rVal)) { + // If only one side is array, treat as modification + prettyAppendLines(linesLeft, linesRight, key, key, lVal, rVal, level + 2, options); + } else { + // Use diffObject for non-array values + const [leftLines, rightLines] = diffObject( + { [key]: lVal }, + { [key]: rVal }, + level + 2, + options, + diffArrayRecursive + ); + linesLeft = concat(linesLeft, leftLines); + linesRight = concat(linesRight, rightLines); + } + } + linesLeft.push({ level: level + 1, type: 'equal', text: '}' }); + linesRight.push({ level: level + 1, type: 'equal', text: '}' }); + } else if (leftType === 'array') { + // For nested arrays, recursively apply the same logic + const [resLeft, resRight] = diffArrayRecursive(leftItem, rightItem, '', '', level + 1, options, [], []); + linesLeft = concat(linesLeft, resLeft); + linesRight = concat(linesRight, resRight); + } else if (isEqual(leftItem, rightItem, options)) { + linesLeft.push(createFormattedLine(level + 1, 'equal', leftItem, options)); + linesRight.push(createFormattedLine(level + 1, 'equal', rightItem, options)); + } else { + if (options.showModifications) { + linesLeft.push(createFormattedLine(level + 1, 'modify', leftItem, options)); + linesRight.push(createFormattedLine(level + 1, 'modify', rightItem, options)); + } else { + linesLeft.push(createFormattedLine(level + 1, 'remove', leftItem, options)); + linesLeft.push({ level: level + 1, type: 'equal', text: '' }); + linesRight.push({ level: level + 1, type: 'equal', text: '' }); + linesRight.push(createFormattedLine(level + 1, 'add', rightItem, options)); + } + } + + return [linesLeft, linesRight]; +} + +// Optimized validation function that combines type checking and compare key validation in single pass +function validateArrayForCompareKey(arr: any[], compareKey: string): { isValid: boolean; objectCount: number } { + let objectCount = 0; + + for (const item of arr) { + const type = getType(item); + if (type === 'object') { + objectCount++; + if (!(compareKey in item)) return { isValid: false, objectCount: 0 }; + // Check nested arrays in object values + for (const value of Object.values(item)) { + if (Array.isArray(value) && !allObjectsHaveCompareKey(value, compareKey)) { + return { isValid: false, objectCount: 0 }; + } + } + } else if (Array.isArray(item)) { + if (!allObjectsHaveCompareKey(item, compareKey)) return { isValid: false, objectCount: 0 }; + } else { + // Non-object, non-array items make the array invalid for compareKey strategy + return { isValid: false, objectCount: 0 }; + } + } + + return { isValid: objectCount > 0, objectCount }; +} + // Recursively checks if all objects (including in nested arrays) have the compare key function allObjectsHaveCompareKey(arr: any[], compareKey: string): boolean { for (const item of arr) { @@ -37,18 +151,18 @@ function diffArrayRecursive( level: number, options: DifferOptions, linesLeft: DiffResult[] = [], - linesRight: DiffResult[] = [], + linesRight: DiffResult[] = [] ): [DiffResult[], DiffResult[]] { if (!options.compareKey) { // Fallback to normal diff if no compare key is specified return diffArrayNormal(arrLeft, arrRight, keyLeft, keyRight, level, options, linesLeft, linesRight); } - // If arrays are not of objects, or not all objects have the compare key (including nested), fallback to unordered LCS diff - const isObjectArray = (arr: any[]) => arr.every(item => getType(item) === 'object'); - if (!isObjectArray(arrLeft) || !isObjectArray(arrRight) || - !allObjectsHaveCompareKey(arrLeft, options.compareKey) || - !allObjectsHaveCompareKey(arrRight, options.compareKey)) { + // Early validation with single pass - combine type checking and compare key validation + const leftValidation = validateArrayForCompareKey(arrLeft, options.compareKey); + const rightValidation = validateArrayForCompareKey(arrRight, options.compareKey); + + if (!leftValidation.isValid || !rightValidation.isValid) { // Use unordered LCS for arrays of primitives, mixed types, or missing compare key return diffArrayNormal(arrLeft, arrRight, keyLeft, keyRight, level, options, linesLeft, linesRight); } @@ -60,174 +174,86 @@ function diffArrayRecursive( } else { const leftProcessed = new Set(); const rightProcessed = new Set(); - + + // For small arrays, use simple O(n²) approach to avoid Map overhead + const useMapOptimization = arrLeft.length > 10 || arrRight.length > 10; + let rightKeyMap: Map | null = null; + let rightValidItems: Set | null = null; + + if (useMapOptimization) { + // Build a map of compareKey values to right array items for O(n) matching + // Only store items that passed validation to reduce memory usage + rightKeyMap = new Map(); + rightValidItems = new Set(); // Track valid items to avoid re-checking + + for (let j = 0; j < arrRight.length; j++) { + const rightItem = arrRight[j]; + if (isObjectWithCompareKey(rightItem, options.compareKey)) { + rightValidItems.add(j); + const rightKeyValue = rightItem[options.compareKey]; + if (!rightKeyMap.has(rightKeyValue)) { + rightKeyMap.set(rightKeyValue, []); + } + rightKeyMap.get(rightKeyValue)!.push({ item: rightItem, index: j }); + } + } + } + // First pass: find matching objects by compareKey for (let i = 0; i < arrLeft.length; i++) { const leftItem = arrLeft[i]; if (leftProcessed.has(i)) continue; - + // Skip if left item is not an object or doesn't have the compare key - if (getType(leftItem) !== 'object' || !(options.compareKey in leftItem)) { + if (!isObjectWithCompareKey(leftItem, options.compareKey)) { continue; } - + const leftKeyValue = leftItem[options.compareKey]; - - // Find matching item in right array let matchIndex = -1; - for (let j = 0; j < arrRight.length; j++) { - if (rightProcessed.has(j)) continue; - - const rightItem = arrRight[j]; - if (getType(rightItem) !== 'object' || !(options.compareKey in rightItem)) { - continue; + + if (useMapOptimization && rightKeyMap) { + // Use map for O(1) lookup + const candidates = rightKeyMap.get(leftKeyValue); + if (candidates) { + // Find the first unprocessed candidate + for (const candidate of candidates) { + if (!rightProcessed.has(candidate.index)) { + matchIndex = candidate.index; + break; + } + } } - - const rightKeyValue = rightItem[options.compareKey]; - - // Compare key values - if (isEqual(leftKeyValue, rightKeyValue, options)) { - matchIndex = j; - break; + } else { + // Use simple O(n) search for small arrays + for (let j = 0; j < arrRight.length; j++) { + if (rightProcessed.has(j)) continue; + const rightItem = arrRight[j]; + if (isObjectWithCompareKey(rightItem, options.compareKey)) { + const rightKeyValue = rightItem[options.compareKey]; + if (isEqual(leftKeyValue, rightKeyValue, options)) { + matchIndex = j; + break; + } + } } } - + if (matchIndex !== -1) { - // Found a match, compare the objects + // Found a match, process the matched objects const rightItem = arrRight[matchIndex]; - const leftType = getType(leftItem); - const rightType = getType(rightItem); - - if (leftType !== rightType) { - prettyAppendLines( - linesLeft, - linesRight, - '', - '', - leftItem, - rightItem, - level + 1, - options, - ); - } else if (leftType === 'object') { - // Always recurse into diffObject for aligned objects, regardless of recursiveEqual/isEqual - linesLeft.push({ level: level + 1, type: 'equal', text: '{' }); - linesRight.push({ level: level + 1, type: 'equal', text: '{' }); - // For each key, if value is array, apply recursive diff logic - const keys = Array.from(new Set([...Object.keys(leftItem), ...Object.keys(rightItem)])); - for (const key of keys) { - const lVal = leftItem[key]; - const rVal = rightItem[key]; - if (Array.isArray(lVal) && Array.isArray(rVal)) { - // Recursively diff arrays - const [arrL, arrR] = diffArrayRecursive(lVal, rVal, '', '', level + 2, options, [], []); - linesLeft = concat(linesLeft, arrL); - linesRight = concat(linesRight, arrR); - } else if (Array.isArray(lVal) || Array.isArray(rVal)) { - // If only one side is array, treat as modification - prettyAppendLines( - linesLeft, - linesRight, - key, - key, - lVal, - rVal, - level + 2, - options, - ); - } else { - // Use diffObject for non-array values - const [leftLines, rightLines] = diffObject( - { [key]: lVal }, - { [key]: rVal }, - level + 2, - options, - diffArrayRecursive - ); - linesLeft = concat(linesLeft, leftLines); - linesRight = concat(linesRight, rightLines); - } - } - linesLeft.push({ level: level + 1, type: 'equal', text: '}' }); - linesRight.push({ level: level + 1, type: 'equal', text: '}' }); - } else if (leftType === 'array') { - // For nested arrays, recursively apply the same logic - const [resLeft, resRight] = diffArrayRecursive(leftItem, rightItem, '', '', level + 1, options, [], []); - linesLeft = concat(linesLeft, resLeft); - linesRight = concat(linesRight, resRight); - } else if (isEqual(leftItem, rightItem, options)) { - linesLeft.push({ - level: level + 1, - type: 'equal', - text: formatValue(leftItem, undefined, undefined, options.undefinedBehavior), - }); - linesRight.push({ - level: level + 1, - type: 'equal', - text: formatValue(rightItem, undefined, undefined, options.undefinedBehavior), - }); - } else { - if (options.showModifications) { - linesLeft.push({ - level: level + 1, - type: 'modify', - text: formatValue(leftItem, undefined, undefined, options.undefinedBehavior), - }); - linesRight.push({ - level: level + 1, - type: 'modify', - text: formatValue(rightItem, undefined, undefined, options.undefinedBehavior), - }); - } else { - linesLeft.push({ - level: level + 1, - type: 'remove', - text: formatValue(leftItem, undefined, undefined, options.undefinedBehavior), - }); - linesLeft.push({ level: level + 1, type: 'equal', text: '' }); - linesRight.push({ level: level + 1, type: 'equal', text: '' }); - linesRight.push({ - level: level + 1, - type: 'add', - text: formatValue(rightItem, undefined, undefined, options.undefinedBehavior), - }); - } - } - + [linesLeft, linesRight] = processMatchedObjects(leftItem, rightItem, level, options, linesLeft, linesRight); leftProcessed.add(i); rightProcessed.add(matchIndex); } } - - // Second pass: handle remaining items (unmatched) - for (let i = 0; i < arrLeft.length; i++) { - if (leftProcessed.has(i)) continue; - - const leftItem = arrLeft[i]; - const removedLines = stringify(leftItem, undefined, 1, undefined, options.undefinedBehavior).split('\n'); - for (let j = 0; j < removedLines.length; j++) { - linesLeft.push({ - level: level + 1 + (removedLines[j].match(/^\s+/)?.[0]?.length || 0), - type: 'remove', - text: removedLines[j].replace(/^\s+/, '').replace(/,$/g, ''), - }); - linesRight.push({ level: level + 1, type: 'equal', text: '' }); - } + + // Second pass: handle remaining items (unmatched) with potential early exit + if (leftProcessed.size < arrLeft.length) { + [linesLeft, linesRight] = processRemovedItems(arrLeft, leftProcessed, level, options, linesLeft, linesRight); } - - for (let i = 0; i < arrRight.length; i++) { - if (rightProcessed.has(i)) continue; - - const rightItem = arrRight[i]; - const addedLines = stringify(rightItem, undefined, 1, undefined, options.undefinedBehavior).split('\n'); - for (let j = 0; j < addedLines.length; j++) { - linesLeft.push({ level: level + 1, type: 'equal', text: '' }); - linesRight.push({ - level: level + 1 + (addedLines[j].match(/^\s+/)?.[0]?.length || 0), - type: 'add', - text: addedLines[j].replace(/^\s+/, '').replace(/,$/g, ''), - }); - } + if (rightProcessed.size < arrRight.length) { + [linesLeft, linesRight] = processAddedItems(arrRight, rightProcessed, level, options, linesLeft, linesRight); } } @@ -235,7 +261,59 @@ function diffArrayRecursive( return [linesLeft, linesRight]; } +// Helper function to process unmatched items (removed from left) +function processRemovedItems( + arr: any[], + processed: Set, + level: number, + options: DifferOptions, + linesLeft: DiffResult[], + linesRight: DiffResult[] +): [DiffResult[], DiffResult[]] { + for (let i = 0; i < arr.length; i++) { + if (processed.has(i)) continue; + + const item = arr[i]; + const removedLines = stringify(item, undefined, 1, undefined, options.undefinedBehavior).split('\n'); + for (let j = 0; j < removedLines.length; j++) { + linesLeft.push({ + level: level + 1 + (removedLines[j].match(/^\s+/)?.[0]?.length || 0), + type: 'remove', + text: removedLines[j].replace(/^\s+/, '').replace(/,$/g, ''), + }); + linesRight.push({ level: level + 1, type: 'equal', text: '' }); + } + } + return [linesLeft, linesRight]; +} + +// Helper function to process unmatched items (added to right) +function processAddedItems( + arr: any[], + processed: Set, + level: number, + options: DifferOptions, + linesLeft: DiffResult[], + linesRight: DiffResult[] +): [DiffResult[], DiffResult[]] { + for (let i = 0; i < arr.length; i++) { + if (processed.has(i)) continue; + + const item = arr[i]; + const addedLines = stringify(item, undefined, 1, undefined, options.undefinedBehavior).split('\n'); + for (let j = 0; j < addedLines.length; j++) { + linesLeft.push({ level: level + 1, type: 'equal', text: '' }); + linesRight.push({ + level: level + 1 + (addedLines[j].match(/^\s+/)?.[0]?.length || 0), + type: 'add', + text: addedLines[j].replace(/^\s+/, '').replace(/,$/g, ''), + }); + } + } + return [linesLeft, linesRight]; +} + const diffArrayCompareKey = diffArrayRecursive; export default diffArrayCompareKey; -export { allObjectsHaveCompareKey }; \ No newline at end of file +export { allObjectsHaveCompareKey }; From 6c60fcac1954d6ec9e61ebf512dac921aae8a65f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=8B=87?= Date: Tue, 1 Jul 2025 02:22:44 +0800 Subject: [PATCH 2/4] fix: Optimize array diffing and validation logic Improved performance in diffing arrays of objects by batching concatenations and simplifying the validation function for compareKey. Updated comments in tests for clarity and replaced Chinese with English. The validation function now returns a boolean, streamlining the recursive diff logic. --- src/utils/diff-array-compare-key.spec.ts | 8 ++--- src/utils/diff-array-compare-key.ts | 41 ++++++++++++++---------- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/utils/diff-array-compare-key.spec.ts b/src/utils/diff-array-compare-key.spec.ts index 3b8efd9..f93cd55 100644 --- a/src/utils/diff-array-compare-key.spec.ts +++ b/src/utils/diff-array-compare-key.spec.ts @@ -173,10 +173,10 @@ describe('Utility function: diff-array-compare-key', () => { expect(linesRight).toContainEqual(expect.objectContaining({ text: '[' })); // Should show specific changes in the nested array content - // 数组 [1, 2, 3] vs [1, 3, 4] 按位置比较: - // - 位置0: 1 vs 1 → equal - // - 位置1: 2 vs 3 → modify - // - 位置2: 3 vs 4 → modify + // Array [1, 2, 3] vs [1, 3, 4] compared by position: + // - Position 0: 1 vs 1 → equal + // - Position 1: 2 vs 3 → modify + // - Position 2: 3 vs 4 → modify // Verify equal element expect(linesLeft.some((line) => line.text === '1' && line.type === 'equal')).toBe(true); diff --git a/src/utils/diff-array-compare-key.ts b/src/utils/diff-array-compare-key.ts index 095be71..68eeaa3 100644 --- a/src/utils/diff-array-compare-key.ts +++ b/src/utils/diff-array-compare-key.ts @@ -48,14 +48,17 @@ function processMatchedObjects( linesRight.push({ level: level + 1, type: 'equal', text: '{' }); const keys = Array.from(new Set([...Object.keys(leftItem), ...Object.keys(rightItem)])); + const leftArraysToConcat: DiffResult[][] = []; + const rightArraysToConcat: DiffResult[][] = []; + for (const key of keys) { const lVal = leftItem[key]; const rVal = rightItem[key]; if (Array.isArray(lVal) && Array.isArray(rVal)) { // Recursively diff arrays const [arrL, arrR] = diffArrayRecursive(lVal, rVal, '', '', level + 2, options, [], []); - linesLeft = concat(linesLeft, arrL); - linesRight = concat(linesRight, arrR); + leftArraysToConcat.push(arrL); + rightArraysToConcat.push(arrR); } else if (Array.isArray(lVal) || Array.isArray(rVal)) { // If only one side is array, treat as modification prettyAppendLines(linesLeft, linesRight, key, key, lVal, rVal, level + 2, options); @@ -68,10 +71,18 @@ function processMatchedObjects( options, diffArrayRecursive ); - linesLeft = concat(linesLeft, leftLines); - linesRight = concat(linesRight, rightLines); + leftArraysToConcat.push(leftLines); + rightArraysToConcat.push(rightLines); } } + + // Concatenate all collected arrays at once for better performance + if (leftArraysToConcat.length > 0) { + linesLeft.push(...leftArraysToConcat.flat()); + } + if (rightArraysToConcat.length > 0) { + linesRight.push(...rightArraysToConcat.flat()); + } linesLeft.push({ level: level + 1, type: 'equal', text: '}' }); linesRight.push({ level: level + 1, type: 'equal', text: '}' }); } else if (leftType === 'array') { @@ -98,29 +109,29 @@ function processMatchedObjects( } // Optimized validation function that combines type checking and compare key validation in single pass -function validateArrayForCompareKey(arr: any[], compareKey: string): { isValid: boolean; objectCount: number } { - let objectCount = 0; +function validateArrayForCompareKey(arr: any[], compareKey: string): boolean { + let hasValidObjects = false; for (const item of arr) { const type = getType(item); if (type === 'object') { - objectCount++; - if (!(compareKey in item)) return { isValid: false, objectCount: 0 }; + hasValidObjects = true; + if (!(compareKey in item)) return false; // Check nested arrays in object values for (const value of Object.values(item)) { if (Array.isArray(value) && !allObjectsHaveCompareKey(value, compareKey)) { - return { isValid: false, objectCount: 0 }; + return false; } } } else if (Array.isArray(item)) { - if (!allObjectsHaveCompareKey(item, compareKey)) return { isValid: false, objectCount: 0 }; + if (!allObjectsHaveCompareKey(item, compareKey)) return false; } else { // Non-object, non-array items make the array invalid for compareKey strategy - return { isValid: false, objectCount: 0 }; + return false; } } - return { isValid: objectCount > 0, objectCount }; + return hasValidObjects; } // Recursively checks if all objects (including in nested arrays) have the compare key @@ -162,7 +173,7 @@ function diffArrayRecursive( const leftValidation = validateArrayForCompareKey(arrLeft, options.compareKey); const rightValidation = validateArrayForCompareKey(arrRight, options.compareKey); - if (!leftValidation.isValid || !rightValidation.isValid) { + if (!leftValidation || !rightValidation) { // Use unordered LCS for arrays of primitives, mixed types, or missing compare key return diffArrayNormal(arrLeft, arrRight, keyLeft, keyRight, level, options, linesLeft, linesRight); } @@ -178,18 +189,14 @@ function diffArrayRecursive( // For small arrays, use simple O(n²) approach to avoid Map overhead const useMapOptimization = arrLeft.length > 10 || arrRight.length > 10; let rightKeyMap: Map | null = null; - let rightValidItems: Set | null = null; if (useMapOptimization) { // Build a map of compareKey values to right array items for O(n) matching - // Only store items that passed validation to reduce memory usage rightKeyMap = new Map(); - rightValidItems = new Set(); // Track valid items to avoid re-checking for (let j = 0; j < arrRight.length; j++) { const rightItem = arrRight[j]; if (isObjectWithCompareKey(rightItem, options.compareKey)) { - rightValidItems.add(j); const rightKeyValue = rightItem[options.compareKey]; if (!rightKeyMap.has(rightKeyValue)) { rightKeyMap.set(rightKeyValue, []); From b26ba79a3bc38518ddd4472e1b1eb643ca7b1b92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=8B=87?= Date: Tue, 1 Jul 2025 20:09:35 +0800 Subject: [PATCH 3/4] fix: Add large-scale and diverse structure tests for diffArrayCompareKey Introduces comprehensive tests to validate diffArrayCompareKey with large arrays, mixed operations, and diverse JSON object structures. These tests ensure correct handling, performance, and structural integrity under various scenarios. --- src/utils/diff-array-compare-key.spec.ts | 202 +++++++++++++++++++++++ 1 file changed, 202 insertions(+) diff --git a/src/utils/diff-array-compare-key.spec.ts b/src/utils/diff-array-compare-key.spec.ts index f93cd55..fed02c6 100644 --- a/src/utils/diff-array-compare-key.spec.ts +++ b/src/utils/diff-array-compare-key.spec.ts @@ -412,4 +412,206 @@ describe('Utility function: diff-array-compare-key', () => { expect(linesRight.some((line) => line.text.includes('Infinity'))).toBe(true); }); }); + + // Large-scale scenario tests to validate compare by key logic with many JSON objects + describe('diffArrayCompareKey - Large-scale validation', () => { + it('should handle dozens of JSON objects correctly with various structures', () => { + // Create arrays with dozens of objects to validate large-scale handling + const createLargeArray = (size: number, baseId = 0) => { + return Array.from({ length: size }, (_, i) => ({ + id: baseId + i + 1, + name: `Item ${baseId + i + 1}`, + type: ['user', 'admin', 'guest'][i % 3], + metadata: { + created: `2024-01-${String(i % 28 + 1).padStart(2, '0')}`, + tags: [`tag${i % 5}`, `category${i % 3}`], + settings: { + enabled: i % 2 === 0, + priority: i % 10, + config: { + theme: ['light', 'dark'][i % 2], + locale: ['en', 'es', 'fr'][i % 3], + }, + }, + }, + data: Array.from({ length: i % 3 + 1 }, (_, j) => ({ + id: (i + 1) * 10 + j, + value: `value-${i}-${j}`, + })), + })); + }; + + const leftArray = createLargeArray(50); + const rightArray = [ + ...createLargeArray(30), // First 30 items unchanged + ...createLargeArray(15, 35).map((item) => ({ // Items 36-50, but modified + ...item, + name: `Modified ${item.name}`, + metadata: { + ...item.metadata, + updated: '2024-06-01', + }, + })), + ...createLargeArray(10, 60), // New items 61-70 + ]; + + const options = createOptions(); + const [linesLeft, linesRight] = diffArrayCompareKey(leftArray, rightArray, '', '', 0, options); + + // Verify the diff results + expect(linesLeft).toBeDefined(); + expect(linesRight).toBeDefined(); + expect(linesLeft.length).toBeGreaterThan(0); + expect(linesRight.length).toBeGreaterThan(0); + + // Should have proper array structure brackets + expect(linesLeft.length).toBeGreaterThan(0); + expect(linesRight.length).toBeGreaterThan(0); + + // Both sides should start and end with array brackets + expect(linesLeft[0].text).toBe('['); + expect(linesLeft[linesLeft.length - 1].text).toBe(']'); + expect(linesRight[0].text).toBe('['); + expect(linesRight[linesRight.length - 1].text).toBe(']'); + + // Verify that items 1-30 are matched correctly (should appear as equal or modify) + const item1Left = linesLeft.find((line) => line.text.includes('"name": "Item 1"')); + const item1Right = linesRight.find((line) => line.text.includes('"name": "Item 1"')); + expect(item1Left).toBeDefined(); + expect(item1Right).toBeDefined(); + + // Verify that items 31-50 are removed (only in left) + const removedItems = linesLeft.filter((line) => line.type === 'remove' && line.text.includes('"name": "Item 3')); + expect(removedItems.length).toBeGreaterThan(0); + + // Verify that items 61-70 are added (only in right) + const addedItems = linesRight.filter((line) => line.type === 'add' && line.text.includes('"name": "Item 6')); + expect(addedItems.length).toBeGreaterThan(0); + }); + + it('should maintain performance with large arrays and mixed operations', () => { + const startTime = process.hrtime.bigint(); + + // Create even larger arrays to test performance + const createComplexArray = (size: number) => { + return Array.from({ length: size }, (_, i) => ({ + id: `complex-${i}`, + data: { + values: Array.from({ length: 10 }, (_, j) => ({ + id: `val-${i}-${j}`, + content: `Content for item ${i}, value ${j}`, + nested: { + deep: { + props: [`prop${j}`, `attr${i % 5}`], + }, + }, + })), + metadata: { + created: Date.now() + i * 1000, + tags: Array.from({ length: i % 5 + 1 }, (_, k) => `tag-${k}`), + }, + }, + })); + }; + + const leftLarge = createComplexArray(100); + const rightLarge = [ + ...createComplexArray(50), // First 50 unchanged + ...createComplexArray(30).map((item, i) => ({ // 30 modified items + ...item, + id: `complex-${i + 50}`, + data: { + ...item.data, + metadata: { + ...item.data.metadata, + updated: true, + }, + }, + })), + ...createComplexArray(40).map((item, i) => ({ // 40 new items + ...item, + id: `complex-new-${i}`, + })), + ]; + + const options = createOptions(); + const [linesLeft, linesRight] = diffArrayCompareKey(leftLarge, rightLarge, '', '', 0, options); + + const endTime = process.hrtime.bigint(); + const executionTime = Number(endTime - startTime) / 1_000_000; // Convert to milliseconds + + // Verify results are correct + expect(linesLeft).toBeDefined(); + expect(linesRight).toBeDefined(); + expect(linesLeft.length).toBeGreaterThan(0); + expect(linesRight.length).toBeGreaterThan(0); + + // Performance should be reasonable (less than 1 second for this scale) + expect(executionTime).toBeLessThan(1000); + + // Verify structural integrity - both sides should have proper array structure + expect(linesLeft[0].text).toBe('['); + expect(linesLeft[linesLeft.length - 1].text).toBe(']'); + expect(linesRight[0].text).toBe('['); + expect(linesRight[linesRight.length - 1].text).toBe(']'); + }); + + it('should correctly handle various JSON object structures without corruption', () => { + // Test with diverse object structures as suggested by the user + const diverseObjects = [ + { id: 'user1', type: 'user', profile: { name: 'John', age: 30 } }, + { id: 'org1', type: 'organization', details: { name: 'Company A', employees: 100 } }, + { id: 'prod1', type: 'product', info: { title: 'Widget', price: 29.99, tags: ['electronics', 'gadget'] } }, + { id: 'event1', type: 'event', data: { name: 'Conference', date: '2024-07-01', attendees: [] } }, + { id: 'config1', type: 'configuration', settings: { theme: 'dark', notifications: true, preferences: {} } }, + ]; + + const modifiedObjects = [ + { id: 'user1', type: 'user', profile: { name: 'John Smith', age: 31 } }, // Modified + { id: 'org1', type: 'organization', details: { name: 'Company A', employees: 120 } }, // Modified + { id: 'prod2', type: 'product', info: { title: 'New Widget', price: 39.99, tags: ['electronics'] } }, // New + { id: 'event1', type: 'event', data: { name: 'Conference', date: '2024-07-01', attendees: ['John'] } }, // Modified + // config1 removed, prod1 removed + ]; + + const options = createOptions(); + const [linesLeft, linesRight] = diffArrayCompareKey(diverseObjects, modifiedObjects, '', '', 0, options); + + // Verify no corruption in output structure + expect(linesLeft).toBeDefined(); + expect(linesRight).toBeDefined(); + + // Should have proper array brackets + expect(linesLeft[0].text).toBe('['); + expect(linesLeft[linesLeft.length - 1].text).toBe(']'); + expect(linesRight[0].text).toBe('['); + expect(linesRight[linesRight.length - 1].text).toBe(']'); + + // Verify that modifications are detected correctly + const userModified = linesLeft.some((line) => line.text.includes('"name": "John"')) && + linesRight.some((line) => line.text.includes('"name": "John Smith"')); + expect(userModified).toBe(true); + + // Verify that removals are handled correctly + const prodRemoved = linesLeft.some((line) => line.type === 'remove' && line.text.includes('prod1')); + expect(prodRemoved).toBe(true); + + // Verify that additions are handled correctly + const prodAdded = linesRight.some((line) => line.type === 'add' && line.text.includes('prod2')); + expect(prodAdded).toBe(true); + + // Ensure no lines are corrupted (all should have valid text) + linesLeft.forEach((line) => { + expect(typeof line.text).toBe('string'); + expect(line.level).toBeGreaterThanOrEqual(0); + expect(['equal', 'add', 'remove', 'modify']).toContain(line.type); + }); + + linesRight.forEach((line) => { + expect(typeof line.text).toBe('string'); + expect(line.level).toBeGreaterThanOrEqual(0); + expect(['equal', 'add', 'remove', 'modify']).toContain(line.type); + }); + }); + }); }); From 19ff92e3b6b5bb3969925c1ebd531aa6ec1d9eae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=8B=87?= Date: Tue, 1 Jul 2025 20:32:07 +0800 Subject: [PATCH 4/4] fix: Expand and refactor complex structure test cases Increased the size and complexity of arrays in the performance test to better simulate real-world scenarios. Refactored the JSON structure test to use a single complex object type, updated modification/addition/removal checks, and improved clarity of test assertions. --- src/utils/diff-array-compare-key.spec.ts | 111 +++++++++++++++++------ 1 file changed, 81 insertions(+), 30 deletions(-) diff --git a/src/utils/diff-array-compare-key.spec.ts b/src/utils/diff-array-compare-key.spec.ts index fed02c6..7bfe24b 100644 --- a/src/utils/diff-array-compare-key.spec.ts +++ b/src/utils/diff-array-compare-key.spec.ts @@ -492,12 +492,12 @@ describe('Utility function: diff-array-compare-key', () => { it('should maintain performance with large arrays and mixed operations', () => { const startTime = process.hrtime.bigint(); - // Create even larger arrays to test performance + // Create much larger arrays to test real-world performance - 1000+ objects const createComplexArray = (size: number) => { return Array.from({ length: size }, (_, i) => ({ id: `complex-${i}`, data: { - values: Array.from({ length: 10 }, (_, j) => ({ + values: Array.from({ length: 5 }, (_, j) => ({ // Reduced nested array size for 1k objects id: `val-${i}-${j}`, content: `Content for item ${i}, value ${j}`, nested: { @@ -508,18 +508,18 @@ describe('Utility function: diff-array-compare-key', () => { })), metadata: { created: Date.now() + i * 1000, - tags: Array.from({ length: i % 5 + 1 }, (_, k) => `tag-${k}`), + tags: Array.from({ length: i % 3 + 1 }, (_, k) => `tag-${k}`), // Reduced tag array size }, }, })); }; - const leftLarge = createComplexArray(100); + const leftLarge = createComplexArray(1000); // 1000 objects const rightLarge = [ - ...createComplexArray(50), // First 50 unchanged - ...createComplexArray(30).map((item, i) => ({ // 30 modified items + ...createComplexArray(600), // First 600 unchanged + ...createComplexArray(200).map((item, i) => ({ // 200 modified items ...item, - id: `complex-${i + 50}`, + id: `complex-${i + 600}`, data: { ...item.data, metadata: { @@ -528,7 +528,7 @@ describe('Utility function: diff-array-compare-key', () => { }, }, })), - ...createComplexArray(40).map((item, i) => ({ // 40 new items + ...createComplexArray(300).map((item, i) => ({ // 300 new items ...item, id: `complex-new-${i}`, })), @@ -546,7 +546,7 @@ describe('Utility function: diff-array-compare-key', () => { expect(linesLeft.length).toBeGreaterThan(0); expect(linesRight.length).toBeGreaterThan(0); - // Performance should be reasonable (less than 1 second for this scale) + // Performance should be reasonable (1000+ objects should complete in under 1 second) expect(executionTime).toBeLessThan(1000); // Verify structural integrity - both sides should have proper array structure @@ -556,26 +556,77 @@ describe('Utility function: diff-array-compare-key', () => { expect(linesRight[linesRight.length - 1].text).toBe(']'); }); - it('should correctly handle various JSON object structures without corruption', () => { - // Test with diverse object structures as suggested by the user - const diverseObjects = [ - { id: 'user1', type: 'user', profile: { name: 'John', age: 30 } }, - { id: 'org1', type: 'organization', details: { name: 'Company A', employees: 100 } }, - { id: 'prod1', type: 'product', info: { title: 'Widget', price: 29.99, tags: ['electronics', 'gadget'] } }, - { id: 'event1', type: 'event', data: { name: 'Conference', date: '2024-07-01', attendees: [] } }, - { id: 'config1', type: 'configuration', settings: { theme: 'dark', notifications: true, preferences: {} } }, + it('should correctly handle complex JSON structures without corruption', () => { + // Test with one type of complex structure - algorithm is generic anyway + const complexObjects = [ + { + id: 'obj1', + data: { + name: 'Test Object 1', + nested: { + values: [1, 2, 3], + config: { enabled: true, priority: 5 }, + }, + }, + }, + { + id: 'obj2', + data: { + name: 'Test Object 2', + nested: { + values: [4, 5], + config: { enabled: false, priority: 1 }, + }, + }, + }, + { + id: 'obj3', + data: { + name: 'Test Object 3', + nested: { + values: [], + config: { enabled: true, priority: 10 }, + }, + }, + }, ]; const modifiedObjects = [ - { id: 'user1', type: 'user', profile: { name: 'John Smith', age: 31 } }, // Modified - { id: 'org1', type: 'organization', details: { name: 'Company A', employees: 120 } }, // Modified - { id: 'prod2', type: 'product', info: { title: 'New Widget', price: 39.99, tags: ['electronics'] } }, // New - { id: 'event1', type: 'event', data: { name: 'Conference', date: '2024-07-01', attendees: ['John'] } }, // Modified - // config1 removed, prod1 removed + { + id: 'obj1', + data: { + name: 'Modified Test Object 1', // Modified + nested: { + values: [1, 2, 3, 4], // Modified + config: { enabled: true, priority: 5 }, + }, + }, + }, + { + id: 'obj2', + data: { + name: 'Test Object 2', + nested: { + values: [4, 5], + config: { enabled: false, priority: 1 }, + }, + }, + }, + // obj3 removed + { + id: 'obj4', // New object + data: { + name: 'New Test Object 4', + nested: { + values: [7, 8, 9], + config: { enabled: true, priority: 3 }, + }, + }, + }, ]; const options = createOptions(); - const [linesLeft, linesRight] = diffArrayCompareKey(diverseObjects, modifiedObjects, '', '', 0, options); + const [linesLeft, linesRight] = diffArrayCompareKey(complexObjects, modifiedObjects, '', '', 0, options); // Verify no corruption in output structure expect(linesLeft).toBeDefined(); @@ -588,17 +639,17 @@ describe('Utility function: diff-array-compare-key', () => { expect(linesRight[linesRight.length - 1].text).toBe(']'); // Verify that modifications are detected correctly - const userModified = linesLeft.some((line) => line.text.includes('"name": "John"')) && - linesRight.some((line) => line.text.includes('"name": "John Smith"')); - expect(userModified).toBe(true); + const nameModified = linesLeft.some((line) => line.text.includes('"name": "Test Object 1"')) && + linesRight.some((line) => line.text.includes('"name": "Modified Test Object 1"')); + expect(nameModified).toBe(true); // Verify that removals are handled correctly - const prodRemoved = linesLeft.some((line) => line.type === 'remove' && line.text.includes('prod1')); - expect(prodRemoved).toBe(true); + const objRemoved = linesLeft.some((line) => line.type === 'remove' && line.text.includes('obj3')); + expect(objRemoved).toBe(true); // Verify that additions are handled correctly - const prodAdded = linesRight.some((line) => line.type === 'add' && line.text.includes('prod2')); - expect(prodAdded).toBe(true); + const objAdded = linesRight.some((line) => line.type === 'add' && line.text.includes('obj4')); + expect(objAdded).toBe(true); // Ensure no lines are corrupted (all should have valid text) linesLeft.forEach((line) => {