Superdiff provides a complete and readable diff for both arrays and objects. Plus, it supports stream and file inputs for handling large datasets efficiently, is battle-tested, has zero dependencies, and is super fast.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

166 lines
4.5 KiB

import {
DEFAULT_LIST_DIFF_OPTIONS,
LIST_STATUS,
ListDiff,
ListDiffOptions,
} from "@models/list";
import { isEqual, isObject } from "@lib/utils";
function getLeanDiff(
diff: ListDiff["diff"],
showOnly = [] as ListDiffOptions["showOnly"],
): ListDiff["diff"] {
return diff.filter((value) => showOnly?.includes(value.status));
}
function formatSingleListDiff<T>(
listData: T[],
status: LIST_STATUS,
options: ListDiffOptions = { showOnly: [] },
): ListDiff {
const diff = listData.map((data, i) => ({
value: data,
prevIndex: status === LIST_STATUS.ADDED ? null : i,
newIndex: status === LIST_STATUS.ADDED ? i : null,
indexDiff: null,
status,
}));
if (options.showOnly && options.showOnly.length > 0) {
return {
type: "list",
status,
diff: diff.filter((value) => options.showOnly?.includes(value.status)),
};
}
return {
type: "list",
status,
diff,
};
}
function getListStatus(listDiff: ListDiff["diff"]): LIST_STATUS {
return listDiff.some((value) => value.status !== LIST_STATUS.EQUAL)
? LIST_STATUS.UPDATED
: LIST_STATUS.EQUAL;
}
function isReferencedObject(
value: unknown,
referenceProperty: ListDiffOptions["referenceProperty"],
): value is Record<string, unknown> {
if (isObject(value) && !!referenceProperty) {
return Object.hasOwn(value, referenceProperty);
}
return false;
}
/**
* Returns the diff between two arrays
* @param {Array<T>} prevList - The original array.
* @param {Array<T>} nextList - The new array.
* @param {ListOptions} options - Options to refine your output.
- `showOnly` gives you the option to return only the values whose status you are interested in (e.g. `["added", "equal"]`).
- `referenceProperty` will consider an object to be updated instead of added or deleted if one of its properties remains stable, such as its `id`. This option has no effect on other datatypes.
* @returns ListDiff
*/
export const getListDiff = <T>(
prevList: T[] | undefined | null,
nextList: T[] | undefined | null,
options: ListDiffOptions = DEFAULT_LIST_DIFF_OPTIONS,
): ListDiff => {
if (!prevList && !nextList) {
return {
type: "list",
status: LIST_STATUS.EQUAL,
diff: [],
};
}
if (!prevList) {
return formatSingleListDiff(nextList as T[], LIST_STATUS.ADDED, options);
}
if (!nextList) {
return formatSingleListDiff(prevList as T[], LIST_STATUS.DELETED, options);
}
const diff: ListDiff["diff"] = [];
const prevIndexMatches: number[] = [];
nextList.forEach((nextValue, i) => {
const prevIndex = prevList.findIndex((prevValue, prevIdx) => {
if (isReferencedObject(prevValue, options.referenceProperty)) {
if (isObject(nextValue)) {
return (
isEqual(
prevValue[options.referenceProperty as string],
nextValue[options.referenceProperty as string],
) && !prevIndexMatches.includes(prevIdx)
);
}
return false;
}
return (
isEqual(prevValue, nextValue) && !prevIndexMatches.includes(prevIdx)
);
});
if (prevIndex > -1) {
prevIndexMatches.push(prevIndex);
}
const indexDiff = prevIndex === -1 ? null : i - prevIndex;
if (indexDiff === 0 || options.ignoreArrayOrder) {
let nextStatus = LIST_STATUS.EQUAL;
if (isReferencedObject(nextValue, options.referenceProperty)) {
if (!isEqual(prevList[prevIndex], nextValue)) {
nextStatus = LIST_STATUS.UPDATED;
}
}
return diff.push({
value: nextValue,
prevIndex,
newIndex: i,
indexDiff,
status: nextStatus,
});
}
if (prevIndex === -1) {
return diff.push({
value: nextValue,
prevIndex: null,
newIndex: i,
indexDiff,
status: LIST_STATUS.ADDED,
});
}
return diff.push({
value: nextValue,
prevIndex,
newIndex: i,
indexDiff,
status: options.considerMoveAsUpdate
? LIST_STATUS.UPDATED
: LIST_STATUS.MOVED,
});
});
prevList.forEach((prevValue, i) => {
if (!prevIndexMatches.includes(i)) {
return diff.splice(i, 0, {
value: prevValue,
prevIndex: i,
newIndex: null,
indexDiff: null,
status: LIST_STATUS.DELETED,
});
}
});
if (options.showOnly && options?.showOnly?.length > 0) {
return {
type: "list",
status: getListStatus(diff),
diff: getLeanDiff(diff, options.showOnly),
};
}
return {
type: "list",
status: getListStatus(diff),
diff,
};
};