UNPKG

9.24 kBJavaScriptView Raw
1/**
2@license
3Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
4This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
5The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
6The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
7Code distributed by Google as part of the polymer project is also
8subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
9*/
10import './boot.js';
11
12function newSplice(index, removed, addedCount) {
13 return {
14 index: index,
15 removed: removed,
16 addedCount: addedCount
17 };
18}
19
20const EDIT_LEAVE = 0;
21const EDIT_UPDATE = 1;
22const EDIT_ADD = 2;
23const EDIT_DELETE = 3;
24
25// Note: This function is *based* on the computation of the Levenshtein
26// "edit" distance. The one change is that "updates" are treated as two
27// edits - not one. With Array splices, an update is really a delete
28// followed by an add. By retaining this, we optimize for "keeping" the
29// maximum array items in the original array. For example:
30//
31// 'xxxx123' -> '123yyyy'
32//
33// With 1-edit updates, the shortest path would be just to update all seven
34// characters. With 2-edit updates, we delete 4, leave 3, and add 4. This
35// leaves the substring '123' intact.
36function calcEditDistances(current, currentStart, currentEnd,
37 old, oldStart, oldEnd) {
38 // "Deletion" columns
39 let rowCount = oldEnd - oldStart + 1;
40 let columnCount = currentEnd - currentStart + 1;
41 let distances = new Array(rowCount);
42
43 // "Addition" rows. Initialize null column.
44 for (let i = 0; i < rowCount; i++) {
45 distances[i] = new Array(columnCount);
46 distances[i][0] = i;
47 }
48
49 // Initialize null row
50 for (let j = 0; j < columnCount; j++)
51 distances[0][j] = j;
52
53 for (let i = 1; i < rowCount; i++) {
54 for (let j = 1; j < columnCount; j++) {
55 if (equals(current[currentStart + j - 1], old[oldStart + i - 1]))
56 distances[i][j] = distances[i - 1][j - 1];
57 else {
58 let north = distances[i - 1][j] + 1;
59 let west = distances[i][j - 1] + 1;
60 distances[i][j] = north < west ? north : west;
61 }
62 }
63 }
64
65 return distances;
66}
67
68// This starts at the final weight, and walks "backward" by finding
69// the minimum previous weight recursively until the origin of the weight
70// matrix.
71function spliceOperationsFromEditDistances(distances) {
72 let i = distances.length - 1;
73 let j = distances[0].length - 1;
74 let current = distances[i][j];
75 let edits = [];
76 while (i > 0 || j > 0) {
77 if (i == 0) {
78 edits.push(EDIT_ADD);
79 j--;
80 continue;
81 }
82 if (j == 0) {
83 edits.push(EDIT_DELETE);
84 i--;
85 continue;
86 }
87 let northWest = distances[i - 1][j - 1];
88 let west = distances[i - 1][j];
89 let north = distances[i][j - 1];
90
91 let min;
92 if (west < north)
93 min = west < northWest ? west : northWest;
94 else
95 min = north < northWest ? north : northWest;
96
97 if (min == northWest) {
98 if (northWest == current) {
99 edits.push(EDIT_LEAVE);
100 } else {
101 edits.push(EDIT_UPDATE);
102 current = northWest;
103 }
104 i--;
105 j--;
106 } else if (min == west) {
107 edits.push(EDIT_DELETE);
108 i--;
109 current = west;
110 } else {
111 edits.push(EDIT_ADD);
112 j--;
113 current = north;
114 }
115 }
116
117 edits.reverse();
118 return edits;
119}
120
121/**
122 * Splice Projection functions:
123 *
124 * A splice map is a representation of how a previous array of items
125 * was transformed into a new array of items. Conceptually it is a list of
126 * tuples of
127 *
128 * <index, removed, addedCount>
129 *
130 * which are kept in ascending index order of. The tuple represents that at
131 * the |index|, |removed| sequence of items were removed, and counting forward
132 * from |index|, |addedCount| items were added.
133 */
134
135/**
136 * Lacking individual splice mutation information, the minimal set of
137 * splices can be synthesized given the previous state and final state of an
138 * array. The basic approach is to calculate the edit distance matrix and
139 * choose the shortest path through it.
140 *
141 * Complexity: O(l * p)
142 * l: The length of the current array
143 * p: The length of the old array
144 *
145 * @param {!Array} current The current "changed" array for which to
146 * calculate splices.
147 * @param {number} currentStart Starting index in the `current` array for
148 * which splices are calculated.
149 * @param {number} currentEnd Ending index in the `current` array for
150 * which splices are calculated.
151 * @param {!Array} old The original "unchanged" array to compare `current`
152 * against to determine splices.
153 * @param {number} oldStart Starting index in the `old` array for
154 * which splices are calculated.
155 * @param {number} oldEnd Ending index in the `old` array for
156 * which splices are calculated.
157 * @return {!Array} Returns an array of splice record objects. Each of these
158 * contains: `index` the location where the splice occurred; `removed`
159 * the array of removed items from this location; `addedCount` the number
160 * of items added at this location.
161 */
162function calcSplices(current, currentStart, currentEnd,
163 old, oldStart, oldEnd) {
164 let prefixCount = 0;
165 let suffixCount = 0;
166 let splice;
167
168 let minLength = Math.min(currentEnd - currentStart, oldEnd - oldStart);
169 if (currentStart == 0 && oldStart == 0)
170 prefixCount = sharedPrefix(current, old, minLength);
171
172 if (currentEnd == current.length && oldEnd == old.length)
173 suffixCount = sharedSuffix(current, old, minLength - prefixCount);
174
175 currentStart += prefixCount;
176 oldStart += prefixCount;
177 currentEnd -= suffixCount;
178 oldEnd -= suffixCount;
179
180 if (currentEnd - currentStart == 0 && oldEnd - oldStart == 0)
181 return [];
182
183 if (currentStart == currentEnd) {
184 splice = newSplice(currentStart, [], 0);
185 while (oldStart < oldEnd)
186 splice.removed.push(old[oldStart++]);
187
188 return [ splice ];
189 } else if (oldStart == oldEnd)
190 return [ newSplice(currentStart, [], currentEnd - currentStart) ];
191
192 let ops = spliceOperationsFromEditDistances(
193 calcEditDistances(current, currentStart, currentEnd,
194 old, oldStart, oldEnd));
195
196 splice = undefined;
197 let splices = [];
198 let index = currentStart;
199 let oldIndex = oldStart;
200 for (let i = 0; i < ops.length; i++) {
201 switch(ops[i]) {
202 case EDIT_LEAVE:
203 if (splice) {
204 splices.push(splice);
205 splice = undefined;
206 }
207
208 index++;
209 oldIndex++;
210 break;
211 case EDIT_UPDATE:
212 if (!splice)
213 splice = newSplice(index, [], 0);
214
215 splice.addedCount++;
216 index++;
217
218 splice.removed.push(old[oldIndex]);
219 oldIndex++;
220 break;
221 case EDIT_ADD:
222 if (!splice)
223 splice = newSplice(index, [], 0);
224
225 splice.addedCount++;
226 index++;
227 break;
228 case EDIT_DELETE:
229 if (!splice)
230 splice = newSplice(index, [], 0);
231
232 splice.removed.push(old[oldIndex]);
233 oldIndex++;
234 break;
235 }
236 }
237
238 if (splice) {
239 splices.push(splice);
240 }
241 return splices;
242}
243
244function sharedPrefix(current, old, searchLength) {
245 for (let i = 0; i < searchLength; i++)
246 if (!equals(current[i], old[i]))
247 return i;
248 return searchLength;
249}
250
251function sharedSuffix(current, old, searchLength) {
252 let index1 = current.length;
253 let index2 = old.length;
254 let count = 0;
255 while (count < searchLength && equals(current[--index1], old[--index2]))
256 count++;
257
258 return count;
259}
260
261/**
262 * Returns an array of splice records indicating the minimum edits required
263 * to transform the `previous` array into the `current` array.
264 *
265 * Splice records are ordered by index and contain the following fields:
266 * - `index`: index where edit started
267 * - `removed`: array of removed items from this index
268 * - `addedCount`: number of items added at this index
269 *
270 * This function is based on the Levenshtein "minimum edit distance"
271 * algorithm. Note that updates are treated as removal followed by addition.
272 *
273 * The worst-case time complexity of this algorithm is `O(l * p)`
274 * l: The length of the current array
275 * p: The length of the previous array
276 *
277 * However, the worst-case complexity is reduced by an `O(n)` optimization
278 * to detect any shared prefix & suffix between the two arrays and only
279 * perform the more expensive minimum edit distance calculation over the
280 * non-shared portions of the arrays.
281 *
282 * @function
283 * @param {!Array} current The "changed" array for which splices will be
284 * calculated.
285 * @param {!Array} previous The "unchanged" original array to compare
286 * `current` against to determine the splices.
287 * @return {!Array} Returns an array of splice record objects. Each of these
288 * contains: `index` the location where the splice occurred; `removed`
289 * the array of removed items from this location; `addedCount` the number
290 * of items added at this location.
291 */
292export function calculateSplices(current, previous) {
293 return calcSplices(current, 0, current.length, previous, 0,
294 previous.length);
295}
296
297function equals(currentValue, previousValue) {
298 return currentValue === previousValue;
299}