Permalink
Browse files

#259 - Optimize compareArrays in preparation for adding support for m…

…ove detection.

This new code is faster and smaller, and passes all specs.
  • Loading branch information...
1 parent 5d724a4 commit 82953a367034febb4589adf2ada19916b3703e3f @mbest mbest committed May 23, 2012
Showing with 53 additions and 64 deletions.
  1. +53 −64 src/binding/editDetection/compareArrays.js
@@ -1,83 +1,72 @@
-(function () {
+ko.utils.compareArrays = (function () {
+ var statusNotInOld = 'added', statusNotInNew = 'deleted';
+
// Simple calculation based on Levenshtein distance.
- function calculateEditDistanceMatrix(oldArray, newArray, maxAllowedDistance) {
- var distances = [];
- for (var i = 0; i <= newArray.length; i++)
- distances[i] = [];
+ function compareArrays(oldArray, newArray) {
+ oldArray = oldArray || [];
+ newArray = newArray || [];
- // Top row - transform old array into empty array via deletions
- for (var i = 0, j = Math.min(oldArray.length, maxAllowedDistance); i <= j; i++)
- distances[0][i] = i;
+ if (oldArray.length <= newArray.length)
+ return compareSmallArrayToBigArray(oldArray, newArray, statusNotInOld, statusNotInNew);
+ else
+ return compareSmallArrayToBigArray(newArray, oldArray, statusNotInNew, statusNotInOld);
+ }
- // Left row - transform empty array into new array via additions
- for (var i = 1, j = Math.min(newArray.length, maxAllowedDistance); i <= j; i++) {
- distances[i][0] = i;
- }
+ function compareSmallArrayToBigArray(smlArray, bigArray, statusNotInSml, statusNotInBig) {
+ var myMin = Math.min,
+ myMax = Math.max,
+ editDistanceMatrix = [],
+ smlIndex, smlIndexMax = smlArray.length,
+ bigIndex, bigIndexMax = bigArray.length,
+ compareRange = (bigIndexMax - smlIndexMax) || 1,
+ maxDistance = smlIndexMax + bigIndexMax + 1,
+ thisRow, lastRow,
+ bigIndexMaxForRow, bigIndexMinForRow;
- // Fill out the body of the array
- var oldIndex, oldIndexMax = oldArray.length, newIndex, newIndexMax = newArray.length;
- var distanceViaAddition, distanceViaDeletion;
- for (oldIndex = 1; oldIndex <= oldIndexMax; oldIndex++) {
- var newIndexMinForRow = Math.max(1, oldIndex - maxAllowedDistance);
- var newIndexMaxForRow = Math.min(newIndexMax, oldIndex + maxAllowedDistance);
- for (newIndex = newIndexMinForRow; newIndex <= newIndexMaxForRow; newIndex++) {
- if (oldArray[oldIndex - 1] === newArray[newIndex - 1])
- distances[newIndex][oldIndex] = distances[newIndex - 1][oldIndex - 1];
+ for (smlIndex = 0; smlIndex <= smlIndexMax; smlIndex++) {
+ lastRow = thisRow;
+ editDistanceMatrix.push(thisRow = []);
+ bigIndexMaxForRow = myMin(bigIndexMax, smlIndex + compareRange);
+ bigIndexMinForRow = myMax(0, smlIndex - 1);
+ for (bigIndex = bigIndexMinForRow; bigIndex <= bigIndexMaxForRow; bigIndex++) {
+ if (!bigIndex)
+ thisRow[bigIndex] = smlIndex + 1;
+ else if (!smlIndex) // Top row - transform empty array into new array via additions
+ thisRow[bigIndex] = bigIndex + 1;
+ else if (smlArray[smlIndex - 1] === bigArray[bigIndex - 1])
+ thisRow[bigIndex] = lastRow[bigIndex - 1]; // copy value (no edit)
else {
- var northDistance = distances[newIndex - 1][oldIndex] === undefined ? Number.MAX_VALUE : distances[newIndex - 1][oldIndex] + 1;
- var westDistance = distances[newIndex][oldIndex - 1] === undefined ? Number.MAX_VALUE : distances[newIndex][oldIndex - 1] + 1;
- distances[newIndex][oldIndex] = Math.min(northDistance, westDistance);
+ var northDistance = lastRow[bigIndex] || maxDistance; // not in big (deletion)
+ var westDistance = thisRow[bigIndex - 1] || maxDistance; // not in small (addition)
+ thisRow[bigIndex] = myMin(northDistance, westDistance) + 1;
}
}
}
- return distances;
- }
-
- function findEditScriptFromEditDistanceMatrix(editDistanceMatrix, oldArray, newArray) {
- var oldIndex = oldArray.length;
- var newIndex = newArray.length;
- var editScript = [];
- var maxDistance = editDistanceMatrix[newIndex][oldIndex];
- if (maxDistance === undefined)
- return null; // maxAllowedDistance must be too small
- while ((oldIndex > 0) || (newIndex > 0)) {
- var me = editDistanceMatrix[newIndex][oldIndex];
- var distanceViaAdd = (newIndex > 0) ? editDistanceMatrix[newIndex - 1][oldIndex] : maxDistance + 1;
- var distanceViaDelete = (oldIndex > 0) ? editDistanceMatrix[newIndex][oldIndex - 1] : maxDistance + 1;
- var distanceViaRetain = (newIndex > 0) && (oldIndex > 0) ? editDistanceMatrix[newIndex - 1][oldIndex - 1] : maxDistance + 1;
- if ((distanceViaAdd === undefined) || (distanceViaAdd < me - 1)) distanceViaAdd = maxDistance + 1;
- if ((distanceViaDelete === undefined) || (distanceViaDelete < me - 1)) distanceViaDelete = maxDistance + 1;
- if (distanceViaRetain < me - 1) distanceViaRetain = maxDistance + 1;
-
- if ((distanceViaAdd <= distanceViaDelete) && (distanceViaAdd < distanceViaRetain)) {
- editScript.push({ status: "added", value: newArray[newIndex - 1] });
- newIndex--;
- } else if ((distanceViaDelete < distanceViaAdd) && (distanceViaDelete < distanceViaRetain)) {
- editScript.push({ status: "deleted", value: oldArray[oldIndex - 1] });
- oldIndex--;
+ var editScript = [], meMinusOne;
+ for (smlIndex = smlIndexMax, bigIndex = bigIndexMax; smlIndex || bigIndex;) {
+ meMinusOne = editDistanceMatrix[smlIndex][bigIndex] - 1;
+ if (bigIndex && meMinusOne === editDistanceMatrix[smlIndex][bigIndex-1]) {
+ editScript[editScript.length] = { // added
+ 'status': statusNotInSml,
+ 'value': bigArray[--bigIndex] };
+ } else if (smlIndex && meMinusOne === editDistanceMatrix[smlIndex - 1][bigIndex]) {
+ editScript[editScript.length] = { // deleted
+ 'status': statusNotInBig,
+ 'value': smlArray[--smlIndex] };
} else {
- editScript.push({ status: "retained", value: oldArray[oldIndex - 1] });
- newIndex--;
- oldIndex--;
+ editScript.push({
+ 'status': "retained",
+ 'value': bigArray[--bigIndex] });
+ --smlIndex;
}
}
+
return editScript.reverse();
}
- ko.utils.compareArrays = function (oldArray, newArray, maxEditsToConsider) {
- if (maxEditsToConsider === undefined) {
- return ko.utils.compareArrays(oldArray, newArray, 1) // First consider likely case where there is at most one edit (very fast)
- || ko.utils.compareArrays(oldArray, newArray, 10) // If that fails, account for a fair number of changes while still being fast
- || ko.utils.compareArrays(oldArray, newArray, Number.MAX_VALUE); // Ultimately give the right answer, even though it may take a long time
- } else {
- oldArray = oldArray || [];
- newArray = newArray || [];
- var editDistanceMatrix = calculateEditDistanceMatrix(oldArray, newArray, maxEditsToConsider);
- return findEditScriptFromEditDistanceMatrix(editDistanceMatrix, oldArray, newArray);
- }
- };
+ return compareArrays;
})();
ko.exportSymbol('utils.compareArrays', ko.utils.compareArrays);

0 comments on commit 82953a3

Please sign in to comment.