object-deep-compare
Version:
A type-safe collection of comparison methods for objects and arrays in TypeScript/JavaScript
316 lines (315 loc) • 14.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.handleDepthComparison = void 0;
const errors_1 = require("./errors");
const path_filtering_1 = require("./path-filtering");
const utils_1 = require("./utils");
/**
* Handles comparison for arrays and objects
* @param firstValue - First value to compare
* @param secondValue - Second value to compare
* @param currentPath - Current path for conflicts
* @param options - Comparison options
* @param isArrayComparison - Whether this is an array comparison
* @param detailed - Whether to return detailed difference information
* @param firstVisited - Map of already visited objects in the first object tree
* @param secondVisited - Map of already visited objects in the second object tree
* @returns Array of conflict paths or detailed differences, or boolean indicating equality
*/
const handleDepthComparison = (firstValue, secondValue, currentPath, options, isArrayComparison, detailed = false, firstVisited = new Map(), secondVisited = new Map()) => {
const { strict = true, circularReferences = 'error', pathFilter } = options;
// If we should skip comparing this path due to pathFilter, return true (consider them equal)
if (currentPath && !(0, path_filtering_1.shouldComparePath)(currentPath, pathFilter)) {
return true;
}
// Handle Date objects specially
if (firstValue instanceof Date && secondValue instanceof Date) {
if (firstValue.getTime() === secondValue.getTime()) {
return true;
}
return detailed
? [{
path: currentPath,
type: 'changed',
oldValue: firstValue,
newValue: secondValue
}]
: [currentPath];
}
// Handle RegExp objects specially
if (firstValue instanceof RegExp && secondValue instanceof RegExp) {
if (firstValue.toString() === secondValue.toString()) {
return true;
}
return detailed
? [{
path: currentPath,
type: 'changed',
oldValue: firstValue,
newValue: secondValue
}]
: [currentPath];
}
// Check for circular references in arrays
if (Array.isArray(firstValue) && Array.isArray(secondValue)) {
// Check if either array has been visited before
const firstVisitedPath = firstVisited.get(firstValue);
const secondVisitedPath = secondVisited.get(secondValue);
if (firstVisitedPath !== undefined || secondVisitedPath !== undefined) {
// If handling is set to error, throw an error
if (circularReferences === 'error') {
throw new errors_1.CircularReferenceError(currentPath);
}
// If both arrays have been visited before and they reference the same relative position in their structures
if (firstVisitedPath !== undefined && secondVisitedPath !== undefined) {
// If both paths are the same, consider them equal
return true;
}
// If only one has been visited or they're at different positions, consider them different
return detailed
? [{
path: currentPath,
type: 'changed',
oldValue: firstValue,
newValue: secondValue
}]
: [currentPath];
}
// Mark arrays as visited before going deeper
firstVisited.set(firstValue, currentPath);
secondVisited.set(secondValue, currentPath);
if (firstValue.length !== secondValue.length) {
// If this is a direct array comparison and the path should be filtered
if (isArrayComparison && pathFilter && !(0, path_filtering_1.shouldComparePath)(currentPath, pathFilter)) {
return true; // Consider them equal if filtered
}
return isArrayComparison
? false
: (detailed
? [{
path: currentPath,
type: 'changed',
oldValue: firstValue,
newValue: secondValue
}]
: [currentPath]);
}
// For direct array comparison or nested arrays
const conflicts = detailed ? [] : [];
let hasConflict = false;
// Iterate through array elements and compare them
for (let i = 0; i < firstValue.length; i++) {
// Construct the array element path
const elemPath = `${currentPath}[${i}]`;
// Skip comparison if the path should be filtered
if (!(0, path_filtering_1.shouldComparePath)(elemPath, pathFilter)) {
continue;
}
// Compare array elements
if ((0, utils_1.isObject)(firstValue[i]) && (0, utils_1.isObject)(secondValue[i])) {
// Recursively compare objects within arrays
try {
const result = (0, exports.handleDepthComparison)(firstValue[i], secondValue[i], elemPath, options, false, detailed, new Map(firstVisited), // Create a new map to avoid shared references
new Map(secondVisited) // Create a new map to avoid shared references
);
if (result !== true) {
hasConflict = true;
if (Array.isArray(result)) {
if (detailed) {
conflicts.push(...result);
}
else {
conflicts.push(...result);
}
}
}
}
catch (error) {
if (error instanceof errors_1.CircularReferenceError) {
if (circularReferences === 'error') {
throw error;
}
// If circularReferences is 'ignore', continue with next comparison
}
else {
throw error;
}
}
}
else if (Array.isArray(firstValue[i]) && Array.isArray(secondValue[i])) {
// Recursively compare nested arrays
try {
const result = (0, exports.handleDepthComparison)(firstValue[i], secondValue[i], elemPath, options, true, detailed, new Map(firstVisited), // Create a new map to avoid shared references
new Map(secondVisited) // Create a new map to avoid shared references
);
if (result !== true) {
hasConflict = true;
if (Array.isArray(result)) {
if (detailed) {
conflicts.push(...result);
}
else {
conflicts.push(...result);
}
}
else if (result === false) {
// For arrays compared directly
if (detailed) {
conflicts.push({
path: elemPath,
type: 'changed',
oldValue: firstValue[i],
newValue: secondValue[i]
});
}
else {
conflicts.push(elemPath);
}
}
}
}
catch (error) {
if (error instanceof errors_1.CircularReferenceError) {
if (circularReferences === 'error') {
throw error;
}
// If circularReferences is 'ignore', continue with next comparison
}
else {
throw error;
}
}
}
else if (!(0, utils_1.areValuesEqual)(firstValue[i], secondValue[i], strict)) {
// For primitive values that are not equal
hasConflict = true;
if (detailed) {
conflicts.push({
path: elemPath,
type: 'changed',
oldValue: firstValue[i],
newValue: secondValue[i]
});
}
else {
conflicts.push(elemPath);
}
}
}
if (isArrayComparison && hasConflict && conflicts.length === 0) {
return false;
}
return conflicts.length > 0 ? conflicts : true;
}
// Handle objects
if ((0, utils_1.isObject)(firstValue) && (0, utils_1.isObject)(secondValue)) {
// Check if either object has been visited before
const firstVisitedPath = firstVisited.get(firstValue);
const secondVisitedPath = secondVisited.get(secondValue);
if (firstVisitedPath !== undefined || secondVisitedPath !== undefined) {
// If handling is set to error, throw an error
if (circularReferences === 'error') {
throw new errors_1.CircularReferenceError(currentPath);
}
// If both objects have been visited before and they reference the same relative position in their structures
if (firstVisitedPath !== undefined && secondVisitedPath !== undefined) {
// If both paths are the same, consider them equal
return true;
}
// If only one has been visited or they're at different positions, consider them different
return detailed
? [{
path: currentPath,
type: 'changed',
oldValue: firstValue,
newValue: secondValue
}]
: [currentPath];
}
// Mark objects as visited before going deeper
firstVisited.set(firstValue, currentPath);
secondVisited.set(secondValue, currentPath);
const allKeys = new Set([...Object.keys(firstValue), ...Object.keys(secondValue)]);
const conflicts = detailed ? [] : [];
for (const key of allKeys) {
const hasFirst = key in firstValue;
const hasSecond = key in secondValue;
const propPath = currentPath ? `${currentPath}.${key}` : key;
// Skip this property if it should be filtered based on pathFilter
if (!(0, path_filtering_1.shouldComparePath)(propPath, pathFilter)) {
continue;
}
// If key exists in one but not in the other
if (!hasFirst || !hasSecond) {
if (detailed) {
const type = !hasFirst ? 'added' : 'removed';
conflicts.push({
path: propPath,
type,
oldValue: !hasFirst ? undefined : firstValue[key],
newValue: !hasSecond ? undefined : secondValue[key]
});
}
else {
conflicts.push(propPath);
}
continue;
}
// Both objects have the key, compare their values
try {
const result = (0, exports.handleDepthComparison)(firstValue[key], secondValue[key], propPath, options, false, detailed, new Map(firstVisited), // Create a new map to avoid shared references
new Map(secondVisited) // Create a new map to avoid shared references
);
if (result !== true) {
if (Array.isArray(result)) {
if (detailed) {
conflicts.push(...result);
}
else {
conflicts.push(...result);
}
}
else if (typeof result === 'string') {
conflicts.push(result);
}
}
}
catch (error) {
if (error instanceof errors_1.CircularReferenceError) {
if (circularReferences === 'error') {
throw error;
}
// If circularReferences is 'ignore', just mark this property as different
if (detailed) {
conflicts.push({
path: propPath,
type: 'changed',
oldValue: firstValue[key],
newValue: secondValue[key]
});
}
else {
conflicts.push(propPath);
}
}
else {
throw error;
}
}
}
return conflicts.length > 0 ? conflicts : true;
}
// Handle primitive values
if ((0, utils_1.areValuesEqual)(firstValue, secondValue, strict)) {
return true;
}
return detailed
? [{
path: currentPath,
type: 'changed',
oldValue: firstValue,
newValue: secondValue
}]
: [currentPath];
};
exports.handleDepthComparison = handleDepthComparison;