UNPKG

data-diff-ts

Version:

Lightweight TypeScript utility for comparing objects, arrays, and nested data structures.

312 lines (307 loc) 9.98 kB
// src/diffArrays.ts function isPrimitive(value) { return value === null || typeof value !== "object" && typeof value !== "function"; } function deepEqual(a, b) { if (a === b) return true; if (typeof a !== typeof b) return false; if (typeof a !== "object" || a === null || b === null) return false; if (Array.isArray(a) && Array.isArray(b)) { if (a.length !== b.length) return false; for (let i = 0; i < a.length; i++) { if (!deepEqual(a[i], b[i])) return false; } return true; } if (Array.isArray(a) !== Array.isArray(b)) return false; const keysA = Object.keys(a); const keysB = Object.keys(b); if (keysA.length !== keysB.length) return false; for (const key of keysA) { if (!Object.prototype.hasOwnProperty.call(b, key)) return false; if (!deepEqual(a[key], b[key])) return false; } return true; } function diffArrays(a, b, options) { const result = { added: [], removed: [], unchanged: [], changed: [] }; const minLength = Math.min(a.length, b.length); for (let i = 0; i < minLength; i++) { const valA = a[i]; const valB = b[i]; let equal; if (options?.deep) { equal = deepEqual(valA, valB); } else if (isPrimitive(valA) && isPrimitive(valB)) { equal = valA === valB; } else { if (typeof valA === "object" && typeof valB === "object" && valA !== null && valB !== null) { if (JSON.stringify(valA) === JSON.stringify(valB)) { equal = true; } else { equal = false; } } else { equal = valA === valB; } } if (equal) { result.unchanged.push(valA); } else { result.changed.push({ index: i, from: valA, to: valB }); } } if (b.length > a.length) { for (let i = a.length; i < b.length; i++) { result.added.push(b[i]); } } if (a.length > b.length) { for (let i = b.length; i < a.length; i++) { result.removed.push(a[i]); } } return result; } // src/diff.ts function isObject(value) { return typeof value === "object" && value !== null && !Array.isArray(value); } function isArray(value) { return Array.isArray(value); } function diff(objA, objB) { const result = { added: {}, removed: {}, changed: {}, unchanged: {} }; const allKeys = /* @__PURE__ */ new Set([...Object.keys(objA), ...Object.keys(objB)]); for (const key of allKeys) { const valA = objA[key]; const valB = objB[key]; if (!(key in objA)) { result.added[key] = valB; } else if (!(key in objB)) { result.removed[key] = valA; } else if (isObject(valA) && isObject(valB)) { if (Object.keys(valA).length === 0 && Object.keys(valB).length > 0) { const nested = { added: { ...valB }, removed: {}, changed: {}, unchanged: {} }; result.changed[key] = nested; } else { const nested = diff(valA, valB); const hasChanges = Object.keys(nested.added).length > 0 || Object.keys(nested.removed).length > 0 || Object.keys(nested.changed).length > 0; if (hasChanges) { result.changed[key] = nested; } if (Object.keys(nested).length === 0) { result.unchanged[key] = valA; } else { result.changed[key] = nested; } } } else if (isArray(valA) && isArray(valB)) { const arrayResult = diffArrays(valA, valB, { deep: true }); const hasChanges = arrayResult.added.length > 0 || arrayResult.removed.length > 0 || arrayResult.changed.length > 0; if (hasChanges) { result.changed[key] = arrayResult; } else { result.unchanged[key] = valA; } } else if (valA !== valB) { result.changed[key] = { from: valA, to: valB }; } else { result.unchanged[key] = valA; } } if (Object.keys(result.added).length === 0 && Object.keys(result.removed).length === 0 && Object.keys(result.changed).length === 0) { return { added: {}, removed: {}, changed: {}, unchanged: {} }; } return result; } // src/types.ts function isFlatChange(value) { return typeof value === "object" && value !== null && "from" in value && "to" in value && Object.keys(value).length === 2; } function isDiffResult(value) { return typeof value === "object" && value !== null && "added" in value && "removed" in value && "changed" in value && "unchanged" in value; } function isArrayDiffResult(value) { return typeof value === "object" && value !== null && "added" in value && "removed" in value && "changed" in value && "unchanged" in value && Array.isArray(value.added); } // src/flattenedDiff.ts function buildPath(base, key) { const isIndex = typeof key === "number" || /^[0-9]+$/.test(key); if (isIndex) { return `${base}[${key}]`; } return base ? `${base}.${key}` : key; } function diffFlattened(a, b, prefix = "") { const flat = {}; const nested = diff(a, b); if ("added" in nested) { for (const [key, val] of Object.entries(nested.added)) { const path = buildPath(prefix, key); if (typeof val === "object" && val !== null) { flattenAddedObject(void 0, val, path, flat); } else { flat[path] = { from: void 0, to: val }; } } } if ("removed" in nested) { for (const [key, val] of Object.entries(nested.removed)) { const path = buildPath(prefix, key); if (typeof val === "object" && val !== null) { flattenRemovedObject(val, void 0, path, flat); } else { flat[path] = { from: val, to: void 0 }; } } } if ("changed" in nested) { for (const [key, val] of Object.entries(nested.changed)) { const path = buildPath(prefix, key); if (isDiffResult(val)) { if (!Array.isArray(b[key])) { for (const [addedKey, addedVal] of Object.entries(val.added)) { const addedPath = buildPath(path, addedKey); flat[addedPath] = { from: void 0, to: addedVal }; } } if (!Array.isArray(a[key])) { for (const [removedKey, removedVal] of Object.entries(val.removed)) { const removedPath = buildPath(path, removedKey); flat[removedPath] = { from: removedVal, to: void 0 }; } } Object.assign(flat, diffFlattened(a[key] || {}, b[key] || {}, path)); } else if (isArrayDiffResult(val)) { if (Array.isArray(a[key]) && Array.isArray(b[key])) { const aArray = a[key]; const bArray = b[key]; val.changed.forEach((change) => { const aValue = aArray[change.index]; const bValue = bArray[change.index]; const arrPath = `${path}[${change.index}]`; if (typeof aValue === "object" && aValue !== null && typeof bValue === "object" && bValue !== null) { Object.assign(flat, diffFlattened(aValue, bValue, arrPath)); } else { flat[arrPath] = { from: aValue, to: bValue }; } }); for (let i = aArray.length; i < bArray.length; i++) { const addedItem = bArray[i]; const addedPath = `${path}[${i}]`; if (typeof addedItem === "object" && addedItem !== null) { flattenAddedObject(aArray[i], addedItem, addedPath, flat); } else { flat[addedPath] = { from: void 0, to: addedItem }; } } for (let i = bArray.length; i < aArray.length; i++) { const removedItem = aArray[i]; const removedPath = `${path}[${i}]`; if (typeof removedItem === "object" && removedItem !== null) { flattenRemovedObject(removedItem, bArray[i], removedPath, flat); } else { flat[removedPath] = { from: removedItem, to: void 0 }; } } } else { val.changed.forEach((entry) => { const subKey = `${path}[${entry.index}]`; flat[subKey] = { from: entry.from, to: entry.to }; }); } } else { flat[path] = val; } } } return flat; } function flattenAddedObject(fromValue, toValue, path, result) { if (typeof toValue !== "object" || toValue === null) { result[path] = { from: fromValue, to: toValue }; return; } if (Array.isArray(toValue)) { const originalLength = Array.isArray(fromValue) ? fromValue.length : 0; for (let i = originalLength; i < toValue.length; i++) { result[`${path}[${i}]`] = { from: void 0, to: toValue[i] }; } return; } for (const [key, val] of Object.entries(toValue)) { const newPath = buildPath(path, key); let nextFromValue = void 0; if (fromValue && typeof fromValue === "object" && !Array.isArray(fromValue)) { nextFromValue = fromValue[key]; } flattenAddedObject(nextFromValue, val, newPath, result); } } function flattenRemovedObject(fromValue, toValue, path, result) { if (typeof fromValue !== "object" || fromValue === null) { result[path] = { from: fromValue, to: toValue }; return; } if (Array.isArray(fromValue)) { const newLength = Array.isArray(toValue) ? toValue.length : 0; for (let i = newLength; i < fromValue.length; i++) { result[`${path}[${i}]`] = { from: fromValue[i], to: void 0 }; } return; } for (const [key, val] of Object.entries(fromValue)) { const newPath = buildPath(path, key); let nextToValue = void 0; if (toValue && typeof toValue === "object" && !Array.isArray(toValue)) { nextToValue = toValue[key]; } flattenRemovedObject(val, nextToValue, newPath, result); } } // src/smartDiff.ts function smartDiff(a, b, options) { if (options?.flatten) { return diffFlattened(a, b); } return diff(a, b); } export { diff, diffArrays, diffFlattened, isArrayDiffResult, isDiffResult, isFlatChange, smartDiff };