Class Java

/**
 * EquivalenceClassSet.java
 * 
 *  @author bret5
 *  Copyright(C) 2007 bret5
 *  
 *  This is a specialized set class which groups the elements into
 *  equivalence classes based on the by the comparator provided at 
 *  set creation time.  Iterators created thru the standard set interface
 *  will return elements sorted by comparator order.  Elements that
 *  are equivalent may be returned in any order.
 *  
 *  The class also provides a special loopIterator iteration 
 *  which will return each element once in comparator order, then loop back
 *  around to the first element at the end.  The loopIterator continues to be valid
 *  as add/remove operations are performed.  If shuffleEquivalenceClasses is set,
 *  it will randomly shuffles the elements in each equivalence class 
 *  every time that equivalence class is reached during iteration.  If not,
 *  each equivalence class will be returned in the same order every time.
 *  shuffleEquivalenceClasses defaults to true.
 *  
 *  This Set does not allow null elements.
 *  
 *  This was written for a program that uses jdk 1.4, so it doesn't yet use the generic style.  
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 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, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.NoSuchElementException;
/**
 * @author bret5
 *
 */
public class EquivalenceClassSet extends AbstractSet 
{
    // the list backing the class.  This is a list of the 
    // equivalence classes - each equivalence class is a sublist
    // which contains all the elements equivalent to each other.
    // The sublists appear in order corresponding to the ordering
    // provided by the comparator provided at set creation time. 
    private List                m_equivalenceClasses;
    private int                 m_size; // cached for efficiency
    private Comparator          m_comparator;
    private ListIterator        m_loopEqvClassIter; // non-null index used by the loop iterator
    private List                m_loopCurrentEqvClass; // ptr to current class, null ok
    private ListIterator     m_loopItemIter; // non-null index used by the loop iterator
    private final static List   m_emptyList = new ArrayList(); // emptyList used for gen-ing iterators
    
    // In order to maintain some sanity in the face of objects changing with respect to
    // the comparator after being added to the set, we keep track of which class every object
    // is in.  This way, the behavior of contains and remove are undisturbed by changes to the 
    // objects.
    private HashMap         m_itemToClassMap;
    
    // TODO - size is now redundant with the size of the item-class map, remove...
    
    private int m_changeID; // supports iterator fail-fast, increment on each modification
    private boolean m_shuffleEquivalenceClasses;
    
    /*
     * Class invariants:
     *   Individual equivalence classes may not be empty
     *   The cached size is equal to the sum of all items in the sublists
     *   The changeID increments montonically whenever the set contents are changed,
     *   although it does reset on a clear()
     */
    
    public EquivalenceClassSet(Comparator c) 
    {
        super();
        m_comparator = c;
        m_equivalenceClasses = new LinkedList();
        m_size = 0;
        m_itemToClassMap = new HashMap();
        resetLoopIterator();
        m_changeID = 0;
        m_shuffleEquivalenceClasses = true;
    }
    protected class OnePassIterator implements Iterator 
    {
        private int localChangeID;
        private ListIterator localEqvClassIter; // class iter
        private ListIterator localItemIter; // item iter
        protected OnePassIterator() 
        {
            localChangeID = m_changeID;
            localEqvClassIter = m_equivalenceClasses.listIterator(); // index used by the loop iterator
            localItemIter = null; // null means before the next class
        }
        
        public boolean hasNext() 
        {
            if (m_changeID != localChangeID) 
            {
                throw new ConcurrentModificationException();
            }
            
            if (localEqvClassIter.hasNext() || 
                    (localItemIter != null && localItemIter.hasNext())) 
            {
                return true;
            }
            
            return false;
        }
        public T next() 
        {
            if (m_changeID != localChangeID) 
            {
                throw new ConcurrentModificationException();
            }
            
            if (localItemIter == null || !localItemIter.hasNext()) 
            {
                localItemIter = ((List)localEqvClassIter.next()).listIterator();
            }
            
            return localItemIter.next();
        }
        public void remove() 
        {
            throw new UnsupportedOperationException();
        }            
    }
    
