blob: ab0e5c2890e3d3322e515673667d4e083e44d2b0 [file] [log] [blame]
// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
part of observable.src.differs;
/// Determines differences between two lists, returning [ListChangeRecord]s.
///
/// While [ListChangeRecord] has more information and can be replayed they carry
/// a more significant cost to calculate and create and should only be used when
/// the details in the record will actually be used.
///
/// See also [EqualityDiffer] for a simpler comparison.
class ListDiffer<E> implements Differ<List<E>> {
final Equality<E> _equality;
const ListDiffer([this._equality = const DefaultEquality()]);
@override
List<ListChangeRecord<E>> diff(List<E> e1, List<E> e2) {
return _calcSplices<E>(
e2,
_equality,
0,
e2.length,
e1,
0,
e1.length,
);
}
}
enum _Edit {
leave,
update,
add,
delete,
}
// Note: This function is *based* on the computation of the Levenshtein
// "edit" distance. The one change is that "updates" are treated as two
// edits - not one. With List splices, an update is really a delete
// followed by an add. By retaining this, we optimize for "keeping" the
// maximum array items in the original array. For example:
//
// 'xxxx123' -> '123yyyy'
//
// With 1-edit updates, the shortest path would be just to update all seven
// characters. With 2-edit updates, we delete 4, leave 3, and add 4. This
// leaves the substring '123' intact.
List<List<int>> _calcEditDistance<E>(
List<E> current,
int currentStart,
int currentEnd,
List<E> old,
int oldStart,
int oldEnd,
) {
// 'Deletion' columns.
final rowCount = oldEnd - oldStart + 1;
final columnCount = currentEnd - currentStart + 1;
final distances = new List<List<int>>(rowCount);
// 'Addition' rows. Initialize null column.
for (var i = 0; i < rowCount; i++) {
distances[i] = new List<int>(columnCount);
distances[i][0] = i;
}
// Initialize null row.
for (var j = 0; j < columnCount; j++) {
distances[0][j] = j;
}
for (var i = 1; i < rowCount; i++) {
for (var j = 1; j < columnCount; j++) {
if (old[oldStart + i - 1] == current[currentStart + j - 1]) {
distances[i][j] = distances[i - 1][j - 1];
} else {
final north = distances[i - 1][j] + 1;
final west = distances[i][j - 1] + 1;
distances[i][j] = math.min(north, west);
}
}
}
return distances;
}
// This starts at the final weight, and walks "backward" by finding
// the minimum previous weight recursively until the origin of the weight
// matrix.
Iterable<_Edit> _spliceOperationsFromEditDistances(List<List<int>> distances) {
var i = distances.length - 1;
var j = distances[0].length - 1;
var current = distances[i][j];
final edits = <_Edit>[];
while (i > 0 || j > 0) {
if (i == 0) {
edits.add(_Edit.add);
j--;
continue;
}
if (j == 0) {
edits.add(_Edit.delete);
i--;
continue;
}
final northWest = distances[i - 1][j - 1];
final west = distances[i - 1][j];
final north = distances[i][j - 1];
final min = math.min(math.min(west, north), northWest);
if (min == northWest) {
if (northWest == current) {
edits.add(_Edit.leave);
} else {
edits.add(_Edit.update);
current = northWest;
}
i--;
j--;
} else if (min == west) {
edits.add(_Edit.delete);
i--;
current = west;
} else {
edits.add(_Edit.add);
j--;
current = north;
}
}
return edits.reversed;
}
int _sharedPrefix<E>(
Equality<E> equality,
List<E> e1,
List<E> e2,
int searchLength,
) {
for (var i = 0; i < searchLength; i++) {
if (!equality.equals(e1[i], e2[i])) {
return i;
}
}
return searchLength;
}
int _sharedSuffix<E>(
Equality<E> equality,
List<E> e1,
List<E> e2,
int searchLength,
) {
var index1 = e1.length;
var index2 = e2.length;
var count = 0;
while (count < searchLength && equality.equals(e1[--index1], e2[--index2])) {
count++;
}
return count;
}
// Lacking individual splice mutation information, the minimal set of
// splices can be synthesized given the previous state and final state of an
// array. The basic approach is to calculate the edit distance matrix and
// choose the shortest path through it.
//
// Complexity: O(l * p)
// l: The length of the current array
// p: The length of the old array
List<ListChangeRecord<E>> _calcSplices<E>(
List<E> current,
Equality<E> equality,
int currentStart,
int currentEnd,
List<E> old,
int oldStart,
int oldEnd,
) {
var prefixCount = 0;
var suffixCount = 0;
final minLength = math.min(currentEnd - currentStart, oldEnd - oldStart);
if (currentStart == 0 && oldStart == 0) {
prefixCount = _sharedPrefix(
equality,
current,
old,
minLength,
);
}
if (currentEnd == current.length && oldEnd == old.length) {
suffixCount = _sharedSuffix(
equality,
current,
old,
minLength - prefixCount,
);
}
currentStart += prefixCount;
oldStart += prefixCount;
currentEnd -= suffixCount;
oldEnd -= suffixCount;
if (currentEnd - currentStart == 0 && oldEnd - oldStart == 0) {
return ListChangeRecord.NONE;
}
if (currentStart == currentEnd) {
final spliceRemoved = old.sublist(oldStart, oldEnd);
return [
new ListChangeRecord<E>.remove(
current,
currentStart,
spliceRemoved,
),
];
}
if (oldStart == oldEnd) {
return [
new ListChangeRecord<E>.add(
current,
currentStart,
currentEnd - currentStart,
),
];
}
final ops = _spliceOperationsFromEditDistances(
_calcEditDistance(
current,
currentStart,
currentEnd,
old,
oldStart,
oldEnd,
),
);
var spliceIndex = -1;
var spliceRemovals = <E>[];
var spliceAddedCount = 0;
bool hasSplice() => spliceIndex != -1;
void resetSplice() {
spliceIndex = -1;
spliceRemovals = <E>[];
spliceAddedCount = 0;
}
var splices = <ListChangeRecord<E>>[];
var index = currentStart;
var oldIndex = oldStart;
for (final op in ops) {
switch (op) {
case _Edit.leave:
if (hasSplice()) {
splices.add(new ListChangeRecord<E>(
current,
spliceIndex,
removed: spliceRemovals,
addedCount: spliceAddedCount,
));
resetSplice();
}
index++;
oldIndex++;
break;
case _Edit.update:
if (!hasSplice()) {
spliceIndex = index;
}
spliceAddedCount++;
index++;
spliceRemovals.add(old[oldIndex]);
oldIndex++;
break;
case _Edit.add:
if (!hasSplice()) {
spliceIndex = index;
}
spliceAddedCount++;
index++;
break;
case _Edit.delete:
if (!hasSplice()) {
spliceIndex = index;
}
spliceRemovals.add(old[oldIndex]);
oldIndex++;
break;
}
}
if (hasSplice()) {
splices.add(new ListChangeRecord<E>(
current,
spliceIndex,
removed: spliceRemovals,
addedCount: spliceAddedCount,
));
}
assert(() {
splices = new List<ListChangeRecord<E>>.unmodifiable(splices);
return true;
}());
return splices;
}
int _intersect(int start1, int end1, int start2, int end2) {
return math.min(end1, end2) - math.max(start1, start2);
}
void _mergeSplices<E>(
List<ListChangeRecord<E>> splices,
ListChangeRecord<E> record,
) {
var spliceIndex = record.index;
var spliceRemoved = record.removed;
var spliceAdded = record.addedCount;
var inserted = false;
var insertionOffset = 0;
// I think the way this works is:
// - the loop finds where the merge should happen
// - it applies the merge in a particular splice
// - then continues and updates the subsequent splices with any offset diff.
for (var i = 0; i < splices.length; i++) {
var current = splices[i];
current = splices[i] = new ListChangeRecord<E>(
current.object,
current.index + insertionOffset,
removed: current.removed,
addedCount: current.addedCount,
);
if (inserted) continue;
var intersectCount = _intersect(
spliceIndex,
spliceIndex + spliceRemoved.length,
current.index,
current.index + current.addedCount,
);
if (intersectCount >= 0) {
// Merge the two splices.
splices.removeAt(i);
i--;
insertionOffset -= current.addedCount - current.removed.length;
spliceAdded += current.addedCount - intersectCount;
final deleteCount =
spliceRemoved.length + current.removed.length - intersectCount;
if (spliceAdded == 0 && deleteCount == 0) {
// Merged splice is a no-op, discard.
inserted = true;
} else {
final removed = current.removed.toList();
if (spliceIndex < current.index) {
// Some prefix of splice.removed is prepended to current.removed.
removed.insertAll(
0,
spliceRemoved.getRange(0, current.index - spliceIndex),
);
}
if (spliceIndex + spliceRemoved.length >
current.index + current.addedCount) {
// Some suffix of splice.removed is appended to current.removed.
removed.addAll(spliceRemoved.getRange(
current.index + current.addedCount - spliceIndex,
spliceRemoved.length,
));
}
spliceRemoved = removed;
if (current.index < spliceIndex) {
spliceIndex = current.index;
}
}
} else if (spliceIndex < current.index) {
// Insert splice here.
inserted = true;
splices.insert(
i,
new ListChangeRecord<E>(
record.object,
spliceIndex,
removed: spliceRemoved,
addedCount: spliceAdded,
),
);
i++;
final offset = spliceAdded - spliceRemoved.length;
current = splices[i] = new ListChangeRecord<E>(
current.object,
current.index + offset,
removed: current.removed,
addedCount: current.addedCount,
);
insertionOffset += offset;
}
}
if (!inserted) {
splices.add(new ListChangeRecord<E>(
record.object,
spliceIndex,
removed: spliceRemoved,
addedCount: spliceAdded,
));
}
}
List<ListChangeRecord<E>> _createInitialSplices<E>(
List<E> list,
List<ListChangeRecord<E>> records,
) {
final splices = <ListChangeRecord<E>>[];
for (var i = 0; i < records.length; i++) {
_mergeSplices(splices, records[i]);
}
return splices;
}
// We need to summarize change records. Consumers of these records want to
// apply the batch sequentially, and ensure that they can find inserted
// items by looking at that position in the list. This property does not
// hold in our record-as-you-go records. Consider:
//
// var model = toObservable(['a', 'b']);
// model.removeAt(1);
// model.insertAll(0, ['c', 'd', 'e']);
// model.removeRange(1, 3);
// model.insert(1, 'f');
//
// Here, we inserted some records and then removed some of them.
// If someone processed these records naively, they would "play back" the
// insert incorrectly, because those items will be shifted.
List<ListChangeRecord<E>> projectListSplices<E>(
List<E> list,
List<ListChangeRecord<E>> records, [
Equality<E> equality = const DefaultEquality(),
]) {
if (records.length <= 1) return records;
final splices = <ListChangeRecord<E>>[];
final initialSplices = _createInitialSplices(list, records);
for (final splice in initialSplices) {
if (splice.addedCount == 1 && splice.removed.length == 1) {
if (splice.removed[0] != list[splice.index]) {
splices.add(splice);
}
continue;
}
splices.addAll(
_calcSplices(
list,
equality,
splice.index,
splice.index + splice.addedCount,
splice.removed,
0,
splice.removed.length,
),
);
}
return splices;
}