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

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
OLDNEW
(Empty)
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
3 // BSD-style license that can be found in the LICENSE file.
4
5 library observe.src.list_diff;
Jennifer Messerly 2013/10/30 23:45:26 hmmm; git totally failed to recognize the move. I'
6
7 import 'dart:math' as math;
8 import 'dart:collection' show UnmodifiableListView;
9
10 /**
11 * A summary of an individual change to a [List].
12 *
13 * Each delta represents that at the [index], [removed] sequence of items were
14 * removed, and counting forward from [index], [addedCount] items were added.
15 */
16 class ListChangeRecord {
Jennifer Messerly 2013/10/30 23:45:26 some changes here to this class: * merged ListCha
17 /** The list that changed. */
18 final List object;
19
20 /** The index of the change. */
21 int get index => _index;
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 */
31 List _removed;
32
33 /** The number of items added. */
34 int get addedCount => _addedCount;
35
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 }
52
53 /** Returns true if the provided index was changed by this operation. */
54 bool indexChanged(key) {
55 // If key isn't an int, or before the index, then it wasn't changed.
56 if (key is! int || key < index) return false;
57
58 // If this was a shift operation, anything after index is changed.
59 if (addedCount != removed.length) return true;
60
61 // Otherwise, anything in the update range was changed.
62 return key < index + addedCount;
63 }
64
65 String toString() => '#<ListChangeRecord index: $index, '
66 'removed: $removed, addedCount: $addedCount>';
67 }
68
69 // Note: This function is *based* on the computation of the Levenshtein
70 // "edit" distance. The one change is that "updates" are treated as two
71 // edits - not one. With List splices, an update is really a delete
72 // followed by an add. By retaining this, we optimize for "keeping" the
73 // maximum array items in the original array. For example:
74 //
75 // 'xxxx123' -> '123yyyy'
76 //
77 // With 1-edit updates, the shortest path would be just to update all seven
78 // characters. With 2-edit updates, we delete 4, leave 3, and add 4. This
79 // leaves the substring '123' intact.
80 List<List<int>> _calcEditDistances(List current, int currentStart,
Jennifer Messerly 2013/10/30 23:45:26 unchanged
81 int currentEnd, List old, int oldStart, int oldEnd) {
82 // "Deletion" columns
83 var rowCount = oldEnd - oldStart + 1;
84 var columnCount = currentEnd - currentStart + 1;
85 var distances = new List(rowCount);
86
87 // "Addition" rows. Initialize null column.
88 for (var i = 0; i < rowCount; i++) {
89 distances[i] = new List(columnCount);
90 distances[i][0] = i;
91 }
92
93 // Initialize null row
94 for (var j = 0; j < columnCount; j++) {
95 distances[0][j] = j;
96 }
97
98 for (var i = 1; i < rowCount; i++) {
99 for (var j = 1; j < columnCount; j++) {
100 if (old[oldStart + i - 1] == current[currentStart + j - 1]) {
101 distances[i][j] = distances[i - 1][j - 1];
102 } else {
103 var north = distances[i - 1][j] + 1;
104 var west = distances[i][j - 1] + 1;
105 distances[i][j] = math.min(north, west);
106 }
107 }
108 }
109
110 return distances;
111 }
112
113 const _EDIT_LEAVE = 0;
114 const _EDIT_UPDATE = 1;
115 const _EDIT_ADD = 2;
116 const _EDIT_DELETE = 3;
117
118 // This starts at the final weight, and walks "backward" by finding
119 // the minimum previous weight recursively until the origin of the weight
120 // matrix.
121 List<int> _spliceOperationsFromEditDistances(List<List<int>> distances) {
Jennifer Messerly 2013/10/30 23:45:26 unchanged
122 var i = distances.length - 1;
123 var j = distances[0].length - 1;
124 var current = distances[i][j];
125 var edits = [];
126 while (i > 0 || j > 0) {
127 if (i == 0) {
128 edits.add(_EDIT_ADD);
129 j--;
130 continue;
131 }
132 if (j == 0) {
133 edits.add(_EDIT_DELETE);
134 i--;
135 continue;
136 }
137 var northWest = distances[i - 1][j - 1];
138 var west = distances[i - 1][j];
139 var north = distances[i][j - 1];
140
141 var min = math.min(math.min(west, north), northWest);
142
143 if (min == northWest) {
144 if (northWest == current) {
145 edits.add(_EDIT_LEAVE);
146 } else {
147 edits.add(_EDIT_UPDATE);
148 current = northWest;
149 }
150 i--;
151 j--;
152 } else if (min == west) {
153 edits.add(_EDIT_DELETE);
154 i--;
155 current = west;
156 } else {
157 edits.add(_EDIT_ADD);
158 j--;
159 current = north;
160 }
161 }
162
163 return edits.reversed.toList();
164 }
165
166 int _sharedPrefix(List arr1, List arr2, int searchLength) {
167 for (var i = 0; i < searchLength; i++) {
168 if (arr1[i] != arr2[i]) {
169 return i;
170 }
171 }
172 return searchLength;
173 }
174
175 int _sharedSuffix(List arr1, List arr2, int searchLength) {
176 var index1 = arr1.length;
177 var index2 = arr2.length;
178 var count = 0;
179 while (count < searchLength && arr1[--index1] == arr2[--index2]) {
180 count++;
181 }
182 return count;
183 }
184
185 /**
186 * Lacking individual splice mutation information, the minimal set of
187 * splices can be synthesized given the previous state and final state of an
188 * array. The basic approach is to calculate the edit distance matrix and
189 * choose the shortest path through it.
190 *
191 * Complexity: O(l * p)
192 * l: The length of the current array
193 * p: The length of the old array
194 */
195 List<ListChangeRecord> calcSplices(List current, int currentStart,
Jennifer Messerly 2013/10/30 23:45:26 unchanged
196 int currentEnd, List old, int oldStart, int oldEnd) {
197
198 var prefixCount = 0;
199 var suffixCount = 0;
200
201 var minLength = math.min(currentEnd - currentStart, oldEnd - oldStart);
202 if (currentStart == 0 && oldStart == 0) {
203 prefixCount = _sharedPrefix(current, old, minLength);
204 }
205
206 if (currentEnd == current.length && oldEnd == old.length) {
207 suffixCount = _sharedSuffix(current, old, minLength - prefixCount);
208 }
209
210 currentStart += prefixCount;
211 oldStart += prefixCount;
212 currentEnd -= suffixCount;
213 oldEnd -= suffixCount;
214
215 if (currentEnd - currentStart == 0 && oldEnd - oldStart == 0) {
216 return const [];
217 }
218
219 if (currentStart == currentEnd) {
220 var splice = new ListChangeRecord(current, currentStart);
221 while (oldStart < oldEnd) {
222 splice._removed.add(old[oldStart++]);
223 }
224
225 return [splice ];
226 } else if (oldStart == oldEnd)
227 return [new ListChangeRecord(current, currentStart,
228 addedCount: currentEnd - currentStart)];
229
230 var ops = _spliceOperationsFromEditDistances(
231 _calcEditDistances(current, currentStart, currentEnd, old, oldStart,
232 oldEnd));
233
234 ListChangeRecord splice = null;
235 var splices = <ListChangeRecord>[];
236 var index = currentStart;
237 var oldIndex = oldStart;
238 for (var i = 0; i < ops.length; i++) {
239 switch(ops[i]) {
240 case _EDIT_LEAVE:
241 if (splice != null) {
242 splices.add(splice);
243 splice = null;
244 }
245
246 index++;
247 oldIndex++;
248 break;
249 case _EDIT_UPDATE:
250 if (splice == null) splice = new ListChangeRecord(current, index);
251
252 splice._addedCount++;
253 index++;
254
255 splice._removed.add(old[oldIndex]);
256 oldIndex++;
257 break;
258 case _EDIT_ADD:
259 if (splice == null) splice = new ListChangeRecord(current, index);
260
261 splice._addedCount++;
262 index++;
263 break;
264 case _EDIT_DELETE:
265 if (splice == null) splice = new ListChangeRecord(current, index);
266
267 splice._removed.add(old[oldIndex]);
268 oldIndex++;
269 break;
270 }
271 }
272
273 if (splice != null) splices.add(splice);
274 return splices;
275 }
276
277 int _intersect(int start1, int end1, int start2, int end2) {
Jennifer Messerly 2013/10/30 23:45:26 newly ported from observe-js
278 // Disjoint
279 if (end1 < start2 || end2 < start1) return -1;
280
281 // Adjacent
282 if (end1 == start2 || end2 == start1) return 0;
283
284 // Non-zero intersect, span1 first
285 if (start1 < start2) {
286 if (end1 < end2) return end1 - start2; // Overlap
287 return end2 - start2; // Contained
288 } else {
289 // Non-zero intersect, span2 first
290 if (end2 < end1) return end2 - start1; // Overlap
291 return end1 - start1; // Contained
292 }
Siggi Cherem (dart-lang) 2013/10/31 00:38:45 maybe not worth it, but this whole function could
Jennifer Messerly 2013/10/31 02:19:34 nice. done. hmm. maybe I should send them a pull r
293 }
294
295 void _mergeSplice(List<ListChangeRecord> splices, ListChangeRecord record) {
Jennifer Messerly 2013/10/30 23:45:26 newly ported from observe-js
296 var splice = new ListChangeRecord(record.object, record.index,
297 removed: record._removed.toList(), addedCount: record.addedCount);
298
299 var inserted = false;
300 var insertionOffset = 0;
301
302 for (var i = 0; i < splices.length; i++) {
303 final current = splices[i];
304 current._index += insertionOffset;
305
306 if (inserted) continue;
Siggi Cherem (dart-lang) 2013/10/31 00:38:45 It took me a while to get why this code works. Thi
Jennifer Messerly 2013/10/31 02:19:34 hey, not my code ;-)
307
308 var intersectCount = _intersect(
309 splice.index, splice.index + splice.removed.length,
310 current.index, current.index + current.addedCount);
311
312 if (intersectCount >= 0) {
313 // Merge the two splices
314
315 splices.removeAt(i);
316 i--;
317
318 insertionOffset -= current.addedCount - current.removed.length;
319
320 splice._addedCount += current.addedCount - intersectCount;
321 var deleteCount = splice.removed.length +
322 current.removed.length - intersectCount;
323
324 if (splice.addedCount == 0 && deleteCount == 0) {
325 // merged splice is a noop. discard.
326 inserted = true;
327 } else {
328 var removed = current._removed;
329
330 if (splice.index < current.index) {
331 // some prefix of splice.removed is prepended to current.removed.
332 removed.insertAll(0,
333 splice.removed.getRange(0, current.index - splice.index));
334 }
335
336 if (splice.index + splice.removed.length >
337 current.index + current.addedCount) {
338 // some suffix of splice.removed is appended to current.removed.
339 removed.addAll(splice.removed.getRange(
340 current.index + current.addedCount - splice.index,
341 splice.removed.length));
342 }
343
344 splice._removed = removed;
345 splice._unmodifiableRemoved = current._unmodifiableRemoved;
346 if (current.index < splice.index) {
347 splice._index = current.index;
348 }
349 }
350 } else if (splice.index < current.index) {
351 // Insert splice here.
352
353 inserted = true;
354
355 splices.insert(i, splice);
356 i++;
357
358 var offset = splice.addedCount - splice.removed.length;
359 current._index += offset;
360 insertionOffset += offset;
361 }
362 }
363
364 if (!inserted) splices.add(splice);
365 }
366
367 List<ListChangeRecord> _createInitialSplices(List<Object> list,
Jennifer Messerly 2013/10/30 23:45:26 newly ported from observe-js
368 List<ListChangeRecord> records) {
369
370 var splices = <ListChangeRecord>[];
371 for (var record in records) {
372 _mergeSplice(splices, record);
373 }
374 return splices;
375 }
376
377 /**
378 * We need to summarize change records. Consumers of these records want to
379 * apply the batch sequentially, and ensure that they can find inserted
380 * items by looking at that position in the list. This property does not
381 * hold in our record-as-you-go records. Consider:
382 *
383 * var model = toObservable(['a', 'b']);
384 * model.removeAt(1);
385 * model.insertAll(0, ['c', 'd', 'e']);
386 * model.removeRange(1, 3);
387 * model.insert(1, 'f');
388 *
389 * Here, we inserted some records and then removed some of them.
390 * If someone processed these records naively, they would "play back" the
391 * insert incorrectly, because those items will be shifted.
392 */
393 List<ListChangeRecord> projectListSplices(List list,
Jennifer Messerly 2013/10/30 23:45:26 newly ported from observe-js
394 List<ListChangeRecord> records) {
395 if (records.length == 1) return records;
396
397 var splices = [];
398 for (var splice in _createInitialSplices(list, records)) {
399 if (splice.addedCount == 1 && splice.removed.length == 1) {
400 if (splice.removed[0] != list[splice.index]) splices.add(splice);
401 continue;
402 }
403
404 splices.addAll(calcSplices(list, splice.index,
405 splice.index + splice.addedCount, splice._removed, 0,
406 splice.removed.length));
407 }
408
409 return splices;
410 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698