    public Iterator iterator() 
    {
        return new OnePassIterator();
    }
    
    // Note that this iterator is intended to continue iteration after element addition 
    // and removal.  Therefore we put the underlying iterators in the main class so that 
    // they can be adjusted/replaced when necessary
    // invariants:
    //   if size = 0, currentEqvClassIter is null.
    //   if size not null, then both currentEqvClassIter and currentItemIter
    //     are not null.
    protected class LoopIterator implements Iterator 
    {
        public boolean hasNext() 
        {
            return ( m_size > 0 );
        }
        public T next() 
        {
            assert m_loopEqvClassIter != null;
            assert m_loopItemIter != null;
            
            if (m_size <= 0) 
            {
                throw new NoSuchElementException();
            }
            
            // first, move to a new class if necessary, resetting the item iter.
            // if switching classes, shuffle.
            if (!m_loopItemIter.hasNext()) 
            {
                if (!m_loopEqvClassIter.hasNext()) 
                {
                    // recycle to beginning
                    m_loopEqvClassIter = m_equivalenceClasses.listIterator();
                }
                
                m_loopCurrentEqvClass = (List)m_loopEqvClassIter.next();
                assert m_loopCurrentEqvClass.size() > 0;
                
                if (m_shuffleEquivalenceClasses) 
                {
                    java.util.Collections.shuffle(m_loopCurrentEqvClass);
                }
                m_loopItemIter = m_loopCurrentEqvClass.listIterator();                
            }
            
            return m_loopItemIter.next();
        }
        public void remove() 
        {
            throw new UnsupportedOperationException();
        }                               
    }
    
    /**
     * The loopIterator is a special iteration for which hasNext() returns 
     * true if the size is greater than 0.  The next() method 
     * traverses the equivalence classes in comparator order.  Within
     * each equivalence class, the items are returned randomly 
     * (by shuffling the elements in the equivalence class every time 
     * that equivalence class is reached during iteration).
     * 
     * Iteration can be reset to the first equivalence class by using
     * the resetLoopIterator method of the main class.
     *   
     * @return the iterator
     */
    public Iterator loopIterator() 
    {
        return new LoopIterator();
    }
   
    public void resetLoopIterator() 
    {
        m_loopEqvClassIter = m_equivalenceClasses.listIterator();
        m_loopCurrentEqvClass = null;
        m_loopItemIter = m_emptyList.listIterator();        
    }
    
