Skip to content

Commit 7c5ecf0

Browse files
author
antoine lanoe
committed
feat: base
0 parents  commit 7c5ecf0

13 files changed

+3234
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/node_modules

index.ts

Whitespace-only changes.

jest.config.js

Whitespace-only changes.

package.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"name": "data-diff",
3+
"version": "1.0.0",
4+
"main": "index.js",
5+
"license": "MIT",
6+
"devDependencies": {
7+
"@babel/preset-env": "^7.20.2",
8+
"@types/jest": "^29.2.4",
9+
"jest": "^29.3.1",
10+
"mkdirp": "^1.0.4",
11+
"rimraf": "^3.0.2",
12+
"ts-jest": "^29.0.3",
13+
"typescript": "^4.9.4"
14+
}
15+
}

src/index.ts

Whitespace-only changes.

src/list-diff.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { STATUS, ListDiff, ListData } from "./model";
2+
import { isEqual } from "./utils";
3+
4+
function formatSingleListDiff(
5+
listData: ListData,
6+
status: "added" | "removed"
7+
): ListDiff {
8+
return {
9+
type: "list",
10+
diff: listData.map((data) => ({ value: data, status })),
11+
};
12+
}
13+
14+
export const getListDiff = (
15+
prevList: ListData[] | undefined | null,
16+
nextList: ListData[] | undefined | null
17+
): ListDiff => {
18+
if (!prevList && !nextList) {
19+
return {
20+
type: "list",
21+
diff: [],
22+
};
23+
}
24+
if (!prevList) {
25+
return formatSingleListDiff(nextList, "added");
26+
}
27+
if (!nextList) {
28+
return formatSingleListDiff(prevList, "removed");
29+
}
30+
const diff: ListDiff["diff"] = [];
31+
nextList.forEach((nextValue, i) => {
32+
const prevIndex = prevList.findIndex((prevValue) =>
33+
isEqual(prevValue, nextValue)
34+
);
35+
const indexDiff = prevIndex === -1 ? null : i - prevIndex;
36+
if (indexDiff === 0) {
37+
return diff.push({
38+
value: nextValue,
39+
prevIndex,
40+
newIndex: i,
41+
indexDiff,
42+
status: STATUS.EQUAL,
43+
});
44+
}
45+
if (prevIndex === -1) {
46+
return diff.push({
47+
value: nextValue,
48+
prevIndex: null,
49+
newIndex: i,
50+
indexDiff,
51+
status: STATUS.ADDED,
52+
});
53+
}
54+
return diff.push({
55+
value: nextValue,
56+
prevIndex,
57+
newIndex: i,
58+
indexDiff,
59+
status: STATUS.MOVED,
60+
});
61+
});
62+
63+
prevList.forEach((prevValue, i) => {
64+
if (!nextList.some((nextValue) => isEqual(nextValue, prevValue))) {
65+
return diff.splice(i, 0, {
66+
value: prevValue,
67+
prevIndex: i,
68+
newIndex: null,
69+
indexDiff: null,
70+
status: STATUS.DELETED,
71+
});
72+
}
73+
});
74+
return {
75+
type: "list",
76+
diff,
77+
};
78+
};
79+
80+
export function hasListChanged(listDiff: ListDiff): boolean {
81+
return listDiff.diff.some((d) => d.status !== STATUS.EQUAL);
82+
}

src/model.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
export const STATUS: Record<string, DiffStatus> = {
2+
ADDED: "added",
3+
EQUAL: "equal",
4+
MOVED: "moved",
5+
DELETED: "deleted",
6+
UPDATED: "updated",
7+
};
8+
9+
export type DiffStatus = "added" | "equal" | "moved" | "deleted" | "updated";
10+
export type ObjectData = Record<string, any> | undefined | null;
11+
export type ListData = any;
12+
13+
export type ListDiff = {
14+
type: "list";
15+
diff: {
16+
value: ListData;
17+
prevIndex: number | null;
18+
newIndex: number | null;
19+
indexDiff: number | null;
20+
status: DiffStatus;
21+
}[];
22+
};
23+
24+
export type Subproperties = {
25+
name: string;
26+
previousValue: any;
27+
currentValue: any;
28+
status: DiffStatus;
29+
};
30+
31+
export type ObjectDiff = {
32+
type: "object";
33+
diff: {
34+
property: string;
35+
previousValue: any;
36+
currentValue: any;
37+
status: DiffStatus;
38+
subPropertiesDiff?: Subproperties[];
39+
}[];
40+
};
41+
42+
export type DataDiff = ListDiff | ObjectDiff;

