/* * (c) Copyright IBM Corp. 2000, 2001. * All Rights Reserved. */ package org.eclipse.compare.rangedifferencer; import java.util.*; import org.eclipse.jface.util.Assert; import org.eclipse.core.runtime.IProgressMonitor; /** * A RangeDifferencer finds the differences between two or three IRangeComparators. *

* To use the differencer, clients provide an IRangeComparator * that breaks their input data into a sequence of comparable entities. The differencer * returns the differences among these sequences as an array of RangeDifference objects * (findDifferences methods). * Every RangeDifference represents a single kind of difference * and the corresponding ranges of the underlying comparable entities in the * left, right, and optionally ancestor sides. *

* Alternatively, the findRanges methods not only return objects for * the differing ranges but for non-differing ranges too. *

* The algorithm used is an objectified version of one described in: * A File Comparison Program, by Webb Miller and Eugene W. Myers, * Software Practice and Experience, Vol. 15, Nov. 1985. * * @see IRangeComparator * @see RangeDifference */ public final class RangeDifferencer { private static final RangeDifference[] EMPTY_RESULT= new RangeDifference[0]; /* (non Javadoc) * Non instantiatiable! */ private RangeDifferencer() { } /** * Finds the differences between two IRangeComparators. * The differences are returned as an array of RangeDifferences. * If no differences are detected an empty array is returned. * * @param left the left range comparator * @param right the right range comparator * @return an array of range differences, or an empty array if no differences were found */ public static RangeDifference[] findDifferences(IRangeComparator left, IRangeComparator right) { return findDifferences((IProgressMonitor)null, left, right); } /** * Finds the differences between two IRangeComparators. * The differences are returned as an array of RangeDifferences. * If no differences are detected an empty array is returned. * * @param pm if not null used to report progress * @param left the left range comparator * @param right the right range comparator * @return an array of range differences, or an empty array if no differences were found */ public static RangeDifference[] findDifferences(IProgressMonitor pm, IRangeComparator left, IRangeComparator right) { // assert that both IRangeComparators are of the same class Assert.isTrue(right.getClass().equals(left.getClass())); int rightSize= right.getRangeCount(); int leftSize= left.getRangeCount(); // // Differences matrix: // only the last d of each diagonal is stored, i.e., lastDiagonal[k] = row of d // int diagLen= 2 * Math.max(rightSize, leftSize); // bound on the size of edit script int maxDiagonal= diagLen; int lastDiagonal[]= new int[diagLen + 1]; // the row containing the last d // on diagonal k (lastDiagonal[k] = row) int origin= diagLen / 2; // origin of diagonal 0 // script corresponding to d[k] LinkedRangeDifference script[]= new LinkedRangeDifference[diagLen + 1]; int row, col; // find common prefix for (row= 0; row < rightSize && row < leftSize && rangesEqual(right, row, left, row) == true; ++row) ; lastDiagonal[origin]= row; script[origin]= null; int lower= (row == rightSize) ? origin + 1 : origin - 1; int upper= (row == leftSize) ? origin - 1 : origin + 1; if (lower > upper) return EMPTY_RESULT; //System.out.println("findDifferences: " + maxDiagonal + " " + lower + " " + upper); // for each value of the edit distance for (int d= 1; d <= maxDiagonal; ++d) { // d is the current edit distance if (pm != null) pm.worked(1); if (right.skipRangeComparison(d, maxDiagonal, left)) return EMPTY_RESULT; // should be something we already found // for each relevant diagonal (-d, -d+2 ..., d-2, d) for (int k= lower; k <= upper; k += 2) { // k is the current diagonal LinkedRangeDifference edit; if (pm != null && pm.isCanceled()) return EMPTY_RESULT; if (k == origin - d || k != origin + d && lastDiagonal[k + 1] >= lastDiagonal[k - 1]) { // // move down // row= lastDiagonal[k + 1] + 1; edit= new LinkedRangeDifference(script[k + 1], LinkedRangeDifference.DELETE); } else { // // move right // row= lastDiagonal[k - 1]; edit= new LinkedRangeDifference(script[k - 1], LinkedRangeDifference.INSERT); } col= row + k - origin; edit.fRightStart= row; edit.fLeftStart= col; Assert.isTrue(k >= 0 && k <= maxDiagonal); script[k]= edit; // slide down the diagonal as far as possible while (row < rightSize && col < leftSize && rangesEqual(right, row, left, col) == true) { ++row; ++col; } Assert.isTrue(k >= 0 && k <= maxDiagonal); // Unreasonable value for diagonal index lastDiagonal[k]= row; if (row == rightSize && col == leftSize) { //showScript(script[k], right, left); return createDifferencesRanges(script[k]); } if (row == rightSize) lower= k + 2; if (col == leftSize) upper= k - 2; } --lower; ++upper; } // too many differences Assert.isTrue(false); return null; } /** * Finds the differences among three IRangeComparators. * The differences are returned as a list of RangeDifferences. * If no differences are detected an empty list is returned. * If the ancestor range comparator is null, a two-way * comparison is performed. * * @param ancestor the ancestor range comparator or null * @param left the left range comparator * @param right the right range comparator * @return an array of range differences, or an empty array if no differences were found */ public static RangeDifference[] findDifferences(IRangeComparator ancestor, IRangeComparator left, IRangeComparator right) { return findDifferences(null, ancestor, left, right); } /** * Finds the differences among three IRangeComparators. * The differences are returned as a list of RangeDifferences. * If no differences are detected an empty list is returned. * If the ancestor range comparator is null, a two-way * comparison is performed. * * @param pm if not null used to report progress * @param ancestor the ancestor range comparator or null * @param left the left range comparator * @param right the right range comparator * @return an array of range differences, or an empty array if no differences were found */ public static RangeDifference[] findDifferences(IProgressMonitor pm, IRangeComparator ancestor, IRangeComparator left, IRangeComparator right) { if (ancestor == null) return findDifferences(pm, left, right); RangeDifference[] leftAncestorScript= null; RangeDifference[] rightAncestorScript= findDifferences(pm, ancestor, right); if (rightAncestorScript != null) leftAncestorScript= findDifferences(pm, ancestor, left); if (rightAncestorScript == null || leftAncestorScript == null) return null; DifferencesIterator myIter= new DifferencesIterator(rightAncestorScript); DifferencesIterator yourIter= new DifferencesIterator(leftAncestorScript); List diff3= new ArrayList(); diff3.add(new RangeDifference(RangeDifference.ERROR)); // add a sentinel int changeRangeStart= 0; int changeRangeEnd= 0; // // Combine the two two-way edit scripts into one // while (myIter.fDifference != null || yourIter.fDifference != null) { DifferencesIterator startThread; myIter.removeAll(); yourIter.removeAll(); // // take the next diff that is closer to the start // if (myIter.fDifference == null) startThread= yourIter; else if (yourIter.fDifference == null) startThread= myIter; else { // not at end of both scripts take the lowest range if (myIter.fDifference.fLeftStart <= yourIter.fDifference.fLeftStart) // 2 -> common (Ancestor) change range startThread= myIter; else startThread= yourIter; } changeRangeStart= startThread.fDifference.fLeftStart; changeRangeEnd= startThread.fDifference.leftEnd(); startThread.next(); // // check for overlapping changes with other thread // merge overlapping changes with this range // DifferencesIterator other= startThread.other(myIter, yourIter); while (other.fDifference != null && other.fDifference.fLeftStart <= changeRangeEnd) { int newMax= other.fDifference.leftEnd(); other.next(); if (newMax >= changeRangeEnd) { changeRangeEnd= newMax; other= other.other(myIter, yourIter); } } diff3.add(createRangeDifference3(myIter, yourIter, diff3, right, left, changeRangeStart, changeRangeEnd)); } // remove sentinel diff3.remove(0); return (RangeDifference[]) diff3.toArray(EMPTY_RESULT); } /** * Finds the differences among two IRangeComparators. * In contrast to findDifferences, the result * contains RangeDifference elements for non-differing ranges too. * * @param left the left range comparator * @param right the right range comparator * @return an array of range differences */ public static RangeDifference[] findRanges(IRangeComparator left, IRangeComparator right) { return findRanges((IProgressMonitor)null, left, right); } /** * Finds the differences among two IRangeComparators. * In contrast to findDifferences, the result * contains RangeDifference elements for non-differing ranges too. * * @param pm if not null used to report progress * @param left the left range comparator * @param right the right range comparator * @return an array of range differences */ public static RangeDifference[] findRanges(IProgressMonitor pm, IRangeComparator left, IRangeComparator right) { RangeDifference[] in= findDifferences(pm, left, right); List out= new ArrayList(); RangeDifference rd; int mstart= 0; int ystart= 0; for (int i= 0; i < in.length; i++) { RangeDifference es= in[i]; rd= new RangeDifference(RangeDifference.NOCHANGE, mstart, es.rightStart() - mstart, ystart, es.leftStart() - ystart); if (rd.maxLength() != 0) out.add(rd); out.add(es); mstart= es.rightEnd(); ystart= es.leftEnd(); } rd= new RangeDifference(RangeDifference.NOCHANGE, mstart, right.getRangeCount() - mstart, ystart, left.getRangeCount() - ystart); if (rd.maxLength() > 0) out.add(rd); return (RangeDifference[]) out.toArray(EMPTY_RESULT); } /** * Finds the differences among three IRangeComparators. * In contrast to findDifferences, the result * contains RangeDifference elements for non-differing ranges too. * If the ancestor range comparator is null, a two-way * comparison is performed. * * @param pm if not null used to report progress * @param ancestor the ancestor range comparator or null * @param left the left range comparator * @param right the right range comparator * @return an array of range differences */ public static RangeDifference[] findRanges(IRangeComparator ancestor, IRangeComparator left, IRangeComparator right) { return findRanges(null, ancestor, left, right); } /** * Finds the differences among three IRangeComparators. * In contrast to findDifferences, the result * contains RangeDifference elements for non-differing ranges too. * If the ancestor range comparator is null, a two-way * comparison is performed. * * @param pm if not null used to report progress * @param ancestor the ancestor range comparator or null * @param left the left range comparator * @param right the right range comparator * @return an array of range differences */ public static RangeDifference[] findRanges(IProgressMonitor pm, IRangeComparator ancestor, IRangeComparator left, IRangeComparator right) { if (ancestor == null) return findRanges(pm, left, right); RangeDifference[] in= findDifferences(pm, ancestor, left, right); List out= new ArrayList(); RangeDifference rd; int mstart= 0; int ystart= 0; int astart= 0; for (int i= 0; i < in.length; i++) { RangeDifference es= (RangeDifference) in[i]; rd= new RangeDifference(RangeDifference.NOCHANGE, mstart, es.rightStart() - mstart, ystart, es.leftStart() - ystart, astart, es.ancestorStart() - astart); if (rd.maxLength() > 0) out.add(rd); out.add(es); mstart= es.rightEnd(); ystart= es.leftEnd(); astart= es.ancestorEnd(); } rd= new RangeDifference(RangeDifference.NOCHANGE, mstart, right.getRangeCount() - mstart, ystart, left.getRangeCount() - ystart, astart, ancestor.getRangeCount() - astart); if (rd.maxLength() > 0) out.add(rd); return (RangeDifference[]) out.toArray(EMPTY_RESULT); } //---- private methods /** * Creates a Vector of DifferencesRanges out of the LinkedRangeDifference. * It coalesces adjacent changes. * In addition, indices are changed such that the ranges are 1) open, i.e, * the end of the range is not included, and 2) are zero based. */ private static RangeDifference[] createDifferencesRanges(LinkedRangeDifference start) { LinkedRangeDifference ep= reverseDifferences(start); ArrayList result= new ArrayList(); RangeDifference es= null; while (ep != null) { es= new RangeDifference(RangeDifference.CHANGE); if (ep.isInsert()) { es.fRightStart= ep.fRightStart + 1; es.fLeftStart= ep.fLeftStart; RangeDifference b= ep; do { ep= ep.getNext(); es.fLeftLength++; } while (ep != null && ep.isInsert() && ep.fRightStart == b.fRightStart); } else { es.fRightStart= ep.fRightStart; es.fLeftStart= ep.fLeftStart; RangeDifference a= ep; // // deleted lines // do { a= ep; ep= ep.getNext(); es.fRightLength++; } while (ep != null && ep.isDelete() && ep.fRightStart == a.fRightStart + 1); boolean change= (ep != null && ep.isInsert() && ep.fRightStart == a.fRightStart); if (change) { RangeDifference b= ep; // // replacement lines // do { ep= ep.getNext(); es.fLeftLength++; } while (ep != null && ep.isInsert() && ep.fRightStart == b.fRightStart); } else { es.fLeftLength= 0; } es.fLeftStart++; // meaning of range changes from "insert after", to "replace with" } // // the script commands are 1 based, subtract one to make them zero based // es.fRightStart--; es.fLeftStart--; result.add(es); } return (RangeDifference[]) result.toArray(EMPTY_RESULT); } /** * Creates a RangeDifference3 given the * state of two DifferenceIterators. */ private static RangeDifference createRangeDifference3( DifferencesIterator myIter, DifferencesIterator yourIter, List diff3, IRangeComparator right, IRangeComparator left, int changeRangeStart, int changeRangeEnd) { int rightStart, rightEnd; int leftStart, leftEnd; int kind= RangeDifference.ERROR; RangeDifference last= (RangeDifference) diff3.get(diff3.size() - 1); Assert.isTrue((myIter.getCount() != 0 || yourIter.getCount() != 0)); // At least one range array must be non-empty // // find corresponding lines to fChangeRangeStart/End in right and left // if (myIter.getCount() == 0) { // only left changed rightStart= changeRangeStart - last.ancestorEnd() + last.rightEnd(); rightEnd= changeRangeEnd - last.ancestorEnd() + last.rightEnd(); kind= RangeDifference.LEFT; } else { RangeDifference f= (RangeDifference) myIter.fRange.get(0); RangeDifference l= (RangeDifference) myIter.fRange.get(myIter.fRange.size() - 1); rightStart= changeRangeStart - f.fLeftStart + f.fRightStart; rightEnd= changeRangeEnd - l.leftEnd() + l.rightEnd(); } if (yourIter.getCount() == 0) { // only right changed leftStart= changeRangeStart - last.ancestorEnd() + last.leftEnd(); leftEnd= changeRangeEnd - last.ancestorEnd() + last.leftEnd(); kind= RangeDifference.RIGHT; } else { RangeDifference f= (RangeDifference) yourIter.fRange.get(0); RangeDifference l= (RangeDifference) yourIter.fRange.get(yourIter.fRange.size() - 1); leftStart= changeRangeStart - f.fLeftStart + f.fRightStart; leftEnd= changeRangeEnd - l.leftEnd() + l.rightEnd(); } if (kind == RangeDifference.ERROR) { // overlapping change (conflict) -> compare the changed ranges if (rangeSpansEqual(right, rightStart, rightEnd - rightStart, left, leftStart, leftEnd - leftStart)) kind= RangeDifference.ANCESTOR; else kind= RangeDifference.CONFLICT; } return new RangeDifference(kind, rightStart, rightEnd - rightStart, leftStart, leftEnd - leftStart, changeRangeStart, changeRangeEnd - changeRangeStart); } /** * Tests if two ranges are equal */ private static boolean rangesEqual(IRangeComparator a, int ai, IRangeComparator b, int bi) { return a.rangesEqual(ai, b, bi); } /** * Tests whether right and left changed in the same way */ private static boolean rangeSpansEqual(IRangeComparator right, int rightStart, int rightLen, IRangeComparator left, int leftStart, int leftLen) { if (rightLen == leftLen) { int i= 0; for (i= 0; i < rightLen; i++) { if (!rangesEqual(right, rightStart + i, left, leftStart + i)) break; } if (i == rightLen) return true; } return false; } /** * Reverses the range differences */ private static LinkedRangeDifference reverseDifferences(LinkedRangeDifference start) { LinkedRangeDifference ep, behind, ahead; ahead= start; ep= null; while (ahead != null) { behind= ep; ep= ahead; ahead= ahead.getNext(); ep.setNext(behind); } return ep; } }