    /**
     * If shuffleEquivalenceClasses is set, the loopItertor will randomly shuffle 
     * the elements in each equivalence class every time that equivalence class 
     * is reached during iteration.
     * 
     * @return the value of shuffleEquivalenceClasses
     */
    public boolean isShuffleEquivalenceClasses() 
    {
        return m_shuffleEquivalenceClasses;
    }
    /**
     * Set the value of shuffleEquivalenceClasses.
     * @see isShuffleEquivalenceClasses()
     */
    public void setShuffleEquivalenceClasses(boolean shuffleEquivalenceClasses) 
    {
        this.m_shuffleEquivalenceClasses = shuffleEquivalenceClasses;
    }
    /* (non-Javadoc)
     * @see java.util.AbstractCollection#size()
     */
    public int size() 
    {
        return m_size;
    }
    /* 
     * Adds the argument to the collection.  If the element is not already
     * a member of the set, and is equivalent to the current equivalence 
     * class, it is added so that it will be returned before any element
     * from another class is returned.
     */
    public boolean add(T arg0) 
    {
        return addPositional(arg0, true);
    }
    /* 
     * Adds the argument to the collection.  If the element is not already
     * a member of the set, and is equivalent to the current equivalence 
     * class, it is added so that it will not be returned before an element
     * from another class is returned (if there are any other classes).
     */
    public boolean addExpired(T arg0) 
    {
        return addPositional(arg0, false);
    }
    protected boolean addPositional(Object arg0, boolean atEnd) 
    {
        EqvPosition eqvPosition = findEqvClass(arg0);
        boolean isChanged = false;
        if (eqvPosition.matchingEqvClass != null) 
        {
            int iterIdx = 0;
            boolean replaceLoopItemIter = false;
            
            if (eqvPosition.matchingEqvClass == m_loopCurrentEqvClass) 
            {
                // we have to replace the item loop iterator for this class, so get the position
                replaceLoopItemIter = true;
                iterIdx = m_loopItemIter.nextIndex();
            }
            
            if (!eqvPosition.matchingEqvClass.contains(arg0)) 
            {
                if (atEnd) 
                {
                    eqvPosition.matchingEqvClass.add(arg0);
                } 
                else
                {
                    eqvPosition.matchingEqvClass.add(0, arg0);
                    iterIdx += 1;
                }
                
                isChanged = true;
                
                if (replaceLoopItemIter) 
                {
                    m_loopItemIter = m_loopCurrentEqvClass.listIterator(iterIdx);
                }
            }
        } 
        else 
        {
            // there is no matching class, so add one
            ArrayList newEqvClass = new ArrayList();
            newEqvClass.add(arg0);
            eqvPosition.matchingEqvClass = newEqvClass;  // cache the eqv class ref for adding to map
            int iterIdx, addIdx;
            iterIdx = 0;
            addIdx = 0;
            
            if (m_size >= 1) 
            {
                iterIdx = m_loopEqvClassIter.nextIndex();
                addIdx = eqvPosition.eqvClassIter.nextIndex();
                if (addIdx < iterIdx) 
                {
                    iterIdx += 1;
                }
            } 
            eqvPosition.eqvClassIter.add(newEqvClass);                
            isChanged = true;
            
            // replace the class loop iterator
            m_loopEqvClassIter = m_equivalenceClasses.listIterator(iterIdx);
            
            // if the new class is next and the add is "expired"/(not atEnd), 
            // advance the iterator past the just added item
            if (iterIdx == addIdx && !atEnd && !m_loopItemIter.hasNext()) 
            {
                m_loopCurrentEqvClass = (List)m_loopEqvClassIter.next();
                m_loopItemIter = m_loopCurrentEqvClass.listIterator(1);
            }
        }
        
        if (isChanged) 
        {
            m_size += 1;
            m_changeID += 1;
            m_itemToClassMap.put(arg0, eqvPosition.matchingEqvClass);
        }
        
        return isChanged;
    }
    // represents the location of the equivalence class matching a value.
    // returned from findEqvClass, so we only have to write that code once.
    // If there is a matching class, then matchingEqvClass will be non-null.
    // If not, then eqvClassIter holds the position that eqv class would have.
    protected class EqvPosition 
    {
        protected List matchingEqvClass;
        protected ListIterator eqvClassIter;
    }
    // If there is a matching class, then matchingEqvClass will be non-null.
    // If not, then eqvClassIter holds the position that eqv class would have.
    protected EqvPosition findEqvClass(Object arg0) 
    {
        EqvPosition eqvPosition = new EqvPosition();
        if (m_itemToClassMap.containsKey(arg0)) 
        {
            eqvPosition.matchingEqvClass = (List)m_itemToClassMap.get(arg0);
            return eqvPosition;  // note that the iterator will be null.
        }
        
        eqvPosition.eqvClassIter = m_equivalenceClasses.listIterator();
        while (eqvPosition.eqvClassIter.hasNext()) 
        {
            List testEqvClass = (List)eqvPosition.eqvClassIter.next();
            assert testEqvClass.size() > 0;
            int comparison = m_comparator.compare(arg0, testEqvClass.get(0));
            if (comparison < 0) 
            {
                // there is no matching class, to insert before this one, return the previous position
                if (eqvPosition.eqvClassIter.hasPrevious()) 
                {
                    eqvPosition.eqvClassIter.previous();
                }
                break;
            } 
            else if (comparison == 0) 
            {
                eqvPosition.matchingEqvClass = testEqvClass;
                return eqvPosition; 
            }
            // and fall through to the next eqvClass
        }
        return eqvPosition;
    }
    
