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