// Myers' Diff Algorithm
|
// Modified from https://github.com/kpdecker/jsdiff/blob/master/src/diff/base.js
|
type EqualFunc<T> = (a: T, b: T) => boolean;
|
|
type DiffComponent = {
|
count: number
|
added: boolean
|
removed: boolean,
|
indices: number[]
|
}
|
|
type DiffPath = {
|
components: DiffComponent[],
|
newPos: number
|
}
|
|
// Using O(ND) algorithm
|
// TODO: Optimize when diff is large.
|
function diff<T>(oldArr: T[], newArr: T[], equals: EqualFunc<T>): DiffComponent[] {
|
if (!equals) {
|
equals = function (a, b) {
|
return a === b;
|
};
|
}
|
|
oldArr = oldArr.slice();
|
newArr = newArr.slice();
|
// Allow subclasses to massage the input prior to running
|
var newLen = newArr.length;
|
var oldLen = oldArr.length;
|
var editLength = 1;
|
var maxEditLength = newLen + oldLen;
|
var bestPath: DiffPath[] = [{ newPos: -1, components: [] }];
|
|
// Seed editLength = 0, i.e. the content starts with the same values
|
var oldPos = extractCommon<T>(bestPath[0], newArr, oldArr, 0, equals);
|
if (!oldLen // All new created
|
|| !newLen // Clear
|
|| (bestPath[0].newPos + 1 >= newLen && oldPos + 1 >= oldLen)) {
|
var indices = [];
|
var allCleared = !newLen && oldLen > 0;
|
var allCreated = !oldLen && newLen > 0;
|
for (let i = 0; i < (allCleared ? oldArr : newArr).length; i++) {
|
indices.push(i);
|
}
|
// Identity per the equality and tokenizer
|
return [{
|
indices: indices,
|
count: indices.length,
|
added: allCreated,
|
removed: allCleared
|
}];
|
}
|
|
// Main worker method. checks all permutations of a given edit length for acceptance.
|
function execEditLength() {
|
for (let diagonalPath = -1 * editLength; diagonalPath <= editLength; diagonalPath += 2) {
|
var basePath;
|
var addPath = bestPath[diagonalPath - 1];
|
var removePath = bestPath[diagonalPath + 1];
|
var oldPos = (removePath ? removePath.newPos : 0) - diagonalPath;
|
if (addPath) {
|
// No one else is going to attempt to use this value, clear it
|
bestPath[diagonalPath - 1] = undefined;
|
}
|
|
var canAdd = addPath && addPath.newPos + 1 < newLen;
|
var canRemove = removePath && 0 <= oldPos && oldPos < oldLen;
|
if (!canAdd && !canRemove) {
|
// If this path is a terminal then prune
|
bestPath[diagonalPath] = undefined;
|
continue;
|
}
|
|
// Select the diagonal that we want to branch from. We select the prior
|
// path whose position in the new string is the farthest from the origin
|
// and does not pass the bounds of the diff graph
|
if (!canAdd || (canRemove && addPath.newPos < removePath.newPos)) {
|
basePath = clonePath(removePath);
|
pushComponent(basePath.components, false, true);
|
}
|
else {
|
basePath = addPath; // No need to clone, we've pulled it from the list
|
basePath.newPos++;
|
pushComponent(basePath.components, true, false);
|
}
|
|
oldPos = extractCommon<T>(basePath, newArr, oldArr, diagonalPath, equals);
|
|
// If we have hit the end of both strings, then we are done
|
if (basePath.newPos + 1 >= newLen && oldPos + 1 >= oldLen) {
|
return buildValues(basePath.components);
|
}
|
else {
|
// Otherwise track this path as a potential candidate and continue.
|
bestPath[diagonalPath] = basePath;
|
}
|
}
|
|
editLength++;
|
}
|
|
while (editLength <= maxEditLength) {
|
var ret = execEditLength();
|
if (ret) {
|
return ret;
|
}
|
}
|
}
|
|
function extractCommon<T>(basePath: DiffPath, newArr: T[], oldArr: T[], diagonalPath: number, equals: EqualFunc<T>) {
|
var newLen = newArr.length;
|
var oldLen = oldArr.length;
|
var newPos = basePath.newPos;
|
var oldPos = newPos - diagonalPath;
|
var commonCount = 0;
|
|
while (newPos + 1 < newLen && oldPos + 1 < oldLen && equals(newArr[newPos + 1], oldArr[oldPos + 1])) {
|
newPos++;
|
oldPos++;
|
commonCount++;
|
}
|
|
if (commonCount) {
|
basePath.components.push({
|
count: commonCount,
|
added: false,
|
removed: false,
|
indices: []
|
});
|
}
|
|
basePath.newPos = newPos;
|
return oldPos;
|
}
|
|
function pushComponent(components: DiffComponent[], added: boolean, removed: boolean) {
|
var last = components[components.length - 1];
|
if (last && last.added === added && last.removed === removed) {
|
// We need to clone here as the component clone operation is just
|
// as shallow array clone
|
components[components.length - 1] = {
|
count: last.count + 1,
|
added,
|
removed,
|
indices: []
|
};
|
}
|
else {
|
components.push({
|
count: 1,
|
added,
|
removed,
|
indices: []
|
});
|
}
|
}
|
|
function buildValues(components: DiffComponent[]) {
|
var componentPos = 0;
|
var componentLen = components.length;
|
var newPos = 0;
|
var oldPos = 0;
|
|
for (; componentPos < componentLen; componentPos++) {
|
var component = components[componentPos];
|
if (!component.removed) {
|
var indices = [];
|
for (let i = newPos; i < newPos + component.count; i++) {
|
indices.push(i);
|
}
|
component.indices = indices;
|
newPos += component.count;
|
// Common case
|
if (!component.added) {
|
oldPos += component.count;
|
}
|
}
|
else {
|
for (let i = oldPos; i < oldPos + component.count; i++) {
|
component.indices.push(i);
|
}
|
oldPos += component.count;
|
}
|
}
|
|
return components;
|
}
|
|
function clonePath(path: DiffPath) {
|
return { newPos: path.newPos, components: path.components.slice(0) };
|
}
|
|
export default function arrayDiff<T>(oldArr: T[], newArr: T[], equal?: EqualFunc<T>): DiffComponent[] {
|
return diff(oldArr, newArr, equal);
|
}
|