package org.tessell.util;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
/**
* Compares new/old versions of lists, e.g. for firing add/remove events.
*/
public class ListDiff<T> {
/** Maps one type to another (e.g. DTO to model, or model to view, or string to int). */
public interface Mapper<A, B> {
B map(A a);
}
/** A minimal list interface that we is all we need for {@link #apply(List)}. */
public interface ListLike<A> {
A remove(int index);
void add(int index, A a);
}
/** @returns a diff of {@code oldValue} and {@code newValue}. */
public static <T> ListDiff<T> of(List<T> oldValue, List<T> newValue) {
List<Location<T>> added = new ArrayList<Location<T>>();
List<Location<T>> moves = new ArrayList<Location<T>>();
List<Location<T>> removed = new ArrayList<Location<T>>();
if (oldValue == null && newValue != null) {
int i = 0;
for (T t : newValue) {
added.add(new Location<T>(t, i++, -1));
}
} else if (oldValue != null && newValue == null) {
for (T t : oldValue) {
removed.add(new Location<T>(t, 0, -1));
}
} else if (oldValue != null && newValue != null) {
// We make oldCopy/newCopy so that we can call .remove on
// the copies (a destructive operation) so that we'll detect
// multiple entries of primitives/value objects.
List<T> oldCopy = new ArrayList<T>(oldValue);
// First find removals, and make oldCopy contain only newValues
List<T> newCopy = new ArrayList<T>(newValue);
for (Iterator<T> i = oldCopy.iterator(); i.hasNext();) {
T t = i.next();
if (!newCopy.remove(t)) {
removed.add(new Location<T>(t, oldCopy.indexOf(t), -1));
i.remove();
}
}
List<T> withRemoves = new ArrayList<T>(oldCopy);
for (T t : newValue) {
if (!oldCopy.remove(t)) {
// we didn't find in old, so it's new
int newIndex = newValue.indexOf(t);
added.add(new Location<T>(t, newIndex, -1));
// keep withRemoves up to date
withRemoves.add(newIndex, t);
} else {
// we did find in old, but it might have moved
int oldIndex = withRemoves.indexOf(t);
int newIndex = newValue.indexOf(t);
if (oldIndex != newIndex) {
moves.add(new Location<T>(t, newIndex, oldIndex));
// keep withRemoves up to date
withRemoves.remove(oldIndex);
withRemoves.add(newIndex, t);
}
}
}
}
return new ListDiff<T>(added, moves, removed);
}
public final Collection<Location<T>> added;
public final Collection<Location<T>> moves;
public final Collection<Location<T>> removed;
/** Tracks an element that was not added or moved to a new index in the list. */
public static class Location<T> {
public final T element;
public final int index;
/** The old index of {@code element}, *after* any removals are applied. */
public final int oldIndex;
private Location(T element, int index, int oldIndex) {
this.element = element;
this.index = index;
this.oldIndex = oldIndex;
}
@Override
public String toString() {
return element + "@" + index;
}
}
/** Brings an old list {@code copy} up to date with our new value by applying adds/removes. */
public void apply(List<T> copy) {
// call the overload with an identity mapper
apply(copy, new Mapper<T, T>() {
public T map(T a) {
return a;
}
});
}
public <U> void apply(final List<U> copy, Mapper<T, U> mapper) {
// sigh, no structural typing
apply(new ListLike<U>() {
public U remove(int index) {
return copy.remove(index);
}
public void add(int index, U a) {
copy.add(index, a);
}
}, mapper);
}
public <U> void apply(ListLike<U> copy, Mapper<T, U> mapper) {
// apply any removes
for (Location<T> remove : removed) {
copy.remove(remove.index);
}
// apply any adds
for (Location<T> add : added) {
copy.add(add.index, mapper.map(add.element));
}
// apply any moves
for (Location<T> move : moves) {
copy.add(move.index, copy.remove(move.oldIndex));
}
}
private ListDiff(Collection<Location<T>> added, Collection<Location<T>> moves, Collection<Location<T>> removed) {
this.added = added;
this.removed = removed;
this.moves = moves;
}
@Override
public String toString() {
return added + "; " + moves + "; " + removed;
}
}