|
1 | | -import { isEqual, isObject } from "@lib/utils"; |
| 1 | +import { isObject } from "@lib/utils"; |
2 | 2 | import { |
3 | 3 | DEFAULT_LIST_DIFF_OPTIONS, |
4 | 4 | ListStatus, |
5 | 5 | ListDiff, |
6 | 6 | ListDiffOptions, |
7 | 7 | } from "@models/list"; |
8 | 8 |
|
| 9 | +function getDiffStatus(statusMap: Set<ListStatus>): ListDiff["status"] { |
| 10 | + if (statusMap.has(ListStatus.UPDATED)) return ListStatus.UPDATED; |
| 11 | + |
| 12 | + const isUniqueStatus = (status: ListStatus) => { |
| 13 | + let isUnique = true; |
| 14 | + for (const value of statusMap) { |
| 15 | + if (value !== status) { |
| 16 | + isUnique = false; |
| 17 | + break; |
| 18 | + } |
| 19 | + } |
| 20 | + return isUnique; |
| 21 | + }; |
| 22 | + |
| 23 | + if (isUniqueStatus(ListStatus.ADDED)) return ListStatus.ADDED; |
| 24 | + if (isUniqueStatus(ListStatus.DELETED)) return ListStatus.DELETED; |
| 25 | + if (isUniqueStatus(ListStatus.EQUAL)) return ListStatus.EQUAL; |
| 26 | + return ListStatus.UPDATED; |
| 27 | +} |
| 28 | + |
9 | 29 | function getLeanDiff( |
10 | 30 | diff: ListDiff["diff"], |
11 | 31 | showOnly = [] as ListDiffOptions["showOnly"], |
@@ -39,17 +59,11 @@ function formatSingleListDiff<T>( |
39 | 59 | }; |
40 | 60 | } |
41 | 61 |
|
42 | | -function getListStatus(listDiff: ListDiff["diff"]): ListStatus { |
43 | | - return listDiff.some((value) => value.status !== ListStatus.EQUAL) |
44 | | - ? ListStatus.UPDATED |
45 | | - : ListStatus.EQUAL; |
46 | | -} |
47 | | - |
48 | 62 | function isReferencedObject( |
49 | 63 | value: unknown, |
50 | 64 | referenceProperty: ListDiffOptions["referenceProperty"], |
51 | 65 | ): value is Record<string, unknown> { |
52 | | - if (isObject(value) && !!referenceProperty) { |
| 66 | + if (!!referenceProperty && isObject(value)) { |
53 | 67 | return Object.hasOwn(value, referenceProperty); |
54 | 68 | } |
55 | 69 | return false; |
@@ -84,84 +98,116 @@ export const getListDiff = <T>( |
84 | 98 | return formatSingleListDiff(prevList as T[], ListStatus.DELETED, options); |
85 | 99 | } |
86 | 100 | const diff: ListDiff["diff"] = []; |
87 | | - const prevIndexMatches = new Set<number>(); |
| 101 | + const previousMap = new Map<string, { indexes: number[]; value: T }>(); |
| 102 | + const statusMap = new Set<ListStatus>(); |
88 | 103 |
|
89 | | - nextList.forEach((nextValue, i) => { |
90 | | - const prevIndex = prevList.findIndex((prevValue, prevIdx) => { |
91 | | - if (prevIndexMatches.has(prevIdx)) { |
92 | | - return false; |
| 104 | + prevList.forEach((prevValue, i) => { |
| 105 | + if (isReferencedObject(prevValue, options.referenceProperty)) { |
| 106 | + const reference = String(prevValue[options.referenceProperty || ""]); |
| 107 | + const match = previousMap.get(reference); |
| 108 | + if (match) { |
| 109 | + const nextIndexes = [...match.indexes, i]; |
| 110 | + previousMap.set(reference, { |
| 111 | + value: match.value, |
| 112 | + indexes: nextIndexes, |
| 113 | + }); |
| 114 | + } else { |
| 115 | + previousMap.set(reference, { value: prevValue, indexes: [i] }); |
93 | 116 | } |
94 | | - if (isReferencedObject(prevValue, options.referenceProperty)) { |
95 | | - if (isObject(nextValue)) { |
96 | | - return isEqual( |
97 | | - prevValue[options.referenceProperty as string], |
98 | | - nextValue[options.referenceProperty as string], |
99 | | - ); |
100 | | - } |
101 | | - return false; |
| 117 | + } else { |
| 118 | + const reference = JSON.stringify(prevValue); |
| 119 | + const match = previousMap.get(reference); |
| 120 | + if (match) { |
| 121 | + const nextIndexes = [...match.indexes, i]; |
| 122 | + previousMap.set(reference, { |
| 123 | + value: match.value, |
| 124 | + indexes: nextIndexes, |
| 125 | + }); |
| 126 | + } else { |
| 127 | + previousMap.set(reference, { value: prevValue, indexes: [i] }); |
102 | 128 | } |
103 | | - return isEqual(prevValue, nextValue); |
104 | | - }); |
105 | | - if (prevIndex > -1) { |
106 | | - prevIndexMatches.add(prevIndex); |
107 | 129 | } |
108 | | - const indexDiff = prevIndex === -1 ? null : i - prevIndex; |
109 | | - if (indexDiff === 0 || options.ignoreArrayOrder) { |
110 | | - let nextStatus = ListStatus.EQUAL; |
111 | | - if (isReferencedObject(nextValue, options.referenceProperty)) { |
112 | | - if (!isEqual(prevList[prevIndex], nextValue)) { |
113 | | - nextStatus = ListStatus.UPDATED; |
114 | | - } |
115 | | - } |
116 | | - return diff.push({ |
| 130 | + }); |
| 131 | + |
| 132 | + const injectNextValueInDiff = ( |
| 133 | + nextValue: T, |
| 134 | + previousMatch: { indexes: number[]; value: T } | undefined | undefined, |
| 135 | + i: number, |
| 136 | + reference: string, |
| 137 | + ) => { |
| 138 | + if (previousMatch) { |
| 139 | + const prevIndex = previousMatch.indexes[0]; |
| 140 | + const indexDiff = i - prevIndex; |
| 141 | + const nextStatus = |
| 142 | + indexDiff === 0 || options.ignoreArrayOrder |
| 143 | + ? ListStatus.EQUAL |
| 144 | + : options.considerMoveAsUpdate |
| 145 | + ? ListStatus.UPDATED |
| 146 | + : ListStatus.MOVED; |
| 147 | + diff.push({ |
117 | 148 | value: nextValue, |
118 | 149 | prevIndex, |
119 | 150 | newIndex: i, |
120 | 151 | indexDiff, |
121 | 152 | status: nextStatus, |
122 | 153 | }); |
123 | | - } |
124 | | - if (prevIndex === -1) { |
125 | | - return diff.push({ |
| 154 | + statusMap.add(nextStatus); |
| 155 | + const nextPrevIndexes = previousMatch.indexes.splice(1); |
| 156 | + if (nextPrevIndexes.length === 0) { |
| 157 | + previousMap.delete(reference); |
| 158 | + } else { |
| 159 | + previousMap.set(reference, { |
| 160 | + value: nextValue, |
| 161 | + indexes: nextPrevIndexes, |
| 162 | + }); |
| 163 | + } |
| 164 | + } else { |
| 165 | + diff.push({ |
126 | 166 | value: nextValue, |
127 | 167 | prevIndex: null, |
128 | 168 | newIndex: i, |
129 | | - indexDiff, |
| 169 | + indexDiff: null, |
130 | 170 | status: ListStatus.ADDED, |
131 | 171 | }); |
| 172 | + statusMap.add(ListStatus.ADDED); |
| 173 | + } |
| 174 | + }; |
| 175 | + |
| 176 | + nextList.forEach((nextValue, i) => { |
| 177 | + if (!!options.referenceProperty && isObject(nextValue)) { |
| 178 | + const reference = String(nextValue[options.referenceProperty || ""]); |
| 179 | + const previousIndexes = previousMap.get(reference); |
| 180 | + return injectNextValueInDiff(nextValue, previousIndexes, i, reference); |
| 181 | + } else { |
| 182 | + const reference = JSON.stringify(nextValue); |
| 183 | + const previousIndexes = previousMap.get(reference); |
| 184 | + return injectNextValueInDiff(nextValue, previousIndexes, i, reference); |
132 | 185 | } |
133 | | - return diff.push({ |
134 | | - value: nextValue, |
135 | | - prevIndex, |
136 | | - newIndex: i, |
137 | | - indexDiff, |
138 | | - status: options.considerMoveAsUpdate |
139 | | - ? ListStatus.UPDATED |
140 | | - : ListStatus.MOVED, |
141 | | - }); |
142 | 186 | }); |
143 | 187 |
|
144 | | - prevList.forEach((prevValue, i) => { |
145 | | - if (!prevIndexMatches.has(i)) { |
146 | | - return diff.push({ |
147 | | - value: prevValue, |
| 188 | + for (const data of previousMap.values()) { |
| 189 | + statusMap.add(ListStatus.DELETED); |
| 190 | + data.indexes.forEach((i) => |
| 191 | + diff.push({ |
| 192 | + value: data.value, |
148 | 193 | prevIndex: i, |
149 | 194 | newIndex: null, |
150 | 195 | indexDiff: null, |
151 | 196 | status: ListStatus.DELETED, |
152 | | - }); |
153 | | - } |
154 | | - }); |
| 197 | + }), |
| 198 | + ); |
| 199 | + } |
| 200 | + |
155 | 201 | if (options.showOnly && options?.showOnly?.length > 0) { |
156 | 202 | return { |
157 | 203 | type: "list", |
158 | | - status: getListStatus(diff), |
| 204 | + status: getDiffStatus(statusMap), |
159 | 205 | diff: getLeanDiff(diff, options.showOnly), |
160 | 206 | }; |
161 | 207 | } |
162 | 208 | return { |
163 | 209 | type: "list", |
164 | | - status: getListStatus(diff), |
| 210 | + status: getDiffStatus(statusMap), |
165 | 211 | diff, |
166 | 212 | }; |
167 | 213 | }; |
0 commit comments