class Comparator {
constructor() {
this.cache = new WeakMap();
this.patternCache = new Map();
this.configCache = new Map();
}
compare(a, b, config = {}) {
return this._coreCompare(a, b, {
path: '',
depth: 0,
filterObjectKeys: this._normalizePatterns(config.filterObjectKeys || []),
filterArrayKeys: config.filterArrayKeys || {}
});
}
_coreCompare(a, b, ctx) {
console.log('a, b, ctx')
if (this._isFiltered(ctx.path, ctx.filterObjectKeys)) return true;
if (Object.is(a, b)) return true;
const aType = this._getType(a);
const bType = this._getType(b);
if (aType !== bType) return false;
if (this._isObject(a)) {
const cached = this._getCache(a, b);
if (cached !== undefined) return cached;
}
let result;
switch (true) {
case Array.isArray(a):
result = this._compareArrays(a, b, ctx);
break;
case a instanceof Date:
result = a.getTime() === b.getTime();
break;
case this._isObject(a):
result = this._compareObjects(a, b, ctx);
break;
default:
result = false;
}
if (this._isObject(a)) this._setCache(a, b, result);
return result;
}
_compareObjects(objA, objB, ctx) {
const keys = new Set([...Object.keys(objA), ...Object.keys(objB)]);
return Array.from(keys).every(key => {
const newCtx = this._updateContext(ctx, key);
if (this._isFiltered(newCtx.path, newCtx.filterObjectKeys)) return true;
return this._coreCompare(objA[key], objB[key], newCtx);
});
}
_compareArrays(arrA, arrB, ctx) {
if (this._isFiltered(ctx.path, ctx.filterObjectKeys)) return true;
const config = this._getArrayConfig(ctx);
const [filteredA, filteredB] = this._filterArrays(arrA, arrB, config);
if (filteredA.length !== filteredB.length) return false;
if (config.arrayOrderMatters) {
return filteredA.every((item, i) =>
this._coreCompare(item, filteredB[i], this._updateContext(ctx, i))
);
}
return this._unorderedCompare(filteredA, filteredB, ctx);
}
_getType(obj) {
return Object.prototype.toString.call(obj);
}
_isObject(v) {
return v !== null && typeof v === 'object' && !Array.isArray(v);
}
_updateContext(ctx, segment) {
const path = ctx.path ? `${ctx.path}.${segment}` : segment.toString();
return {
...ctx,
path,
depth: ctx.depth + 1
};
}
_getCache(a, b) {
return this.cache.get(a) ?.get(b) || this.cache.get(b) ?.get(a);
}
_setCache(a, b, value) {
if (!this.cache.has(a)) this.cache.set(a, new WeakMap());
this.cache.get(a).set(b, value);
}
_getArrayConfig(ctx) {
const cached = this.configCache.get(ctx.path);
if (cached) return cached;
for (const [pattern, config] of Object.entries(ctx.filterArrayKeys)) {
if (this._matchPattern(ctx.path, pattern)) {
this.configCache.set(ctx.path, config);
return config;
}
}
return {};
}
_normalizePatterns(patterns) {
return patterns.map(p => p.replace(/\.\*\*/g, '.[^.]+(\\..*)?'));
}
_isFiltered(path, patterns) {
return patterns.some(pattern => this._matchPattern(path, pattern));
}
_matchPattern(path, pattern) {
const normalized = pattern
.replace(/$$(\d+)$$/g, '.$1')
.replace(/\*/g, '[^.]+')
.replace(/\.\*\*/g, '.[^.]+(\\..*)?');
const regex = new RegExp(`^${normalized}$`);
return regex.test(path);
}
_filterArrays(arrA, arrB, config) {
const filter = new Set(config.filterArrayValues || []);
return [
arrA.filter((_, i) => !filter.has(i)),
arrB.filter((_, i) => !filter.has(i))
];
}
_unorderedCompare(a, b, ctx) {
const map = new Map();
a.forEach(item => {
const key = this._createHashKey(item, ctx);
map.set(key, (map.get(key) || 0) + 1);
});
return b.every(item => {
const key = this._createHashKey(item, ctx);
const count = map.get(key) || 0;
if (count === 0) return false;
map.set(key, count - 1);
return true;
});
}
_createHashKey(item, ctx) {
return JSON.stringify(item, (_, v) => {
if (this._isObject(v)) return '';
return v;
}) + '|' + ctx.depth;
}
}
const comparator = new Comparator();
const snapshot = {
name: "John1",
age: 30,
hobbies: [
{ name: "reading", level: "high" },
{ name: "coding", level: "medium" },
],
arr: [[{ a: 1, b: 2 }], [{ a: 2, b: 3 }]],
address: {
city: "New York",
zip: "10001"
},
orderArr: ['a', 'v', 's', 'd'],
orderArrObj: [{ a: 1, b: 2 }, { a: 2, b: 3 }],
users: [
{ id: 1, name: 'Alice', permissions: new Set(['read']) },
{ id: 2, name: 'Bob' }
],
meta: { timestamp: new Date('2023-01-01') }
};
const form = {
name: "John",
age: 30,
hobbies: [
{ name: "reading", level: "high" },
],
arr: [[{ a: 1, b: 2 }], [{ a: 2, b: 5 }]],
address: {
city: "New York",
zip: "10002"
},
orderArr: ['a', 'v', 'd', 's'],
orderArrObj: [{ a: 1, b: 2 }, { a: 2, b: 5 }],
users: [
{ id: 2, name: 'Bob' },
{ id: 1, name: 'Alice', permissions: new Set(['read']) }
],
meta: { timestamp: new Date('2023-01-01') }
};
const omit = {
filterObjectKeys: ["age", 'hobbies[*].level', 'arr', 'orderArr', 'address.zip', 'orderArrObj', 'users', 'meta'],
filterArrayKeys: {
"hobbies": {
filterArrayValues: [1]
},
}
};
const result = comparator.compare(snapshot, form, omit);
console.log(result);