data-diff-ts
Version:
Lightweight TypeScript utility for comparing objects, arrays, and nested data structures.
312 lines (307 loc) • 9.98 kB
JavaScript
// 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
};