/* * Copyright (C) 2011 Laurent Caillette * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation, either * version 3 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even 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, see <http://www.gnu.org/licenses/>. */ package org.novelang.common.tree; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import com.google.common.base.Preconditions; import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.collect.AbstractIterator; import static com.google.common.base.Preconditions.checkNotNull; /** * Manipulation of immutable {@link Tree}s through {@link Treepath}s. * <p> * Convention for diagrams: the star (*) marks the {@link Tree} * objects referenced by the {@code Treepath} object. * The apostrophe (') marks new {@link Tree} objects * created for reflecting the new state of a logically "modified" Tree. * * @author Laurent Caillette */ public class TreepathTools { private TreepathTools() { throw new Error( "Don't call" ) ; } /** * Returns the n<sup>th</sup> sibling starting from end's parent. * {@link Treepath}. * <pre> * getEndChildAt( t1, 2 ): * * *t0 *t0 * / | \ / | \ * *t1 t2 t3 --> t1 t2 *t3 * </pre> * @param treepath non-null object with minimum depth of 2. * @param index inside [ 0, child count of previous tree [ * @return non-null object. */ public static< T extends Tree< T > > Treepath< T > getSiblingAt( final Treepath< T > treepath, final int index ) throws IllegalArgumentException { final T end = treepath.getTreeAtEnd() ; Preconditions.checkArgument( treepath.getLength() > 1, "length of treepath [%s] should be greater than 1" ) ; Preconditions.checkArgument( index >= 0, "index [%s] should be 0 or greater", index ) ; Preconditions.checkArgument( treepath.getPrevious().getTreeAtEnd().getChildCount() > index, "child count [%s] should be greater than index [%s]", end.getChildCount(), index ) ; return Treepath.create( treepath.getPrevious().getTreeAtEnd(), index ) ; } /** * Returns true if given {@code Treepath} has a previous sibling, false otherwise. * @param treepath a non-null {@code Treepath} with a minimum length of 2. * @throws IllegalArgumentException * @see #getPreviousSibling(Treepath) */ public static< T extends Tree< T > > boolean hasPreviousSibling( final Treepath< T > treepath ) throws IllegalArgumentException { if( treepath.getLength() < 2 ) { throw new IllegalArgumentException( "Treepath must have minimum length of 2" ) ; } return treepath.getIndexInPrevious() > 0 ; } /** * Returns true if given {@code Treepath} has a next sibling, false otherwise. * @param treepath a non-null {@code Treepath} with a minimum length of 2. * This may seem an excessive constraint but helps to detect many logical problems. * @throws IllegalArgumentException * @see #getNextSibling(Treepath) */ public static< T extends Tree< T > > boolean hasNextSibling( final Treepath< T > treepath ) throws IllegalArgumentException { if( treepath.getLength() < 2 ) { throw new IllegalArgumentException( "Treepath must have minimum length of 2" ) ; } return treepath.getIndexInPrevious() < treepath.getPrevious().getTreeAtEnd().getChildCount() - 1 ; } /** * Returns the sibling on the left of the bottom of given * {@link Treepath}. * <pre> * *t0 *t0 * | \ | \ * t1 *t2 --> *t1 t2 * </pre> * @param treepath non-null object with minimum length of 2. * @return non-null object. * @see #hasPreviousSibling(Treepath) */ public static< T extends Tree< T > > Treepath< T > getPreviousSibling( final Treepath< T > treepath ) throws IllegalArgumentException { Preconditions.checkArgument( hasPreviousSibling( treepath ) ) ; final Treepath< T > previousTreepath = treepath.getPrevious() ; return Treepath.create( previousTreepath, treepath.getIndexInPrevious() - 1 ) ; } /** * Returns the sibling on the left of the end of given {@link Treepath}. * <pre> * *t0 *t0 * / | / | * *t1 t2 --> t1 *t2 * </pre> * @param treepath non-null object with minimum length of 2. * @return non-null object. * @see #hasNextSibling(Treepath) */ public static< T extends Tree< T > > Treepath< T > getNextSibling( final Treepath< T > treepath ) { Preconditions.checkArgument( hasNextSibling( treepath ) ) ; final Treepath< T > previousTreepath = treepath.getPrevious() ; return Treepath.create( previousTreepath, treepath.getIndexInPrevious() + 1 ) ; } /** * Adds a sibling on the right of end of given {@link Treepath}. * <pre> * *t0 *t0' * | | \ * *t1 --> *t1 t2 * </pre> * * @param treepath non-null object. * @param tree non-null object. * @return non-null {@code Treepath} with the same end but with updated parents. * */ public static< T extends Tree< T > > Treepath< T > addSiblingLast( final Treepath< T > treepath, final T tree ) { Preconditions.checkArgument( treepath.getLength() > 1, "Minimum length is 2, got %s", treepath.getLength() ) ; final T oldParent = treepath.getTreeAtDistance( 1 ) ; final T newParent = TreeTools.addLast( oldParent, tree ) ; return Treepath.create( replaceTreepathEnd( treepath.getPrevious(), newParent ), newParent.getChildCount() - 1 ) ; } /** * Adds a first child to the end of given {@link Treepath}. * <pre> * *t0 *t0' * | | * *t1 --> *t1' * | / | * t2 *new t2 * </pre> * * @param treepath non-null object. * @param tree non-null object. * @return non-null {@code Treepath} referencing updated trees. * */ public static < T extends Tree< T > > Treepath< T > addChildFirst( final Treepath< T > treepath, final T tree ) { Preconditions.checkArgument( treepath.getLength() > 0, "Minimum length is 1, got %s", treepath.getLength() ) ; final T newParent = TreeTools.addFirst( treepath.getTreeAtEnd(), tree ) ; return Treepath.create( replaceTreepathEnd( treepath, newParent ), newParent.getChildCount() - 1 ) ; } /** * Adds a child at given position on the end of given {@link Treepath}. * <pre> * *t0 *t0' * | | * *t1 --> *t1' * / \ / | \ * t2 t3 t2 *new t3 * </pre> * * @param treepath non-null object. * @param tree non-null object. * @param position position of added {@code tree}. * @return non-null {@code Treepath} referencing updated trees. * */ public static < T extends Tree< T > > Treepath< T > addChildAt( final Treepath< T > treepath, final T tree, final int position ) { Preconditions.checkArgument( treepath.getLength() > 0, "Minimum length is 1, got ", treepath.getLength() ) ; final T newParent = TreeTools.add( treepath.getTreeAtEnd(), tree, position ) ; return Treepath.create( replaceTreepathEnd( treepath, newParent ), position ) ; } /** * Adds a last child to the end of given {@link Treepath}. * <pre> * *t0 *t0' * | | * *t1 --> *t1' * | | \ * t2 t2 *new * </pre> * * @param treepath non-null object. * @param tree non-null object. * @return non-null {@code Treepath} referencing updated trees. * */ public static < T extends Tree< T > > Treepath< T > addChildLast( final Treepath< T > treepath, final T tree ) { Preconditions.checkArgument( treepath.getLength() > 0, "Minimum length is 1, got %s", treepath.getLength() ) ; final T newParent = TreeTools.addLast( treepath.getTreeAtEnd(), tree ) ; return Treepath.create( replaceTreepathEnd( treepath, newParent ), newParent.getChildCount() - 1 ) ; } /** * Returns a {@link Treepath} corresponding to a replacement of the end of the * given {@link Treepath}. * <pre> * *t0 *t0' * | | * *t1 --> *t1' * | | * *old *new * </pre> * * @param treepath non-null object. * @param newTree non-null object. * @return non-null {@code Treepath} with the same end referencing updated trees. * */ public static< T extends Tree< T > > Treepath< T > replaceTreepathEnd( final Treepath< T > treepath, final T newTree ) { if( null == treepath.getPrevious() ) { return Treepath.create( newTree ) ; } else { final Treepath< T > parentTreepath = treepath.getPrevious() ; final T newParent = TreeTools.replace( parentTreepath.getTreeAtEnd(), treepath.getIndexInPrevious(), newTree ) ; return Treepath.create( replaceTreepathEnd( parentTreepath, newParent ), treepath.getIndexInPrevious() ) ; } } /** * Removes the end of a given {@code Treepath}. * * @param treepath a non-null object with a minimum height of 2. * @return a {@code Treepath} referencing updated trees. */ public static< T extends Tree< T > > Treepath< T > removeEnd( final Treepath< T > treepath ) { Preconditions.checkArgument( treepath.getLength() > 1, "Treepath length must be 2 or more" ) ; final T removed = treepath.getTreeAtEnd() ; final T parentOfRemoved = treepath.getTreeAtDistance( 1 ) ; T newTree = null ; for( int i = 0 ; i < parentOfRemoved.getChildCount() ; i++ ) { final Tree child = parentOfRemoved.getChildAt( i ) ; if( child == removed ) { newTree = TreeTools.remove( parentOfRemoved, i ) ; break ; } } if( null == newTree ) { throw new Error( "Internal error: found no end" ) ; } return replaceTreepathEnd( treepath.getPrevious(), newTree ) ; } /** * Removes a {@code Tree} from its direct parent, and adds it as child of its former * previous sibling. * <pre> * *t0 *t0' * | \ | * t1 *t2 --> *t1' * | * *t2 * </pre> * * @param targetTreepath non-null, minimum depth of 2. * @return non-null object representing path to moved {@code Tree}. * @throws IllegalArgumentException if there was no previous sibling. */ public static < T extends Tree< T > > Treepath< T > becomeLastChildOfPreviousSibling( final Treepath< T > targetTreepath ) throws IllegalArgumentException { final T moving = targetTreepath.getTreeAtEnd() ; final Treepath< T > previousSibling = getPreviousSibling( targetTreepath ) ; final Treepath< T > afterRemoval = removeEnd( targetTreepath ) ; return addChildLast( Treepath.create( afterRemoval, previousSibling.getIndexInPrevious() ), moving ) ; } /** * Removes the previous sibling of end of given {@code Treepath}. * <pre> * *t0 t0' * | \ | * t1 *t2 --> *t2' * </pre> * * @param treepath non-null, minimum depth of 2. * @return non-null object representing path to moved {@code Tree}. * @throws IllegalArgumentException if there was no previous sibling. */ public static< T extends Tree< T > > Treepath< T > removePreviousSibling( final Treepath< T > treepath ) { Preconditions.checkArgument( treepath.getLength() >= 2, "Treepath length must be 2 or more" ) ; Preconditions.checkArgument( hasPreviousSibling( treepath ), "Tree at end of treepath must have a previous sibling" ) ; final int indexOfPreviousSibling = treepath.getIndexInPrevious() - 1 ; final T parentBeforeRemoval = treepath.getTreeAtDistance( 1 ) ; final T parentAfterRemoval = TreeTools.remove( parentBeforeRemoval, indexOfPreviousSibling ) ; final Treepath< T > treepathToParentWithRemoval = replaceTreepathEnd( treepath.getPrevious(), parentAfterRemoval ) ; return Treepath.create( treepathToParentWithRemoval, treepath.getIndexInPrevious() - 1 ) ; } /** * Removes the next sibling at end of given {@code Treepath}. * <pre> * *t0 t0' * | \ | * *t1 t2 --> *t1' * </pre> * * @param treepath non-null, minimum depth of 2. * @return non-null object representing path to moved {@code Tree}. * @throws IllegalArgumentException if there was no previous sibling. */ public static< T extends Tree< T > > Treepath< T > removeNextSibling( final Treepath< T > treepath ) { if( treepath.getLength() < 2 ) { throw new IllegalArgumentException( "Treepath length must be 2 or more" ) ; } if( ! hasNextSibling( treepath )) { throw new IllegalArgumentException( "Tree at end of treepath must have a next sibling" ) ; } final int indexOfNextSibling = treepath.getIndexInPrevious() + 1 ; final T parentBeforeRemoval = treepath.getTreeAtDistance( 1 ) ; final T parentAfterRemoval = TreeTools.remove( parentBeforeRemoval, indexOfNextSibling ) ; final Treepath< T > treepathToParentWithRemoval = replaceTreepathEnd( treepath.getPrevious(), parentAfterRemoval ) ; return Treepath.create( treepathToParentWithRemoval, treepath.getIndexInPrevious() ) ; } /** * Removes a subtree of a container tree, while retaining consistency of a {@code Treepath} * to somewhere in the container tree. * Both {@code Treepath} are supposed to have the same root (tested by reference equality). * <pre> * -*t0 *t0' -*t0 *t0' -*t0 * | | | | | * -*t1 --> *t1' -*t1 --> *t1' -*t1 --> exception * / \ | | | * -t2 *t3 *t3 -t2 *t2 * * * : treepath to some element * - : treepath representing subtree * </pre> * * @param containerTreepath a {@code Treepath} where the tree at start is the tree to remove from. * @param subTreepath a {@code Treepath} where the tree at end is the subtree to remove. * @return a {@code Treepath} with the same start and end tree, but with subtree removed. * @throws IllegalArgumentException if the subtree is containing the tree it is supposed to be * removed from, or if both treepaths don't have the same tree at start. */ public static< T extends Tree< T > > Treepath< T > removeSubtree( final Treepath< T > containerTreepath, final Treepath< T > subTreepath ) { Preconditions.checkArgument( containerTreepath.getTreeAtStart() == subTreepath.getTreeAtStart() ) ; Preconditions.checkArgument( subTreepath.getLength() > 1 ) ; final Iterator< Treepath< T > > invertedPathForContainer = invert( containerTreepath ).iterator() ; final Iterator< Treepath< T > > invertedPathForSub = invert( subTreepath ).iterator() ; invertedPathForContainer.next() ; invertedPathForSub.next() ; RemovalProgress progress = RemovalProgress.UNSPLIT ; Treepath< T > result = TreepathTools.removeEnd( subTreepath ).getStart() ; while( invertedPathForContainer.hasNext() ) { final Treepath< T > currentTreepathInContainer = invertedPathForContainer.next() ; final Treepath< T > currentTreepathInSub = invertedPathForSub.next() ; if( currentTreepathInContainer.getTreeAtEnd() == currentTreepathInSub.getTreeAtEnd() ) { if( ! invertedPathForSub.hasNext() ) { throw new IllegalArgumentException( "The subtree entierely contains the containing treepath" ) ; } result = Treepath.create( result, currentTreepathInContainer.getIndexInPrevious() ) ; } else { if( RemovalProgress.UNSPLIT == progress ) { if( currentTreepathInContainer.getIndexInPrevious() > currentTreepathInSub.getIndexInPrevious() ) { progress = RemovalProgress.REMOVAL_ON_LEFT ; } else { progress = RemovalProgress.REMOVAL_ON_RIGHT ; } } switch( progress ) { case REMOVAL_ON_LEFT : result = Treepath.create( result, currentTreepathInContainer.getIndexInPrevious() - 1 ) ; break ; case REMOVAL_ON_RIGHT : case SPLIT : result = Treepath.create( result, currentTreepathInContainer.getIndexInPrevious() ) ; case UNSPLIT: break ; } progress = RemovalProgress.SPLIT ; } } return result ; } public static< T extends Tree< T > > Iterator iteratorOnChildren( final Treepath< T > treepath ) { return iteratorOnChildren( treepath, Predicates.< T >alwaysTrue() ) ; } public static< T extends Tree< T > > Iterator< Treepath< T > > iteratorOnChildren( final Treepath< T > treepath, final Predicate< T > filter ) { checkNotNull( filter ) ; final T treeAtEnd = treepath.getTreeAtEnd() ; final int childCount = treeAtEnd.getChildCount() ; return new AbstractIterator< Treepath< T > >() { private int childIndex = 0 ; @Override protected Treepath< T > computeNext() { while( true ) { if( childIndex >= childCount ) { endOfData() ; return null ; } final int childIndexForCreation = childIndex ++ ; final T child = treeAtEnd.getChildAt( childIndexForCreation ) ; if( filter.apply( child ) ) { return Treepath.create( treepath, childIndexForCreation ) ; } } } } ; } private enum RemovalProgress { UNSPLIT, REMOVAL_ON_LEFT, REMOVAL_ON_RIGHT, SPLIT } private static< T extends Tree< T > > Iterable< Treepath< T > > invert( Treepath< T > treepath ) { final List< Treepath< T > > treepaths = new ArrayList< Treepath< T > >( treepath.getLength() ) ; while( true ) { treepaths.add( treepath ) ; final Treepath< T > previous = treepath.getPrevious(); if( null == previous ) { break ; } else { treepath = previous ; } } Collections.reverse( treepaths ) ; return treepaths ; } /** * Returns if a {@code Treepath} has the same indices in its parenthood as another * {@code Treepath}, for the length they have in common. * * @param maybeParent a non-null object. * @param maybeChild a non-null object with {@link Treepath#getLength()} greater than the * one of {@code maybeparent}. * @return true if index in each parent tree is the same. */ public static < T extends Tree< T > > boolean hasSameStartingIndicesAs( final Treepath< T > maybeParent, Treepath< T > maybeChild ) { checkNotNull( maybeParent ) ; checkNotNull( maybeChild ) ; Preconditions.checkArgument( maybeParent.getLength() <= maybeChild.getLength() ) ; while( maybeChild.getLength() > maybeParent.getLength() ) { maybeChild = maybeChild.getPrevious() ; } return hasSameStartingIndicesAsWithoutCheck( maybeParent, maybeChild ) ; } private static < T extends Tree< T > > boolean hasSameStartingIndicesAsWithoutCheck( final Treepath< T > maybeParent, final Treepath< T > maybeChild ) { if( maybeParent.getPrevious() == null ) { return true ; // No check needed as both arguments are supposed to have the same length. } else { if( maybeParent.getIndexInPrevious() == maybeChild.getIndexInPrevious() ) { return hasSameStartingIndicesAsWithoutCheck( maybeParent.getPrevious(), maybeChild.getPrevious() ) ; } else { return false ; } } } }