source.ca.odell.glazedlists.impl.Diff Maven / Gradle / Ivy
Show all versions of glazedlists_java15 Show documentation
/* Glazed Lists (c) 2003-2006 */
/* http://publicobject.com/glazedlists/ publicobject.com,*/
/* O'Dell Engineering Ltd.*/
package ca.odell.glazedlists.impl;
import ca.odell.glazedlists.EventList;
import java.util.*;
/**
* Implementation of Eugene W. Myer's paper, "An O(ND) Difference Algorithm and
* Its Variations", the same algorithm found in GNU diff.
*
* Note that this is a cleanroom implementation of this popular algorithm
* that is particularly suited for the Java programmer. The variable names are
* descriptive and the approach is more object-oriented than Myer's sample
* algorithm.
*
* @author Jesse Wilson
*/
public final class Diff {
/**
* Convenience method for {@link #replaceAll(EventList,List,boolean,Comparator)
* replaceAll()} that uses {@link Object#equals(Object)} to determine
* equality.
*/
public static void replaceAll(EventList target, List source, boolean updates) {
replaceAll(target, source, updates, (Comparator)GlazedListsImpl.equalsComparator());
}
/**
* Replace the complete contents of the target {@link EventList} with the
* complete contents of the source {@link EventList} while making as few
* list changes as possible.
*
* @param comparator a {@link Comparator} to use to test only for equality.
* This comparator shall return 0 to signal that two elements are
* equal, and nonzero otherwise.
* @param updates whether to fire update events for Objects that are equal
* in both {@link List}s.
*/
public static void replaceAll(EventList target, List source,
boolean updates, Comparator comparator) {
DiffMatcher listDiffMatcher = new ListDiffMatcher(target, source, comparator);
List editScript = shortestEditScript(listDiffMatcher);
// target is x axis. Changes in X mean advance target index
// source is y axis. Changes to y mean advance source index
int targetIndex = 0;
int sourceIndex = 0;
// walk through points, applying changes as they arrive
Point previousPoint = null;
for(Iterator i = editScript.iterator(); i.hasNext();) {
Point currentPoint = i.next();
// skip the first point
if(previousPoint == null) {
previousPoint = currentPoint;
continue;
}
// figure out what the relationship in the values is
int deltaX = currentPoint.getX() - previousPoint.getX();
int deltaY = currentPoint.getY() - previousPoint.getY();
// handle an update
if(deltaX == deltaY) {
if(updates) {
for(int u = 0; u < deltaX; u++) {
target.set(targetIndex + u, source.get(sourceIndex + u));
}
}
targetIndex += deltaX;
sourceIndex += deltaY;
// handle a remove
} else if(deltaX == 1 && deltaY == 0) {
target.remove(targetIndex);
// handle an insert
} else if(deltaX == 0 && deltaY == 1) {
target.add(targetIndex, source.get(sourceIndex));
sourceIndex++;
targetIndex++;
// should never be reached
} else {
throw new IllegalStateException();
}
// the next previous point is this current point
previousPoint = currentPoint;
}
}
/**
* Calculate the length of the longest common subsequence for the specified
* input.
*/
private static List shortestEditScript(DiffMatcher input) {
// calculate limits based on the size of the input matcher
int N = input.getAlphaLength();
int M = input.getBetaLength();
Point maxPoint = new Point(N, M);
int maxSteps = N + M;
// use previous round furthest reaching D-path to determine the
// new furthest reaching (D+1)-path
Map furthestReachingPoints = new HashMap();
// walk through in stages, each stage adding one non-diagonal.
// D == count of non-diagonals in current stage
for(int D = 0; D <= maxSteps; D++) {
// exploit diagonals in order to save storing both X and Y
// diagonal k means every point on k, (k = x - y)
for(int k = -D; k <= D; k += 2) {
// the furthest reaching D-path on the left and right diagonals
// either of these may be null. The terms 'below left' and 'above
// right' refer to the diagonals that the points are on and may
// not be representative of the point positions
Point belowLeft = furthestReachingPoints.get(new Integer(k - 1));
Point aboveRight = furthestReachingPoints.get(new Integer(k + 1));
// the new furthest reaching point to create
Point point;
// first round: we have matched zero in word X
if(furthestReachingPoints.isEmpty()) {
point = new Point(0, 0);
// if this is the leftmost diagonal, or the left edge is
// further than the right edge, our new X is that value and
// our y is one greater (shift verically by one)
} else if(k == -D || (k != D && belowLeft.getX() < aboveRight.getX())) {
point = aboveRight.createDeltaPoint(0, 1);
// if the right edge is further than the left edge, use that
// x and keep y the same (shift horizontally by one)
} else {
point = belowLeft.createDeltaPoint(1, 0);
}
// match as much diagonal as possible from the previous endpoint
while(point.isLessThan(maxPoint) && input.matchPair(point.getX(), point.getY())) {
point = point.incrementDiagonally();
}
// save this furthest reaching path
furthestReachingPoints.put(new Integer(k), point);
// if we're past the end, we have a solution!
if(point.isEqualToOrGreaterThan(maxPoint)) {
return point.trail();
}
}
}
// no solution was found
throw new IllegalStateException();
}
/**
* Models an X and Y point in a path. The top-left corner of the axis is the point (0,
* 0). This is the lowest point in both the x and y dimensions. Negative points are
* not allowed.
*/
private static class Point {
private int x = 0;
private int y = 0;
private Point predecessor = null;
/**
* Create a new point with the specified coordinates and no predecessor.
*/
public Point(int x, int y) {
this.x = x;
this.y = y;
}
/**
* Creates a new point from this point by shifting its values as specified. The
* new point keeps a reference to its source in order to create a path later.
*/
public Point createDeltaPoint(int deltaX, int deltaY) {
Point result = new Point(x + deltaX, y + deltaY);
result.predecessor = this;
return result;
}
/**
* Shifts x
and y
values down and to the
* right by one.
*/
public Point incrementDiagonally() {
Point result = createDeltaPoint(1, 1);
// shortcut to the predecessor (to save memory!)
if(predecessor != null) {
int deltaX = result.x - predecessor.x;
int deltaY = result.y - predecessor.y;
if(deltaX == deltaY) {
result.predecessor = this.predecessor;
}
}
return result;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public boolean isLessThan(Point other) {
return x < other.x && y < other.y;
}
public boolean isEqualToOrGreaterThan(Point other) {
return x >= other.x && y >= other.y;
}
public String toString() {
return "(" + x + "," + y + ")";
}
/**
* Get a trail from the original point to this point. This is a list of
* all points created via a series of {@link #createDeltaPoint(int,int)}
* calls.
*/
public List trail() {
List reverse = new ArrayList();
Point current = this;
while (current != null) {
reverse.add(current);
current = current.predecessor;
}
Collections.reverse(reverse);
return reverse;
}
}
/**
* Determines if the values at the specified points match or not.
*
* This class specifies that each element should specify a character value.
* This is for testing and debugging only and it is safe for implementing
* classes to throw {@link UnsupportedOperationException} for both the
* {@link #alphaAt(int)} and {@link #betaAt(int)} methods.
*/
interface DiffMatcher {
public int getAlphaLength();
public int getBetaLength();
public boolean matchPair(int alphaIndex, int betaIndex);
/**
* Output a character representing the specified element, for
* the convenience of testing.
*/
public char alphaAt(int index);
public char betaAt(int index);
}
/**
* Matcher for Lists.
*/
static class ListDiffMatcher implements DiffMatcher {
private List alpha;
private List beta;
private Comparator comparator;
public ListDiffMatcher(List alpha, List beta, Comparator comparator) {
this.alpha = alpha;
this.beta = beta;
this.comparator = comparator;
}
public int getAlphaLength() {
return alpha.size();
}
public char alphaAt(int index) {
return alpha.get(index).toString().charAt(0);
}
public char betaAt(int index) {
return beta.get(index).toString().charAt(0);
}
public int getBetaLength() {
return beta.size();
}
public boolean matchPair(int alphaIndex, int betaIndex) {
return (comparator.compare(alpha.get(alphaIndex), beta.get(betaIndex)) == 0);
}
}
}