    /* (non-Javadoc)
     * @see java.util.AbstractCollection#clear()
     */
    public void clear() 
    {
        m_equivalenceClasses.clear();
        m_itemToClassMap.clear();
        m_size = 0;
        m_changeID = 0; // can reset to original state
        resetLoopIterator();
    }
    /* (non-Javadoc)
     * @see java.util.AbstractCollection#contains(java.lang.Object)
     */
    public boolean contains(Object arg0) 
    {
        EqvPosition eqvPosition = findEqvClass(arg0);
        if (eqvPosition.matchingEqvClass != null &&
                eqvPosition.matchingEqvClass.contains(arg0)) 
        {
            return true; // already a member, do nothing
        }   
        return false;
    }
    /* (non-Javadoc)
     * @see java.util.AbstractCollection#remove(java.lang.Object)
     */
    public boolean remove(Object arg0) 
    {
        assert m_size == m_itemToClassMap.size();
        EqvPosition eqvPosition = findEqvClass(arg0);
        return removeAtPosition(eqvPosition, arg0);
    }
    /* (non-Javadoc)
     * @see java.util.AbstractCollection#remove(java.lang.Object)
     */
    private boolean removeAtPosition(EqvPosition eqvPosition, Object arg0) 
    {
        // when removing an object, we may have to replace the item loop iterator
        // if we removed an item from the current loop class
        // also, if the removal results in removal of a class, we may have to 
        // replace the class loop iter 
        boolean isChanged = false;
        boolean replaceLoopItemIter = false;
        if (eqvPosition.matchingEqvClass != null) 
        {
            int itemLocationIdx = 0;
            int loopNextItemIdx = 0;
            int itemClassIdx = 0;
            int loopNextClassIdx = 0;
            itemLocationIdx = eqvPosition.matchingEqvClass.indexOf(arg0);
            if (itemLocationIdx >= 0) 
            {
                // the item is a member of this class and will be removed 
                isChanged = true; 
                if (eqvPosition.matchingEqvClass == m_loopCurrentEqvClass) 
                {
                    // we may have to replace the item loop iterator for this class, so get the position
                    replaceLoopItemIter = true;
                    loopNextItemIdx = m_loopItemIter.nextIndex();
                    if (itemLocationIdx < loopNextItemIdx) 
                    {
                        loopNextItemIdx -= 1;
                    }
                }
                eqvPosition.matchingEqvClass.remove(arg0);
                if (eqvPosition.matchingEqvClass.size() <= 0) 
                {
                    // the class is now empty, remove it
                    loopNextClassIdx = m_loopEqvClassIter.nextIndex();
                    itemClassIdx = m_equivalenceClasses.indexOf(eqvPosition.matchingEqvClass);
                    if (itemClassIdx < loopNextClassIdx) 
                    {
                        loopNextClassIdx -= 1;
                    }
                    
                    m_equivalenceClasses.remove(eqvPosition.matchingEqvClass);
                    
                    // and replace the loop iterator, and maybe the item iterator 
                    if (m_equivalenceClasses.size() == 0) 
                    {
                        resetLoopIterator();
                    } 
                    else 
                    {                        
                        m_loopEqvClassIter = m_equivalenceClasses.listIterator(loopNextClassIdx);
                        if (eqvPosition.matchingEqvClass == m_loopCurrentEqvClass) 
                        {
                            m_loopCurrentEqvClass = null;
                            m_loopItemIter = m_emptyList.listIterator(); 
                        }                        
                    }
                } 
                else if (replaceLoopItemIter) 
                {
                    // replace the item iterator for the class
                    m_loopItemIter = m_loopCurrentEqvClass.listIterator(loopNextItemIdx); 
                }   
            }
        }
        
        if (isChanged) 
        {
            m_itemToClassMap.remove(arg0);
            m_size -= 1;
            assert m_size >= 0;
            assert m_size == m_itemToClassMap.size();
            m_changeID += 1;
        }
        
        return isChanged;
    }
    
