/*______________________________________________________________________________ * * org.eidola.util.CollectionDiff * * Part of the Eidola Kernel Reference Implementation * See http://eidola.org for oodles of relevant fun! * *______________________________________________________________________________ * * Copyright 2001 Paul Cantrell * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License version 2, as published by the * Free Software Foundation. See the file LICENSE.html for more information. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY, including the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with * this program; if not, write to the Free Software Foundation, Inc. / 59 Temple * Place, Suite 330 / Boston, MA 02111-1307 / USA. *_______________________________________________________________________________ */ package org.eidola.util; import java.util.*; /** Finds the difference between two ordered lists. Given a pair of lists, ListDiff provides a sequence of steps which will mutate the first list into the second. These steps implement {@link ListDiff.Step}, and include insert, move, and remove. You can ask for the steps using {@link #getSteps()}, or call {@link #applySteps(ListMutator) applySteps} to have the ListDiff walk a {@link ListMutator} through the steps.

ListDiff is useful for situations where some structure (such as a user interface) is shadowing a changing list, and you don't want to rebuild the structure from scratch every time the list changes -- perhaps beacuse the cost of recreating the structure is high, or because you don't want to lose state information about existing elements whose position has changed in the list.

The algorithm ListDiff uses for generating the steps is quite simple. It does not guarantee the shortest possible sequence of steps, but usually comes quite close. @author Paul Cantrell @version [Development version] */ public class ListDiff extends CollectionDiff { /** For testing */ public static void main(String[] args) throws Exception { while(true) { System.out.println(); System.out.println(); java.io.BufferedReader in = new java.io.BufferedReader(new java.io.InputStreamReader(System.in)); List a = new LinkedList(), b = new LinkedList(); System.out.print("Old: "); StringTokenizer st = new StringTokenizer(in.readLine()); while(st.hasMoreTokens()) a.add(st.nextToken()); System.out.print("New: "); st = new StringTokenizer(in.readLine()); while(st.hasMoreTokens()) b.add(st.nextToken()); for(Iterator i = (new ListDiff(a, b)).getSteps(); i.hasNext(); ) System.out.println(i.next()); } } /** Prepares a comparison of two lists. */ public ListDiff(List oldStuff, List newStuff) { super(oldStuff, newStuff, true); this.oldStuff = oldStuff; this.newStuff = newStuff; calculateSteps(); } /** Returns a sequence of steps which transform the old list into the new. * The Iterator returns {@link ListDiff.Step} objects. */ public Iterator/**/ getSteps() { return steps.iterator(); } /** Walks a ListMutator through the sequence of steps which transform the old * list into the new. */ public void applySteps(ListMutator mutator) { for(Iterator i = getSteps(); i.hasNext(); ) { Step step = (Step) i.next(); if(step instanceof Insert) mutator.insert(step.getObject(), step.getIndex()); else if(step instanceof Remove) mutator.remove(step.getObject(), step.getIndex()); else if(step instanceof Move) mutator.move(step.getObject(), step.getIndex(), ((Move) step).getToIndex()); } } /** Returns true if the other object is a list diff comparing equal lists. */ public boolean equals(Object other) { ListDiff otherdiff = (ListDiff) other; return otherdiff.oldStuff.equals(oldStuff) && otherdiff.newStuff.equals(newStuff); } /** A step in a list transformation. */ public abstract class Step { public Step(Object object, int index) { this.index = index; this.object = object; } public Object getObject() { return object; } public int getIndex() { return index; } private Object object; private int index; } /** A list transformation step: insert an object at a given position. * After the step, the length of the list has increased by one, and the * item which was at index is now at index+1. */ public class Insert extends Step { public Insert(Object object, int index) { super(object, index); } public String toString() { return "Insert " + getObject() + " at position " + getIndex(); } } /** A list transformation step: remove an object at a given position. * After the step, the length of the list has decreased by one. */ public class Remove extends Step { public Remove(Object object,int index) { super(object, index); } public String toString() { return "Remove " + getObject() + " at position " + getIndex(); } } /** A list transformation step: move an object from one position to another. * Before the move, object is at index; * after the move, object is at toIndex. * Items in the list between index and toIndex shift * over one position to accomodate the move. */ public class Move extends Step { public Move(Object object, int index, int toIndex) { super(object, index); this.toIndex = toIndex; } public int getToIndex() { return toIndex; } public String toString() { return "Insert " + getObject() + " from position " + getIndex() + " to " + getToIndex(); } private int toIndex; } private void calculateSteps() { // We transform oldWork to look like newWork one step at a time. // At step i, the first i positions of the two lists match. // We could generate a shorter transformation list by being // cleverer, but we're not, and so we don't. steps = new LinkedList(); List oldWork = new ArrayList(oldStuff), newWork = new ArrayList(newStuff); int curPos; for(curPos = 0; curPos < oldWork.size() && curPos < newWork.size(); curPos++) { // Look for next object from new list in old list Object nextObject = newWork.get(curPos); int oldPos = -1; for(int j = curPos; j < oldWork.size(); j++) if(oldWork.get(j).equals(nextObject)) { oldPos = j; break; } if(oldPos == -1) { // Didn't find new object, so insert it steps.add(new Insert(nextObject, curPos)); oldWork.add(curPos, nextObject); } else if(curPos != oldPos) { // Found new object in old list, so move it steps.add(new Move(nextObject, oldPos, curPos)); oldWork.remove(oldPos); oldWork.add(curPos, nextObject); } } // If objects remain only in new, insert them for(; curPos < newWork.size(); curPos++) steps.add(new Insert(newWork.get(curPos), curPos)); // If objects remain only in old, remove them for(int j = curPos; j < oldWork.size(); j++) steps.add(new Remove(oldWork.get(curPos), curPos)); } private List oldStuff, newStuff, steps; }