1 | /**
|
2 | @license
|
3 | Copyright (c) 2017 The Polymer Project Authors. All rights reserved.
|
4 | This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
|
5 | The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
|
6 | The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
|
7 | Code distributed by Google as part of the polymer project is also
|
8 | subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
|
9 | */
|
10 | import './boot.js';
|
11 |
|
12 | function newSplice(index, removed, addedCount) {
|
13 | return {
|
14 | index: index,
|
15 | removed: removed,
|
16 | addedCount: addedCount
|
17 | };
|
18 | }
|
19 |
|
20 | const EDIT_LEAVE = 0;
|
21 | const EDIT_UPDATE = 1;
|
22 | const EDIT_ADD = 2;
|
23 | const 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.
|
36 | function 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.
|
71 | function 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 | */
|
162 | function 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 |
|
244 | function 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 |
|
251 | function 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 | */
|
292 | export function calculateSplices(current, previous) {
|
293 | return calcSplices(current, 0, current.length, previous, 0,
|
294 | previous.length);
|
295 | }
|
296 |
|
297 | function equals(currentValue, previousValue) {
|
298 | return currentValue === previousValue;
|
299 | }
|