    public Comparator getComparator() 
    {
        return m_comparator;
    }
    
    /**
     * Partition this set into two sets, returning the new one.
     * 
     * The argument specifies how many elements to put in the new set.  Elements will be 
     * chosen in comparator order.  All elements put into the new set will be removed from 
     * this set.  If the original set contained less elements than the argument, then
     * after the partition the new set will contain all the elements and the original set
     * will be empty.
     *  
     * If the partition is non-trivial (that is, if the new set contains at least one 
     * element), then the counters for the loop iterator will be reset.
     *  
     * @param numberToRemove number of elements to remove from the original set
     * 
     * @return the new set
     */
    public EquivalenceClassSet partition(int numberToRemove) 
    {
        EquivalenceClassSet newSet = new EquivalenceClassSet(m_comparator);
        while (numberToRemove > 0 && m_size > 0) 
        {
            ArrayList firstEqvClass = (ArrayList)(m_equivalenceClasses.get(0));
            int sizeOfFEqvClass = firstEqvClass.size();
            int numberMoved = 0;
            List movedEqvClass;
            if (numberToRemove >= sizeOfFEqvClass) 
            {
                movedEqvClass = (List)m_equivalenceClasses.remove(0);
                newSet.m_equivalenceClasses.add(movedEqvClass);
                numberMoved = sizeOfFEqvClass;
                
            } 
            else 
            {
                // shuffle the equivalence class prior to a partial selection
                if (m_shuffleEquivalenceClasses) 
                {
                    java.util.Collections.shuffle(firstEqvClass);
                }
                
                movedEqvClass = new ArrayList(firstEqvClass.subList(0, numberToRemove));
                firstEqvClass.subList(0, numberToRemove).clear();
                newSet.m_equivalenceClasses.add(movedEqvClass);
                numberMoved = numberToRemove;
            }
            
            m_size -= numberMoved;
            newSet.m_size += numberMoved;
            numberToRemove -= numberMoved;
            
            // now fix up the item to class map
            Iterator iter = movedEqvClass.iterator();
            while (iter.hasNext()) 
            {
                Object obj = iter.next();
                m_itemToClassMap.remove(obj);                
                newSet.m_itemToClassMap.put(obj, movedEqvClass);                
            }
        }
        
        if (newSet.size() > 0) 
        {
            newSet.resetLoopIterator();
            resetLoopIterator();
            m_changeID += 1;
            newSet.m_changeID += 1;
        }
        assert m_size == m_itemToClassMap.size(); 
        assert newSet.m_size == newSet.m_itemToClassMap.size(); 
        return newSet;
    }
    
    /**
     * In some cases, the equivalence class of an object will change.  This can leave
     * the list in an inconsistent state.  It is essential to fix the problem. 
     * This method moves the object to the correct equivalence class, keeping the 
     * loopIterator where it was.
     * 
     * @param arg0 The object to move.
     * @return true if the object is a member of the set, false otherwise.
     */
    public boolean resetEquivalenceClass(T arg0) 
    {
        boolean found = false;
        EqvPosition eqvPosition = new EqvPosition();
        eqvPosition.matchingEqvClass = (List)m_itemToClassMap.get(arg0);
        
        if (eqvPosition.matchingEqvClass != null) 
        {
            removeAtPosition(eqvPosition, arg0);
            found = true;
        }
        
        if (found) 
        {
            addExpired(arg0);
        }
        
        return found;
    }
}