OLD | NEW |
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 library template_binding.src.list_diff; | 5 library observe.src.list_diff; |
6 | 6 |
7 import 'dart:math' as math; | 7 import 'dart:math' as math; |
8 import 'package:observe/observe.dart' show ListChangeRecord; | 8 import 'dart:collection' show UnmodifiableListView; |
9 | 9 |
10 /** | 10 /** |
11 * A summary of an individual change to a [List]. | 11 * A summary of an individual change to a [List]. |
12 * | 12 * |
13 * Each delta represents that at the [index], [removed] sequence of items were | 13 * Each delta represents that at the [index], [removed] sequence of items were |
14 * removed, and counting forward from [index], [addedCount] items were added. | 14 * removed, and counting forward from [index], [addedCount] items were added. |
15 * | |
16 * See also: [summarizeListChanges]. | |
17 */ | 15 */ |
18 class ListChangeDelta implements ListChangeRecord { | 16 class ListChangeRecord { |
| 17 /** The list that changed. */ |
| 18 final List object; |
| 19 |
19 /** The index of the change. */ | 20 /** The index of the change. */ |
20 final int index; | 21 int get index => _index; |
21 | 22 |
| 23 /** The items removed, if any. Otherwise this will be an empty list. */ |
| 24 List get removed => _unmodifiableRemoved; |
| 25 UnmodifiableListView _unmodifiableRemoved; |
| 26 |
| 27 /** |
| 28 * Mutable version of [removed], used during the algorithms as they are |
| 29 * constructing the object. |
| 30 */ |
22 List _removed; | 31 List _removed; |
23 | 32 |
24 // Note: conceptually final, but for convenience we increment it as we build | |
25 // the object. It will be "frozen" by the time it is returned the the user. | |
26 int _addedCount = 0; | |
27 | |
28 ListChangeDelta(this.index, {List removed, int addedCount: 0}) | |
29 : _removed = removed != null ? removed : [], | |
30 _addedCount = addedCount; | |
31 | |
32 // TODO(jmesserly): freeze remove list before handing it out? | |
33 /** The items removed, if any. Otherwise this will be an empty list. */ | |
34 List get removed => _removed; | |
35 | |
36 /** The number of items added. */ | 33 /** The number of items added. */ |
37 int get addedCount => _addedCount; | 34 int get addedCount => _addedCount; |
38 | 35 |
39 int get removedCount => _removed.length; | 36 // Note: conceptually these are final, but for convenience we increment it as |
| 37 // we build the object. It will be "frozen" by the time it is returned the the |
| 38 // user. |
| 39 int _index, _addedCount; |
| 40 |
| 41 ListChangeRecord._(this.object, this._index, removed, this._addedCount) |
| 42 : _removed = removed, |
| 43 _unmodifiableRemoved = new UnmodifiableListView(removed); |
| 44 |
| 45 factory ListChangeRecord(List object, int index, |
| 46 {List removed, int addedCount}) { |
| 47 |
| 48 if (removed == null) removed = []; |
| 49 if (addedCount == null) addedCount = 0; |
| 50 return new ListChangeRecord._(object, index, removed, addedCount); |
| 51 } |
40 | 52 |
41 /** Returns true if the provided index was changed by this operation. */ | 53 /** Returns true if the provided index was changed by this operation. */ |
42 bool indexChanged(key) { | 54 bool indexChanged(key) { |
43 // If key isn't an int, or before the index, then it wasn't changed. | 55 // If key isn't an int, or before the index, then it wasn't changed. |
44 if (key is! int || key < index) return false; | 56 if (key is! int || key < index) return false; |
45 | 57 |
46 // If this was a shift operation, anything after index is changed. | 58 // If this was a shift operation, anything after index is changed. |
47 if (addedCount != removedCount) return true; | 59 if (addedCount != removed.length) return true; |
48 | 60 |
49 // Otherwise, anything in the update range was changed. | 61 // Otherwise, anything in the update range was changed. |
50 return key < index + addedCount; | 62 return key < index + addedCount; |
51 } | 63 } |
52 | 64 |
53 String toString() => '#<$runtimeType index: $index, ' | 65 String toString() => '#<ListChangeRecord index: $index, ' |
54 'removed: $removed, addedCount: $addedCount>'; | 66 'removed: $removed, addedCount: $addedCount>'; |
55 } | 67 } |
56 | 68 |
57 // Note: This function is *based* on the computation of the Levenshtein | 69 // Note: This function is *based* on the computation of the Levenshtein |
58 // "edit" distance. The one change is that "updates" are treated as two | 70 // "edit" distance. The one change is that "updates" are treated as two |
59 // edits - not one. With List splices, an update is really a delete | 71 // edits - not one. With List splices, an update is really a delete |
60 // followed by an add. By retaining this, we optimize for "keeping" the | 72 // followed by an add. By retaining this, we optimize for "keeping" the |
61 // maximum array items in the original array. For example: | 73 // maximum array items in the original array. For example: |
62 // | 74 // |
63 // 'xxxx123' -> '123yyyy' | 75 // 'xxxx123' -> '123yyyy' |
(...skipping 14 matching lines...) Expand all Loading... |
78 distances[i][0] = i; | 90 distances[i][0] = i; |
79 } | 91 } |
80 | 92 |
81 // Initialize null row | 93 // Initialize null row |
82 for (var j = 0; j < columnCount; j++) { | 94 for (var j = 0; j < columnCount; j++) { |
83 distances[0][j] = j; | 95 distances[0][j] = j; |
84 } | 96 } |
85 | 97 |
86 for (var i = 1; i < rowCount; i++) { | 98 for (var i = 1; i < rowCount; i++) { |
87 for (var j = 1; j < columnCount; j++) { | 99 for (var j = 1; j < columnCount; j++) { |
88 if (identical(old[oldStart + i - 1], current[currentStart + j - 1])) { | 100 if (old[oldStart + i - 1] == current[currentStart + j - 1]) { |
89 distances[i][j] = distances[i - 1][j - 1]; | 101 distances[i][j] = distances[i - 1][j - 1]; |
90 } else { | 102 } else { |
91 var north = distances[i - 1][j] + 1; | 103 var north = distances[i - 1][j] + 1; |
92 var west = distances[i][j - 1] + 1; | 104 var west = distances[i][j - 1] + 1; |
93 distances[i][j] = math.min(north, west); | 105 distances[i][j] = math.min(north, west); |
94 } | 106 } |
95 } | 107 } |
96 } | 108 } |
97 | 109 |
98 return distances; | 110 return distances; |
(...skipping 47 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
146 j--; | 158 j--; |
147 current = north; | 159 current = north; |
148 } | 160 } |
149 } | 161 } |
150 | 162 |
151 return edits.reversed.toList(); | 163 return edits.reversed.toList(); |
152 } | 164 } |
153 | 165 |
154 int _sharedPrefix(List arr1, List arr2, int searchLength) { | 166 int _sharedPrefix(List arr1, List arr2, int searchLength) { |
155 for (var i = 0; i < searchLength; i++) { | 167 for (var i = 0; i < searchLength; i++) { |
156 if (!identical(arr1[i], arr2[i])) { | 168 if (arr1[i] != arr2[i]) { |
157 return i; | 169 return i; |
158 } | 170 } |
159 } | 171 } |
160 return searchLength; | 172 return searchLength; |
161 } | 173 } |
162 | 174 |
163 int _sharedSuffix(List arr1, List arr2, int searchLength) { | 175 int _sharedSuffix(List arr1, List arr2, int searchLength) { |
164 var index1 = arr1.length; | 176 var index1 = arr1.length; |
165 var index2 = arr2.length; | 177 var index2 = arr2.length; |
166 var count = 0; | 178 var count = 0; |
167 while (count < searchLength && identical(arr1[--index1], arr2[--index2])) { | 179 while (count < searchLength && arr1[--index1] == arr2[--index2]) { |
168 count++; | 180 count++; |
169 } | 181 } |
170 return count; | 182 return count; |
171 } | 183 } |
172 | 184 |
173 /** | 185 /** |
174 * Lacking individual splice mutation information, the minimal set of | 186 * Lacking individual splice mutation information, the minimal set of |
175 * splices can be synthesized given the previous state and final state of an | 187 * splices can be synthesized given the previous state and final state of an |
176 * array. The basic approach is to calculate the edit distance matrix and | 188 * array. The basic approach is to calculate the edit distance matrix and |
177 * choose the shortest path through it. | 189 * choose the shortest path through it. |
178 * | 190 * |
179 * Complexity: O(l * p) | 191 * Complexity: O(l * p) |
180 * l: The length of the current array | 192 * l: The length of the current array |
181 * p: The length of the old array | 193 * p: The length of the old array |
182 */ | 194 */ |
183 List<ListChangeDelta> calculateSplices(List current, List previous) => | 195 List<ListChangeRecord> calcSplices(List current, int currentStart, |
184 _calcSplices(current, 0, current.length, previous, 0, previous.length); | |
185 | |
186 List<ListChangeDelta> _calcSplices(List current, int currentStart, | |
187 int currentEnd, List old, int oldStart, int oldEnd) { | 196 int currentEnd, List old, int oldStart, int oldEnd) { |
188 | 197 |
189 var prefixCount = 0; | 198 var prefixCount = 0; |
190 var suffixCount = 0; | 199 var suffixCount = 0; |
191 | 200 |
192 var minLength = math.min(currentEnd - currentStart, oldEnd - oldStart); | 201 var minLength = math.min(currentEnd - currentStart, oldEnd - oldStart); |
193 if (currentStart == 0 && oldStart == 0) { | 202 if (currentStart == 0 && oldStart == 0) { |
194 prefixCount = _sharedPrefix(current, old, minLength); | 203 prefixCount = _sharedPrefix(current, old, minLength); |
195 } | 204 } |
196 | 205 |
197 if (currentEnd == current.length && oldEnd == old.length) { | 206 if (currentEnd == current.length && oldEnd == old.length) { |
198 suffixCount = _sharedSuffix(current, old, minLength - prefixCount); | 207 suffixCount = _sharedSuffix(current, old, minLength - prefixCount); |
199 } | 208 } |
200 | 209 |
201 currentStart += prefixCount; | 210 currentStart += prefixCount; |
202 oldStart += prefixCount; | 211 oldStart += prefixCount; |
203 currentEnd -= suffixCount; | 212 currentEnd -= suffixCount; |
204 oldEnd -= suffixCount; | 213 oldEnd -= suffixCount; |
205 | 214 |
206 if (currentEnd - currentStart == 0 && oldEnd - oldStart == 0) { | 215 if (currentEnd - currentStart == 0 && oldEnd - oldStart == 0) { |
207 return const []; | 216 return const []; |
208 } | 217 } |
209 | 218 |
210 if (currentStart == currentEnd) { | 219 if (currentStart == currentEnd) { |
211 var splice = new ListChangeDelta(currentStart); | 220 var splice = new ListChangeRecord(current, currentStart); |
212 while (oldStart < oldEnd) { | 221 while (oldStart < oldEnd) { |
213 splice.removed.add(old[oldStart++]); | 222 splice._removed.add(old[oldStart++]); |
214 } | 223 } |
215 | 224 |
216 return [splice ]; | 225 return [splice ]; |
217 } else if (oldStart == oldEnd) | 226 } else if (oldStart == oldEnd) |
218 return [new ListChangeDelta(currentStart, | 227 return [new ListChangeRecord(current, currentStart, |
219 addedCount: currentEnd - currentStart)]; | 228 addedCount: currentEnd - currentStart)]; |
220 | 229 |
221 var ops = _spliceOperationsFromEditDistances( | 230 var ops = _spliceOperationsFromEditDistances( |
222 _calcEditDistances(current, currentStart, currentEnd, old, oldStart, | 231 _calcEditDistances(current, currentStart, currentEnd, old, oldStart, |
223 oldEnd)); | 232 oldEnd)); |
224 | 233 |
225 ListChangeDelta splice = null; | 234 ListChangeRecord splice = null; |
226 var splices = <ListChangeDelta>[]; | 235 var splices = <ListChangeRecord>[]; |
227 var index = currentStart; | 236 var index = currentStart; |
228 var oldIndex = oldStart; | 237 var oldIndex = oldStart; |
229 for (var i = 0; i < ops.length; i++) { | 238 for (var i = 0; i < ops.length; i++) { |
230 switch(ops[i]) { | 239 switch(ops[i]) { |
231 case _EDIT_LEAVE: | 240 case _EDIT_LEAVE: |
232 if (splice != null) { | 241 if (splice != null) { |
233 splices.add(splice); | 242 splices.add(splice); |
234 splice = null; | 243 splice = null; |
235 } | 244 } |
236 | 245 |
237 index++; | 246 index++; |
238 oldIndex++; | 247 oldIndex++; |
239 break; | 248 break; |
240 case _EDIT_UPDATE: | 249 case _EDIT_UPDATE: |
241 if (splice == null) splice = new ListChangeDelta(index); | 250 if (splice == null) splice = new ListChangeRecord(current, index); |
242 | 251 |
243 splice._addedCount++; | 252 splice._addedCount++; |
244 index++; | 253 index++; |
245 | 254 |
246 splice.removed.add(old[oldIndex]); | 255 splice._removed.add(old[oldIndex]); |
247 oldIndex++; | 256 oldIndex++; |
248 break; | 257 break; |
249 case _EDIT_ADD: | 258 case _EDIT_ADD: |
250 if (splice == null) splice = new ListChangeDelta(index); | 259 if (splice == null) splice = new ListChangeRecord(current, index); |
251 | 260 |
252 splice._addedCount++; | 261 splice._addedCount++; |
253 index++; | 262 index++; |
254 break; | 263 break; |
255 case _EDIT_DELETE: | 264 case _EDIT_DELETE: |
256 if (splice == null) splice = new ListChangeDelta(index); | 265 if (splice == null) splice = new ListChangeRecord(current, index); |
257 | 266 |
258 splice.removed.add(old[oldIndex]); | 267 splice._removed.add(old[oldIndex]); |
259 oldIndex++; | 268 oldIndex++; |
260 break; | 269 break; |
261 } | 270 } |
262 } | 271 } |
263 | 272 |
264 if (splice != null) { | 273 if (splice != null) splices.add(splice); |
265 splices.add(splice); | 274 return splices; |
| 275 } |
| 276 |
| 277 int _intersect(int start1, int end1, int start2, int end2) => |
| 278 math.min(end1, end2) - math.max(start1, start2); |
| 279 |
| 280 void _mergeSplice(List<ListChangeRecord> splices, ListChangeRecord record) { |
| 281 var splice = new ListChangeRecord(record.object, record.index, |
| 282 removed: record._removed.toList(), addedCount: record.addedCount); |
| 283 |
| 284 var inserted = false; |
| 285 var insertionOffset = 0; |
| 286 |
| 287 // I think the way this works is: |
| 288 // - the loop finds where the merge should happen |
| 289 // - it applies the merge in a particular splice |
| 290 // - then continues and updates the subsequent splices with any offset diff. |
| 291 for (var i = 0; i < splices.length; i++) { |
| 292 final current = splices[i]; |
| 293 current._index += insertionOffset; |
| 294 |
| 295 if (inserted) continue; |
| 296 |
| 297 var intersectCount = _intersect( |
| 298 splice.index, splice.index + splice.removed.length, |
| 299 current.index, current.index + current.addedCount); |
| 300 |
| 301 if (intersectCount >= 0) { |
| 302 // Merge the two splices |
| 303 |
| 304 splices.removeAt(i); |
| 305 i--; |
| 306 |
| 307 insertionOffset -= current.addedCount - current.removed.length; |
| 308 |
| 309 splice._addedCount += current.addedCount - intersectCount; |
| 310 var deleteCount = splice.removed.length + |
| 311 current.removed.length - intersectCount; |
| 312 |
| 313 if (splice.addedCount == 0 && deleteCount == 0) { |
| 314 // merged splice is a noop. discard. |
| 315 inserted = true; |
| 316 } else { |
| 317 var removed = current._removed; |
| 318 |
| 319 if (splice.index < current.index) { |
| 320 // some prefix of splice.removed is prepended to current.removed. |
| 321 removed.insertAll(0, |
| 322 splice.removed.getRange(0, current.index - splice.index)); |
| 323 } |
| 324 |
| 325 if (splice.index + splice.removed.length > |
| 326 current.index + current.addedCount) { |
| 327 // some suffix of splice.removed is appended to current.removed. |
| 328 removed.addAll(splice.removed.getRange( |
| 329 current.index + current.addedCount - splice.index, |
| 330 splice.removed.length)); |
| 331 } |
| 332 |
| 333 splice._removed = removed; |
| 334 splice._unmodifiableRemoved = current._unmodifiableRemoved; |
| 335 if (current.index < splice.index) { |
| 336 splice._index = current.index; |
| 337 } |
| 338 } |
| 339 } else if (splice.index < current.index) { |
| 340 // Insert splice here. |
| 341 |
| 342 inserted = true; |
| 343 |
| 344 splices.insert(i, splice); |
| 345 i++; |
| 346 |
| 347 var offset = splice.addedCount - splice.removed.length; |
| 348 current._index += offset; |
| 349 insertionOffset += offset; |
| 350 } |
| 351 } |
| 352 |
| 353 if (!inserted) splices.add(splice); |
| 354 } |
| 355 |
| 356 List<ListChangeRecord> _createInitialSplices(List<Object> list, |
| 357 List<ListChangeRecord> records) { |
| 358 |
| 359 var splices = <ListChangeRecord>[]; |
| 360 for (var record in records) { |
| 361 _mergeSplice(splices, record); |
266 } | 362 } |
267 return splices; | 363 return splices; |
268 } | 364 } |
| 365 |
| 366 /** |
| 367 * We need to summarize change records. Consumers of these records want to |
| 368 * apply the batch sequentially, and ensure that they can find inserted |
| 369 * items by looking at that position in the list. This property does not |
| 370 * hold in our record-as-you-go records. Consider: |
| 371 * |
| 372 * var model = toObservable(['a', 'b']); |
| 373 * model.removeAt(1); |
| 374 * model.insertAll(0, ['c', 'd', 'e']); |
| 375 * model.removeRange(1, 3); |
| 376 * model.insert(1, 'f'); |
| 377 * |
| 378 * Here, we inserted some records and then removed some of them. |
| 379 * If someone processed these records naively, they would "play back" the |
| 380 * insert incorrectly, because those items will be shifted. |
| 381 */ |
| 382 List<ListChangeRecord> projectListSplices(List list, |
| 383 List<ListChangeRecord> records) { |
| 384 if (records.length == 1) return records; |
| 385 |
| 386 var splices = []; |
| 387 for (var splice in _createInitialSplices(list, records)) { |
| 388 if (splice.addedCount == 1 && splice.removed.length == 1) { |
| 389 if (splice.removed[0] != list[splice.index]) splices.add(splice); |
| 390 continue; |
| 391 } |
| 392 |
| 393 splices.addAll(calcSplices(list, splice.index, |
| 394 splice.index + splice.addedCount, splice._removed, 0, |
| 395 splice.removed.length)); |
| 396 } |
| 397 |
| 398 return splices; |
| 399 } |
OLD | NEW |