Compare commits

..
3 Commits
Author SHA1 Message Date
ztimson 383c0a3939 isEqual callback safety
Build / Publish Docs (push) Successful in 29s
Build / Build NPM Project (push) Successful in 32s
Build / Tag Version (push) Successful in 7s
2026-10-08 12:28:30 -04:00
ztimson f0849255df isEqual callback support for custom equality checks
Build / Publish Docs (push) Successful in 56s
Build / Build NPM Project (push) Successful in 58s
Build / Tag Version (push) Successful in 12s
2026-10-08 12:13:22 -04:00
ztimson 54aaa58990 Added everyNested to walk nested objects/arrays
Build / Publish Docs (push) Successful in 31s
Build / Build NPM Project (push) Successful in 36s
Build / Tag Version (push) Successful in 7s
2026-10-08 11:28:05 -04:00
2 changed files with 47 additions and 36 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ztimson/utils",
"version": "0.30.10",
"version": "0.31.1",
"description": "Utility library",
"author": "Zak Timson",
"license": "MIT",
+32 -21
View File
@@ -165,6 +165,15 @@ export function encodeQuery(data: any): string {
).join('&');
}
/** Recursively walks obj/arrays, calling cb(dotPath, value) for every leaf+branch prop */
export function everyNested(obj: any, cb: (key: string, value: any) => void, prefix = ''){
Object.entries(obj).forEach(([k, v]) => {
const key = prefix ? `${prefix}.${k}` : k;
cb(key, v);
if (v && typeof v === 'object') everyNested(v, cb, key);
});
}
/**
* Recursively flatten a nested object, while maintaining key structure
*
@@ -234,50 +243,52 @@ export function includes(target: any, values: any, allowMissing = false): boolea
/**
* Deep check if two items are equal.
* Handles primitives, objects, arrays, functions, Date, RegExp, and circular references.
* Handles primitives, objects, arrays, functions, Date, RegExp, circular refs,
* and optional key-based exclusions.
*
* @param {any} a - first item to compare
* @param {any} b - second item to compare
* @param {WeakMap<object, object>} [seen] - Internal parameter to track circular references
* @param {string[] | ((key: string, a: any, b: any) => boolean | void)} [compare] -
* Ignore patterns (dot-path, '*' = wildcard segment, matches whole subtree) OR
* a comparator callback: return true/false to short-circuit, undefined to use default logic.
* @param {WeakMap<object, object>} [seen] - Internal, tracks circular references
* @param {string} [path] - Internal, current dot-path
* @returns {boolean} True if they match
*/
export function isEqual(a: any, b: any, seen = new WeakMap<object, object>()): boolean {
// Simple cases
export function isEqual(a: any, b: any, compare?: string[] | ((key: string, a: any, b: any) => boolean | void)): boolean {
const eq = (a: any, b: any, compare: any, seen: WeakMap<object, object>, path: string) => {
if(Array.isArray(compare)) {
if(compare.some(p => new RegExp(`^${p.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^.]*')}(\\.|$)`).test(path))) return true;
} else if(typeof compare === 'function') {
const r = compare(path, a, b);
if(r !== undefined) return r;
}
if(a === b) return true;
if(a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
if(a instanceof RegExp && b instanceof RegExp) return a.source === b.source && a.flags === b.flags;
// Null checks
if(typeof a !== 'object' || a === null || typeof b !== 'object' || b === null) {
if(Number.isNaN(a) && Number.isNaN(b)) return true;
if(typeof a === 'function' && typeof b === 'function') return a.toString() === b.toString()
if(typeof a === 'function' && typeof b === 'function') return a.toString() === b.toString();
return false;
}
// Circular references
if(seen.has(a)) return seen.get(a) === b;
seen.set(a, b);
const isArrayA = Array.isArray(a);
const isArrayB = Array.isArray(b);
// Array checks
const isArrayA = Array.isArray(a), isArrayB = Array.isArray(b);
if(isArrayA && isArrayB) {
if(a.length !== b.length) return false;
for(let i = 0; i < a.length; i++) {
if(!isEqual(a[i], b[i], seen)) return false;
}
for(let i = 0; i < a.length; i++) if(!eq(a[i], b[i], compare, seen, path ? `${path}.${i}` : `${i}`)) return false;
return true;
}
if(isArrayA !== isArrayB) return false;
// Key & value deep comparison
const keysA = Object.keys(a);
const keysB = Object.keys(b);
const keysA = Object.keys(a), keysB = Object.keys(b);
if(keysA.length !== keysB.length) return false;
for(const key of keysA) {
if(!Object.prototype.hasOwnProperty.call(b, key) || !isEqual(a[key], b[key], seen)) return false;
if(!Object.prototype.hasOwnProperty.call(b, key) || !eq(a[key], b[key], compare, seen, path ? `${path}.${key}` : key)) return false;
}
return true;
}
return eq(a, b, compare, new WeakMap(), '');
}
/**