blob: c3b53bdedf94fa6d9f13ab9dc0f0c9f5786244d4 [file] [log] [blame]
/// Determines the set of Levenshtein edits needed to convert [before] into
/// [after].
///
/// Returns the list of differences.
/// From: https://en.wikipedia.org/wiki/Levenshtein_distance
List<Diff<T>> calculateDiffs<T>(List<T> before, List<T> after) {
var matrix = _Matrix(before.length + 1, after.length + 1);
// Initialize the first row of distances. It's the number of edits to get
// from an empty expected list to the prefix of the actual list of a given
// length, which is just that many inserts.
for (var x = 0; x < before.length + 1; x++) {
matrix.set(x, 0, x);
}
// For each prefix of the after list, calculate the edit distances to reach
// it from each prefix of the before list. Each row is calculated from the
// previous row.
for (var y = 1; y < after.length + 1; y++) {
// The first element of v1 is A[i+1][0].
// The edit distance is delete (i+1) elements from s to match empty t.
matrix.set(0, y, y);
// Use formula to fill in the rest of the row.
for (var x = 1; x < before.length + 1; x++) {
var cost = after[y - 1] == before[x - 1] ? 0 : 1;
var left = matrix.get(x - 1, y) + 1;
var up = matrix.get(x, y - 1) + 1;
var diagonal = matrix.get(x - 1, y - 1) + cost;
matrix.set(x, y, _min3(left, up, diagonal));
}
}
var x = matrix.width - 1;
var y = matrix.height - 1;
var edits = <Diff<T>>[];
while (x > 0 || y > 0) {
var here = matrix.get(x, y);
var left = x > 0 ? matrix.get(x - 1, y) : here + 999;
var up = y > 0 ? matrix.get(x, y - 1) : here + 999;
var diagonal = x > 0 && y > 0 ? matrix.get(x - 1, y - 1) : here + 999;
if (diagonal <= left && diagonal <= up) {
// We're consuming an element from both.
if (diagonal != here) {
// And they didn't match, so substitute.
edits.add(SubstituteDiff(before[x - 1], after[y - 1]));
}
x--;
y--;
} else if (left <= up) {
// We're consuming an element from before, so there is a new element.
edits.add(InsertDiff(before[x - 1]));
x--;
} else {
// We're consuming an element from after, so there is a removed element.
edits.add(DeleteDiff(after[y - 1]));
y--;
}
}
return edits.reversed.toList();
}
/// Determines the number of Levenshtein edits needed to convert [before] into
/// [after].
///
/// Treates a substitution as a single edit and not a delete + insert.
int countDifferences<T>(
List<T> before,
List<T> after, {
bool Function(T, T)? compare,
}) => calculateDiffs(before, after).length;
sealed class Diff<T> {}
final class SubstituteDiff<T> extends Diff<T> {
final T before;
final T after;
SubstituteDiff(this.before, this.after);
}
final class InsertDiff<T> extends Diff<T> {
final T after;
InsertDiff(this.after);
}
final class DeleteDiff<T> extends Diff<T> {
final T before;
DeleteDiff(this.before);
}
/// Returns the minimum of [a], [b], and [c].
int _min3(int a, int b, int c) {
if (a < b) {
return a < c ? a : c;
} else {
return b < c ? b : c;
}
}
/// A two-dimensional fixed-size matrix of integers.
class _Matrix {
/// The number of elements in a row of the matrix.
final int width;
/// The number of elements in a column of the matrix.
final int height;
final List<int> _elements;
/// Creates a new matrix with [width], [height] value initialized to `0`.
_Matrix(this.width, this.height) : _elements = List.filled(width * height, 0);
/// Gets the element in the array at [x], [y].
int get(int x, int y) => _elements[y * width + x];
/// Sets the value in the matrix at [x], [y] to [value].
void set(int x, int y, int value) {
_elements[y * width + x] = value;
}
}