src/object-diff.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import {
2+
ObjectData,
3+
ObjectDiff,
4+
DiffStatus,
5+
STATUS,
6+
Subproperties,
7+
} from "./model";
8+
import { hasNestedValues, isEqual } from "./utils";
9+
10+
function formatSingleObjectDiff(
11+
data: ObjectData,
12+
status: DiffStatus
13+
): ObjectDiff {
14+
const diff: ObjectDiff["diff"] = [];
15+
Object.entries(data).forEach(([property, value]) => {
16+
if (hasNestedValues(value)) {
17+
const subPropertiesDiff: Subproperties[] = [];
18+
Object.entries(value).forEach(([subProperty, subValue]) => {
19+
subPropertiesDiff.push({
20+
name: subProperty,
21+
previousValue: status === STATUS.ADDED ? undefined : subValue,
22+
currentValue: status === STATUS.ADDED ? subValue : undefined,
23+
status,
24+
});
25+
});
26+
return diff.push({
27+
property: property,
28+
previousValue: status === STATUS.ADDED ? undefined : data[property],
29+
currentValue: status === STATUS.ADDED ? value : undefined,
30+
status,
31+
subPropertiesDiff,
32+
});
33+
}
34+
return diff.push({
35+
property,
36+
previousValue: status === STATUS.ADDED ? undefined : data[property],
37+
currentValue: status === STATUS.ADDED ? value : undefined,
38+
status,
39+
});
40+
});
41+
return {
42+
type: "object",
43+
diff,
44+
};
45+
}
46+
47+
export function getObjectDiff(
48+
prevData: ObjectData,
49+
nextData: ObjectData
50+
): ObjectDiff {
51+
if (!prevData && !nextData) {
52+
return {
53+
type: "object",
54+
diff: [],
55+
};
56+
}
57+
if (!prevData) {
58+
return formatSingleObjectDiff(nextData, STATUS.ADDED);
59+
}
60+
if (!nextData) {
61+
return formatSingleObjectDiff(prevData, STATUS.DELETED);
62+
}
63+
const diff: ObjectDiff["diff"] = [];
64+
Object.entries(nextData).forEach(([nextProperty, nextValue]) => {
65+
const previousValue = prevData[nextProperty];
66+
67+
if (hasNestedValues(nextValue)) {
68+
const prevSubValues = previousValue
69+
? Object.entries(previousValue)
70+
: null;
71+
const subPropertiesDiff: Subproperties[] = [];
72+
Object.entries(nextValue).forEach(([nextSubProperty, nextSubValue]) => {
73+
if (prevSubValues) {
74+
const previousMatch = prevSubValues.find(([subPreviousKey]) =>
75+
isEqual(subPreviousKey, nextSubProperty)
76+
);
77+
if (previousMatch) {
78+
subPropertiesDiff.push({
79+
name: nextSubProperty,
80+
previousValue: previousMatch[1],
81+
currentValue: nextSubValue,
82+
status: isEqual(previousMatch[1], nextSubValue)
83+
? STATUS.EQUAL
84+
: STATUS.UPDATED,
85+
});
86+
}
87+
}
88+
});
89+
const _status = subPropertiesDiff.some(
90+
(property) => property.status !== STATUS.EQUAL
91+
)
92+
? STATUS.UPDATED
93+
: STATUS.EQUAL;
94+
return diff.push({
95+
property: nextProperty,
96+
previousValue,
97+
currentValue: nextValue,
98+
status: _status,
99+
subPropertiesDiff,
100+
});
101+
}
102+
return diff.push({
103+
property: nextProperty,
104+
previousValue,
105+
currentValue: nextValue,
106+
status: previousValue === nextValue ? STATUS.EQUAL : STATUS.UPDATED,
107+
});
108+
});
109+
return {
110+
type: "object",
111+
diff,
112+
};
113+
}

src/utils.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export function isEqual(a: any, b: any): boolean {
2+
if (typeof a !== typeof b) return true;
3+
if (Array.isArray(a)) {
4+
return a.toString() === b.toString();
5+
}
6+
if (typeof a === "object") {
7+
return JSON.stringify(a) === JSON.stringify(b);
8+
}
9+
return a === b;
10+
}
11+
12+
export function hasNestedValues(value: any): value is Record<string, any> {
13+
return typeof value === "object" && !Array.isArray(value);
14+
}

test/list-diff.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { getListDiff } from "../src/list-diff";
2+
3+
describe("getListDiff", () => {
4+
it("", () => {
5+
expect(getListDiff(null, null)).toStrictEqual(null);
6+
});
7+
});

test/object-diff.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { getObjectDiff } from "../src/object-diff";
2+
3+
describe("getObjectDiff", () => {
4+
it("", () => {
5+
expect(getObjectDiff(null, null)).toStrictEqual(null);
6+
});
7+
});

test/utils.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { isEqual, hasNestedValues } from "../src/utils";
2+
3+
describe("isEqual", () => {
4+
it("", () => {
5+
expect(isEqual(null, null)).toStrictEqual(null);
6+
});
7+
it("", () => {
8+
expect(hasNestedValues(null)).toStrictEqual(null);
9+
});
10+
});

0 commit comments

Comments
 (0)