Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(460)

Side by Side Diff: pkg/observe/lib/src/list_diff.dart

Issue 53743002: introduce ObservableList.listChanges to keep list changes separate from property changes (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Created 7 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « pkg/observe/lib/src/change_record.dart ('k') | pkg/observe/lib/src/list_path_observer.dart » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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
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 }
OLDNEW
« no previous file with comments | « pkg/observe/lib/src/change_record.dart ('k') | pkg/observe/lib/src/list_path_observer.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698