diff --git a/nb-configuration.xml b/nb-configuration.xml new file mode 100644 index 0000000000..1cbabe5aa2 --- /dev/null +++ b/nb-configuration.xml @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + + + + + JDK_8 + + diff --git a/nbactions.xml b/nbactions.xml new file mode 100644 index 0000000000..b779dba874 --- /dev/null +++ b/nbactions.xml @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + + + run + + jar + + + process-classes + org.codehaus.mojo:exec-maven-plugin:3.5.1:exec + + + + ${exec.vmArgs} -classpath %classpath ${exec.mainClass} ${exec.appArgs} + + org.apache.commons.collections4.list.jmh.TreeListVsIndexedLinkedListBenchmark + java + + + + debug + + jar + + + process-classes + org.codehaus.mojo:exec-maven-plugin:3.5.1:exec + + + -agentlib:jdwp=transport=dt_socket,server=n,address=${jpda.address} + ${exec.vmArgs} -classpath %classpath ${exec.mainClass} ${exec.appArgs} + + org.apache.commons.collections4.list.jmh.TreeListVsIndexedLinkedListBenchmark + java + true + + + + profile + + jar + + + process-classes + org.codehaus.mojo:exec-maven-plugin:3.5.1:exec + + + + ${exec.vmArgs} -classpath %classpath ${exec.mainClass} ${exec.appArgs} + org.apache.commons.collections4.list.jmh.TreeListVsIndexedLinkedListBenchmark + java + + + + diff --git a/pom.xml b/pom.xml index c51e26167d..3ce91c2322 100644 --- a/pom.xml +++ b/pom.xml @@ -86,8 +86,8 @@ org.openjdk.jmh jmh-core - ${commons.jmh.version} - test + 1.37 + jar @@ -382,14 +382,12 @@ org.openjdk.jmh jmh-core - ${commons.jmh.version} - test + 1.37 org.openjdk.jmh jmh-generator-annprocess - ${commons.jmh.version} - test + 1.37 diff --git a/src/main/java/org/apache/commons/collections4/list/ExtendedTreeList.java b/src/main/java/org/apache/commons/collections4/list/ExtendedTreeList.java new file mode 100644 index 0000000000..d8c6ba94f0 --- /dev/null +++ b/src/main/java/org/apache/commons/collections4/list/ExtendedTreeList.java @@ -0,0 +1,284 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.collections4.list; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.Objects; + +/** + * This class extends {@link TreeList}. It provides some methods that are needed + * in benchmarking the actual {@code TreeList} against the + * {@link IndexedLinkedList}. + */ +public class ExtendedTreeList extends TreeList { + + public boolean add(E element) { + super.add(super.size(), element); + return true; + } + + public void addFirst(E element) { + super.add(0, element); + } + + public boolean addAll(int index, Collection coll) { + if (coll.isEmpty()) { + return false; + } + + if (index == super.size()) { + super.addAll(coll); + } else { + super.addAll(index, coll); + } + + return true; + } + + public E removeFirst() { + if (isEmpty()) { + throw new IllegalStateException("removeFirst on empty list"); + } + + return remove(0); + } + + public E removeLast() { + if (isEmpty()) { + throw new IllegalStateException("removeFirst on empty list"); + } + + return remove(size() - 1); + } + + public List subList(int fromIndex, int toIndex) { + subListRangeCheck(fromIndex, + toIndex, + size()); + + return new EnhancedSubList(this, + fromIndex, + toIndex); + } + + public boolean equals(Object object) { + if (object == null) { + return false; + } + + if (!getClass().equals(object.getClass())) { + return false; + } + + List other = (List) object; + + if (size() != other.size()) { + return false; + } + + Iterator iter1 = iterator(); + Iterator iter2 = other.iterator(); + + while (iter1.hasNext() && iter2.hasNext()) { + if (!Objects.equals(iter1.next(), iter2.next())) { + return false; + } + } + + if (iter1.hasNext() || iter2.hasNext()) { + throw new IllegalStateException("Problems with iteration"); + } + + return true; + } + + final class EnhancedSubList implements List { + + /** + * The root list. + */ + private final List root; + + /** + * The parent view. This view cannot be wider than its parent view. + */ + private final EnhancedSubList parent; + + /** + * The offset with regard to the parent view or the root list. + */ + private final int offset; + + /** + * The length of this view. + */ + private int size; + + /** + * The modification count. + */ + private int modCount; + + EnhancedSubList(List root, + int fromIndex, + int toIndex) { + + this.root = root; + this.parent = null; + this.offset = fromIndex; + this.size = toIndex - fromIndex; + } + + @Override + public void clear() { + for (int i = 0; i < size; ++i) { + parent.remove(offset); + } + } + + @Override + public int size() { + throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody + } + + @Override + public boolean isEmpty() { + throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody + } + + @Override + public boolean contains(Object o) { + throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody + } + + @Override + public Iterator iterator() { + throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody + } + + @Override + public Object[] toArray() { + throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody + } + + @Override + public T[] toArray(T[] a) { + throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody + } + + @Override + public boolean add(E e) { + throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody + } + + @Override + public boolean remove(Object o) { + throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody + } + + @Override + public boolean containsAll(Collection c) { + throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody + } + + @Override + public boolean addAll(Collection c) { + throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody + } + + @Override + public boolean addAll(int index, Collection c) { + throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody + } + + @Override + public boolean removeAll(Collection c) { + throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody + } + + @Override + public boolean retainAll(Collection c) { + throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody + } + + @Override + public E get(int index) { + throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody + } + + @Override + public E set(int index, E element) { + throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody + } + + @Override + public void add(int index, E element) { + throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody + } + + @Override + public E remove(int index) { + throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody + } + + @Override + public int indexOf(Object o) { + throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody + } + + @Override + public int lastIndexOf(Object o) { + throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody + } + + @Override + public ListIterator listIterator() { + throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody + } + + @Override + public ListIterator listIterator(int index) { + throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody + } + + @Override + public List subList(int fromIndex, int toIndex) { + throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody + } + } + + static void subListRangeCheck(int fromIndex, + int toIndex, + int size) { + if (fromIndex < 0) { + throw new IndexOutOfBoundsException("fromIndex = " + fromIndex); + } + + if (toIndex > size){ + throw new IndexOutOfBoundsException( + "toIndex(" + toIndex + ") > size(" + size + ")"); + } + + if (fromIndex > toIndex) + throw new IllegalArgumentException( + "fromIndex(" + fromIndex + ") > toIndex(" + + toIndex + ")"); + } +} diff --git a/src/main/java/org/apache/commons/collections4/list/IndexedLinkedList.java b/src/main/java/org/apache/commons/collections4/list/IndexedLinkedList.java new file mode 100644 index 0000000000..282df70b43 --- /dev/null +++ b/src/main/java/org/apache/commons/collections4/list/IndexedLinkedList.java @@ -0,0 +1,5706 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.collections4.list; + +import java.lang.reflect.Array; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.ConcurrentModificationException; +import java.util.Deque; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Random; +import java.util.Set; +import java.util.Spliterator; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.function.UnaryOperator; + +/** + *

+ * This class implements the indexed, heuristic doubly-linked list data + * structure that runs all the single-element operations in expected + * \(\mathcal{O}(\sqrt{n})\) time. Under the hood, the actual elements are + * stored in a doubly-linked list. However, we also maintain a list of so-called + * "fingers" stored in a random access array. Each finger {@code F} + * contains two data fields: + *

    + *
  • {@code F.node} - the actual element node,
  • + *
  • {@code F.index} - the appearance index of {@code F.node} in the actual + * list.
  • + *
+ * + *

+ * + * For the list of size \(n\), we maintain + * \(\bigg \lceil \sqrt{n} \bigg \rceil + 1\) fingers. The rightmost finger in + * the finger list is a special end-of-list sentinel. It always has + * {@code F.node = null} and {@code F.index = } \(n\). The fingers are sorted by + * their indices. That arrangement allows simpler and faster code in the method + * that accesses a finger via element index; see + * {@link FingerList#getFingerIndexImpl(int)}. Since number of fingers is + * \(\sqrt{n}\), and assuming that the fingers are evenly distributed, each + * finger "covers" \(n / \sqrt{n} = \sqrt{n}\) elements. In order to access an + * element in the actual list, we first consult the finger list for the index + * {@code i} of the finger {@code fingerArray[i]} that is closest to the index + * of the target element. This runs in + * + * \[ + * \mathcal{O}(\log \sqrt{n}) = \mathcal{O}(\log n^{1/2}) = \mathcal{O}(\frac{1}{2} \log n) = \mathcal{O}(\log n). + * \] + * + * The rest is to "rewind" the closest finger to point to the target + * element (which requires \(\mathcal{O}(\sqrt{n})\) on evenly distributed + * finger lis). + * + * @param the element type. + */ +public class IndexedLinkedList implements Deque, + List, + Cloneable, + java.io.Serializable { + /** + * The static inner class implements a node in a doubly-linked list. + * + * @param the element type stored in each {@code Node}. + */ + static final class Node { + + /** + * The actual satellite datum. + */ + E item; + + /** + * The previous node or {@code null} if this {@link Node} is the head of the + * list. + */ + Node prev; + + /** + * The next node or {@code null} if this {@link Node} is the tail of the + * list. + */ + Node next; + + /** + * Constructs a new {@link Node} object. + * + * @param item the satellite datum of the newly created {@link Node}. + */ + Node(E item) { + this.item = item; + } + + /** + * Returns {@code true} if and only if the input object is another node with + * the same item. + * + * @param o the object to test. + * + * @return {@code true} iff the two nodes are the same. + */ + @Override + public boolean equals(Object o) { + if (o == null) { + return false; + } + + if (o == this) { + return true; + } + + if (!getClass().equals(o.getClass())) { + return false; + } + + Node other = (Node) o; + return Objects.equals(this.item, other.item); + } + + /** + * Returns the textual representation of this {@link Node}. + * + * @return the textual representation. + */ + @Override + public String toString() { + return String.format("", Objects.toString(item)); + } + } + + /** + * This class implements the finger data structure. + * + * @param the element type stored in a node of a finger. + */ + static final class Finger { + + /** + * The pointed to {@link Node}. + */ + Node node; + + /** + * The index at which the {@code node} appears in the list. + */ + int index; + + /** + * Constructs a new {@link Finger}. + * + * @param node the pointed node. + * @param index the index of {@code node} in the actual list. + */ + Finger(Node node, int index) { + this.node = node; + this.index = index; + } + + /** + * Copy constructs this finger. + * + * @param finger the finger whose state to copy. + */ + Finger(Finger finger) { + this.node = finger.node; + this.index = finger.index; + } + + /** + * Returns the index of this finger. Used for research. + * + * @return the index of this finger. + */ + public int getIndex() { + return index; + } + + /** + * {@inheritDoc } + */ + @Override + public boolean equals(Object o) { + if (o == null) { + return false; + } + + if (o == this) { + return true; + } + + if (!o.getClass().equals(this.getClass())) { + return false; + } + + final Finger other = (Finger) o; + + return Objects.equals(index, other.index) + && Objects.equals(node, other.node); + } + + /** + * Returns the textual representation of this finger. + * + * @return the textual representation. + */ + @Override + public String toString() { + return String.format( + "[index = %d, item = %s]", + index, + node == null ? "null" : Objects.toString(node.item)); + } + } + + /** + * Implements the actual finger list index for faster access and + * modification. + * + * @param the list element data type. + */ + static final class FingerList { + + /** + * The owner indexed linked list. + */ + final IndexedLinkedList list; + + /** + * This is also the minimum capacity. + */ + static final int INITIAL_CAPACITY = 8; + + /** + * When the actual size of the finger list (end-sentinel included) is + * smaller than {@code fingerArray.length / THRESHOLD_FACTOR}, the array is + * contracted to {@code fingerArray.length / CONTRACTION_FACTOR} elements. + */ + static final int THRESHOLD_FACTOR = 4; + + /** + * The actual contraction factor. The capacity of the finger array will be + * divided by this constant. + */ + static final int CONTRACTION_FACTOR = 2; + + /** + * The actual list storage array. + */ + Finger[] fingerArray = new Finger[INITIAL_CAPACITY]; + + /** + * Constructs this finger list setting it to empty. + * + * @param list the owner list. + */ + FingerList(IndexedLinkedList list) { + this.list = list; + this.fingerArray[0] = new Finger<>(null, 0); + } + + /** + * Verifies that this finger list and {@code o} have the same size and + * content. Runs in worst-case linear time. + * + * @param o the object to compare to. + * @return {@code true} if and only if {@code o} is a {@code FingerList}, + * has the same size as this finger list and the same content. + */ + @Override + public boolean equals(Object o) { + if (o == null) { + return false; + } + + if (o == this) { + return true; + } + + if (!o.getClass().equals(this.getClass())) { + return false; + } + + final FingerList other = (FingerList) o; + + if (size != other.size) { + return false; + } + + for (int i = 0; i < size; i++) { + if (!Objects.equals(fingerArray[i], other.fingerArray[i])) { + return false; + } + } + + return true; + } + + @Override + public String toString() { + StringBuilder sb = + new StringBuilder() + .append("[FingerList (size = ") + .append(size + 1) + .append(") | "); + + boolean first = true; + + for (int i = 0; i != size + 1; i++) { + if (first) { + first = false; + } else { + sb.append(", "); + } + + sb.append(fingerArray[i].toString()); + } + + return sb.append("]").toString(); + } + + /** + * The number of fingers stored in the list. This field does not count the + * end-of-list sentinel finger {@code F} for which {@code F.index = size}. + */ + int size; + + /** + * Adjusts the finger list after removing the first finger. runs in worst- + * case \(\mathcal{O}(\sqrt{n})\) time. + */ + void adjustOnRemoveFirst() { + int lastPrefixIndex = Integer.MAX_VALUE; + + for (int i = 0; i < size; ++i) { + Finger finger = fingerArray[i]; + + if (finger.index != i) { + lastPrefixIndex = i; + break; + } else { + finger.node = finger.node.next; + } + } + + shiftFingerIndicesToLeftOnceAll(lastPrefixIndex); + } + + /** + * Appends the input finger to the tail of the finger list. Runs in + * amortized constant time. + * + * @param finger the finger to append. + */ + void appendFingerImpl(Finger finger) { + + enlargeFingerArrayWithEmptyRange(size + 2 , + size, + 1, + 1); + fingerArray[size - 1] = finger; + fingerArray[size].index = list.size; + } + + /** + * Pushes {@code numberOfFingersToMoveToPrefix} fingers to the prefix with + * {@code numberOfFingersInPrefix} fingers. + * + * @param fromIndex the index of the leftmost element to + * remove. + * @param numberOfPrefixFingers the number of fingers already in the + * prefix. + * @param numberOfFingersToMove the number of fingers we need to + * move to the prefix. + */ + void arrangePrefix(int fromIndex, + int numberOfPrefixFingers, + int numberOfFingersToMove) { + + makeRoomAtPrefix(fromIndex, + numberOfPrefixFingers, + numberOfFingersToMove); + + pushCoveredFingersToPrefix(fromIndex, + numberOfPrefixFingers, + numberOfFingersToMove); + } + + void arrangeSuffix(int toIndex, + int toFingerIndex, + int numberOfSuffixFingers, + int numberOfFingetsToMove) { + + makeRoomAtSuffix(toIndex, + toFingerIndex, + numberOfSuffixFingers, + numberOfFingetsToMove); + + pushCoveredFingersToSuffix(toIndex, + numberOfSuffixFingers, + numberOfFingetsToMove); + } + + /** + * Clears entirely this finger list. Only the end-of-finger-list finger + * remains in the finger list. Not {@code private} since is used in the unit + * tests. + */ + void clear() { + Arrays.fill(fingerArray, + 0, + size, + null); + + fingerArray = new Finger[INITIAL_CAPACITY]; + fingerArray[0] = new Finger<>(null, 0); + size = 0; + } + + /** + * Contracts the finger array, if possible. The {@code nextSize} defines the + * requested finger array size not counting the end-of-finger-list sentinel + * finger. + * + * @param nextSize the requested size not counting the end-of-finger-list + * sentinel finger. + */ + void contractFingerArrayIfNeeded(int nextSize) { + if (fingerArray.length == INITIAL_CAPACITY) { + // Nothing to contract: + return; + } + + // Can we contract at least once? + if (nextSize + 1 < fingerArray.length / THRESHOLD_FACTOR) { + + int nextCapacity = fingerArray.length / CONTRACTION_FACTOR; + + // Good, we can. But can we keep on splitting in half the + // capacity any further? + while (nextCapacity >= (nextSize + 1) * CONTRACTION_FACTOR + && nextCapacity > INITIAL_CAPACITY) { + // Yes, we can do it as well. + nextCapacity /= CONTRACTION_FACTOR; + } + + Finger[] nextFingerArray = new Finger[nextCapacity]; + + System.arraycopy(fingerArray, + 0, + nextFingerArray, + 0, + nextCapacity); + + fingerArray = nextFingerArray; + } + } + + + void enlargeFingerArrayWithEmptyRange(int requestedCapacity, + int fingerRangeStartIndex, + int fingerRangeLength, + int elementRangeLength) { + + if (requestedCapacity > fingerArray.length) { + // Compute the next accommodating capacity: + int nextCapacity = 2 * fingerArray.length; + + while (nextCapacity < requestedCapacity) { + nextCapacity *= 2; + } + + // Here, we have a next accommodating capacity! + Finger[] nextFingerArray = new Finger[nextCapacity]; + + // Copy the finger array prefix: + System.arraycopy(fingerArray, + 0, + nextFingerArray, + 0, + fingerRangeStartIndex); + + // Compute the number of fingers to shift to the right: + int numberOfFingersToShift = size + - fingerRangeStartIndex + + 1; + + // Make room for the finger range: + System.arraycopy(fingerArray, + fingerRangeStartIndex, + nextFingerArray, + fingerRangeStartIndex + fingerRangeLength, + numberOfFingersToShift); + + // Deploy 'nextFingerArraqy': + fingerArray = nextFingerArray; + + // Update the number of fingers in this finger list: + size += fingerRangeLength; + + // Update the indices of the suffix finger list: + shiftFingerIndicesToRight(fingerRangeStartIndex + fingerRangeLength, + elementRangeLength); + } else { + // Shift the right part to the right: + shiftFingerIndicesToRight(fingerRangeStartIndex, + elementRangeLength); + + int numberOfSuffixFingers = size + + 1 + - fingerRangeStartIndex; + + // Make room for the finger range: + System.arraycopy(fingerArray, + fingerRangeStartIndex, + fingerArray, + fingerRangeStartIndex + fingerRangeLength, + numberOfSuffixFingers); + + size += fingerRangeLength; + } + } + + /** + * Returns {@code index}th finger. + * + * @param index the index of the target finger. + * @return the {@code index}th finger. + */ + Finger getFinger(int index) { + return fingerArray[index]; + } + + /** + * Returns the index of the finger that is closest to the + * {@code elementIndex}th list element. + * + * @param elementIndex the target element index. + * @return the index of the finger that is closest to the + * {@code elementIndex}th element. + */ + int getClosestFingerIndex(int elementIndex) { + return normalize(getFingerIndexImpl(elementIndex), + elementIndex); + } + + /** + * Returns the finger index {@code i}, such that + * {@code fingerArray[i].index} is no less than {@code elementIndex}, and + * {@code fingerArray[i].index} is closest to {@code elementIndex}. This + * algorithm is translated from + * C++ + * lower_bound algorithm. + * + * @param elementIndex the target element index. + * @return the index of the finger {@code f}, for which + * {@code elementIndex <= f.index} and {@code f} is the leftmost such + * finger. + */ + int getFingerIndexImpl(int elementIndex) { + int count = size + 1; // + 1 for the end sentinel. + int idx = 0; + + while (count > 0) { + int it = idx; + int step = count / 2; + it += step; + + if (fingerArray[it].index < elementIndex) { + idx = ++it; + count -= step + 1; + } else { + count = step; + } + } + + return idx; + } + + /** + * Access the {@code index}th node without modifying the fingers unlike + * {@link #getNode(int)}. + * + * @param index the index of the desired node. + * + * @return the {@code index}th node. + */ + Node getNodeNoFingersFix(int index) { + Finger finger = fingerArray[getClosestFingerIndex(index)]; + int steps = index - finger.index; + + return IndexedLinkedList.rewindFinger(finger, + steps); + } + + /** + * Returns the {@code i}th node of this linked list. The closest finger is + * updated to point to the returned node. + * + * @param elementIndex the element index. + * @return the {@code index}th node in the linked list. + */ + Node getNode(int elementIndex) { + if (size < 3) { + // We need at least 3 fingers to do the actual trick: + return list.getNodeSequentially(elementIndex); + } + + int fingerIndex = getFingerIndexImpl(elementIndex); + + if (fingerIndex == 0) { + // There is no required preceding finger: + return getPrefixNode(elementIndex); + } + + if (fingerIndex >= size - 1) { + return getSuffixNode(elementIndex); + } + + Finger a = fingerArray[fingerIndex - 1]; + Finger b = fingerArray[fingerIndex]; + Finger c = fingerArray[fingerIndex + 1]; + + int diff = c.index - a.index; + int step = diff / 2; + int saveBIndex = b.index; + int nextBIndex = a.index + step; + + b.index = nextBIndex; + + // Rewind the finger b node: + if (saveBIndex < nextBIndex) { + for (int i = 0; i != nextBIndex - saveBIndex; i++) { + b.node = b.node.next; + } + } else { + // Here, 'saveBIndex >= nextBIndex': + for (int i = 0; i != saveBIndex - nextBIndex; i++) { + b.node = b.node.prev; + } + } + + // Go fetch the correct node: + if (elementIndex < nextBIndex) { + // Here, the desired element is between a and b: + int leftDistance = elementIndex - a.index; + int rightDistance = b.index - elementIndex; + + if (leftDistance < rightDistance) { + return scrollToRight(a.node, + leftDistance); + } else { + return scrollToLeft(b.node, + rightDistance); + } + } else { + // Here, the desired element is between c and b: + int leftDistance = elementIndex - b.index; + int rightDistance = c.index - elementIndex; + + if (leftDistance < rightDistance) { + return scrollToRight(b.node, + leftDistance); + } else { + return scrollToLeft(c.node, + rightDistance); + } + } + } + + /** + * Normalizes the first finger and returns the {@code elementIndex}th node. + * + * @param elementIndex the index of the desired element. + * + * @return the node corresponding to the {@code elementIndex}th position. + */ + private Node getPrefixNode(int elementIndex) { + Finger a = fingerArray[0]; + Finger b = fingerArray[1]; + Node aNode = a.node; + + // Put a between b and the beginning of the list: + int nextAIndex = b.index / 2; + int saveAIndex = a.index; + + a.index = nextAIndex; + + if (saveAIndex < nextAIndex) { + // Here, we need to rewind to the right: + for (int i = saveAIndex; i < nextAIndex; i++) { + aNode = aNode.next; + } + } else { + // Once here, 'saveAIndex >= nextAIndex'. + // We need to rewind to the left: + for (int i = nextAIndex; i < saveAIndex; i++) { + aNode = aNode.prev; + } + } + + a.node = aNode; + + // Go get the proper node: + if (elementIndex < nextAIndex) { + // Here, the desired element is between the head of the list and + // the very first fi nger: + int leftDistance = elementIndex; + int rightDistance = nextAIndex - elementIndex; + + if (leftDistance < rightDistance) { + return scrollToRight(list.head, + elementIndex); + } else { + return scrollToLeft(aNode, + rightDistance); + } + } else { + return aNode; + } + } + + /** + * Returns the {@code elementIndex}th node and normalizes the last finger. + * + * @param elementIndex the index of the desired element. + * + * @return the {@code elementIndex}th node. + */ + private Node getSuffixNode(int elementIndex) { + Finger a = fingerArray[size - 2]; + Finger b = fingerArray[size - 1]; + Node bNode = b.node; + + int saveBIndex = b.index; + int nextBIndex = (a.index + list.size) / 2; + + b.index = nextBIndex; + + // Rewind the finger 'b' to between 'a' and tail: + if (saveBIndex < nextBIndex) { + int distance = nextBIndex - saveBIndex; + + for (int i = 0; i != distance; i++) { + bNode = bNode.next; + } + } else { + // Here, 'nextBIndex <= saveBIndex': + int distance = saveBIndex - nextBIndex; + + for (int i = 0; i != distance; i++) { + bNode = bNode.prev; + } + } + + b.node = bNode; + + // Go get the proper node: + if (elementIndex < nextBIndex) { + // Here, the desired element node is between 'a' and 'b': + int leftDistance = elementIndex - a.index; + int rightDistance = nextBIndex - elementIndex; + + if (leftDistance < rightDistance) { + return scrollToRight(a.node, + leftDistance); + } else { + return scrollToLeft(b.node, + rightDistance); + } + } else { + // Here, the desired element node is between 'b' and the tail + // node of the list: + int leftDistance = elementIndex - nextBIndex; + int rightDistance = list.size - elementIndex - 1; + + if (leftDistance < rightDistance) { + // Once here, rewind the node reference from bNode to the + // right: + return scrollToRight(bNode, + leftDistance); + } else { + // Once here, rewind the node reference from tail to the + // left: + return scrollToLeft(list.tail, + rightDistance); + } + } + } + + /** + * Inserts the input finger into the finger list such that the entire finger + * list is sorted by indices. + * + * @param finger the finger to insert. + */ + void insertFingerAndShiftOnceToRight(Finger finger) { + int beforeFingerIndex = getFingerIndexImpl(finger.index); + + enlargeFingerArrayWithEmptyRange(size + 2, + beforeFingerIndex, + 1, + 1); + + fingerArray[beforeFingerIndex] = finger; + } + + /** + * Returns {@code true} if this finger list is empty. + * + * @return {@code true} if this finger contains no fingers (except the + * end-of-finger-list sentinel). + */ + boolean isEmpty() { + return size == 0; + } + + /** + * Make sure we can insert {@code roomSize} fingers starting from + * {@code fingerIndex}, shifting all the fingers starting from + * {@code numberOfNodes} to the right. + * + * @param fingerIndex the finger index of the first finger in the shifted + * finger slice. + * @param roomSize the number of free spots requested. + * @param numberOfNodes the shift amount of the moved fingers. + */ + void makeRoomAtIndex(int fingerIndex, + int roomSize, + int numberOfNodes) { + + enlargeFingerArrayWithEmptyRange(size + 1 + roomSize, + fingerIndex, + roomSize, + numberOfNodes); + } + + void makeRoomAtPrefix(int fromIndex, + int numberOfPrefixFingers, + int numberOfFingersToMove) { + + if (numberOfPrefixFingers == 0) { + // Here, no fingers in the prefix to move. + return; + } + + int targetFingerIndex = numberOfPrefixFingers - 1; + int freeFingerSpotsSoFar = fromIndex + - getFinger(targetFingerIndex).index + - 1; + + if (freeFingerSpotsSoFar >= numberOfFingersToMove) { + return; + } + + for (; targetFingerIndex > 0; targetFingerIndex--) { + Finger finger1 = getFinger(targetFingerIndex - 1); + Finger finger2 = getFinger(targetFingerIndex); + + int distance = finger2.index + - finger1.index + - 1; + + freeFingerSpotsSoFar += distance; + + if (freeFingerSpotsSoFar >= numberOfFingersToMove) { + break; + } + } + + if (freeFingerSpotsSoFar < numberOfFingersToMove) { + // Once here, we need to move the leftmost prefix finger to the + // left. + int index = fromIndex + - numberOfPrefixFingers + - numberOfFingersToMove; + + Node node = getNodeNoFingersFix(index); + + for (int i = 0; i < numberOfPrefixFingers; i++) { + Finger finger = getFinger(i); + finger.index = index++; + finger.node = node; + node = node.next; + } + } else { + Finger startFinger = getFinger(targetFingerIndex - 1); + int index = startFinger.index; + Node node = startFinger.node; + + for (int i = targetFingerIndex; i < numberOfPrefixFingers; i++) { + Finger finger = getFinger(i); + node = node.next; + finger.node = node; + finger.index = ++index; + } + } + } + + void makeRoomAtSuffix(int toIndex, + int toFingerIndex, + int numberOfSuffixFingers, + int numberOfFingersToMove) { + + if (numberOfSuffixFingers == 0) { + // Here, no fingers in the suffix to move. + return; + } + + int targetFingerIndex = size - numberOfSuffixFingers; + int freeFingerSpotsSoFar = getFinger(targetFingerIndex).index + - toIndex; + + if (freeFingerSpotsSoFar >= numberOfFingersToMove) { + return; + } + + for (; targetFingerIndex < size - 1; targetFingerIndex++) { + Finger finger1 = getFinger(targetFingerIndex); + Finger finger2 = getFinger(targetFingerIndex + 1); + + int distance = finger2.index + - finger1.index + - 1; + + freeFingerSpotsSoFar += distance; + + if (freeFingerSpotsSoFar >= numberOfFingersToMove) { + break; + } + } + + if (freeFingerSpotsSoFar < numberOfFingersToMove) { + // Once here, we need to move the rightmost suffix finger to the + // right. + int index = list.size + - numberOfSuffixFingers; + + Node node = getNodeNoFingersFix(index); + + for (int i = 0; i < numberOfSuffixFingers; i++) { + Finger finger = + getFinger(size - numberOfSuffixFingers + i); + + finger.index = index++; + finger.node = node; + node = node.next; + } + } else { + Finger startFinger = getFinger(targetFingerIndex + 1); + int index = startFinger.index - 1; + Node node = startFinger.node.prev; + + for (int i = targetFingerIndex; + i >= toFingerIndex; + i--) { + Finger finger = getFinger(i); + finger.index = index--; + finger.node = node; + node = node.prev; + } + } + } + + /** + * Makes sure that the returned finger index {@code i} points to the closest + * finger in the finger array. + * + * @param fingerIndex the finger index. + * @param elementIndex the element index. + * + * @return the index of the finger that is closest to the + * {@code elementIndex}th element. + */ + private int normalize(int fingerIndex, int elementIndex) { + if (fingerIndex == 0) { + // Since we cannot point to '-1'th finger, return 0: + return 0; + } + + if (fingerIndex == size) { + // Don't go outside of 'size - 1': + return size - 1; + } + + Finger finger1 = fingerArray[fingerIndex - 1]; + Finger finger2 = fingerArray[fingerIndex]; + + int distance1 = elementIndex - finger1.index; + int distance2 = finger2.index - elementIndex; + + // Return the closest finger index: + return distance1 < distance2 ? fingerIndex - 1 : fingerIndex; + } + + /** + * Creates a finger for the input node {@code node} and inserts it at the + * head of the finger array. + * + * @param node the target node. + */ + void prependFingerForNode(Node node) { + Finger finger = new Finger<>(node, 0); + + // 'size + 1': actual number of fingers + the end-of-finger-list + // sentinel: + if (size + 1 == fingerArray.length) { + // Once here, the 'fingerArray' is fully filled: + Finger[] newFingerArray = new Finger[2 * fingerArray.length]; + + // Move the current finger list contents to the new finger array: + System.arraycopy(fingerArray, + 0, + newFingerArray, + 1, + size + 1); + + fingerArray = newFingerArray; + + // Shift all the rest fingers' indices one step to the right towards + // higher indices: + shiftFingerIndicesToRightOnce(1); + + // Update the index of the new end-of-finger-list sentinel: + ++getFinger(size() + 1).index; + } else { + // Shift the all fingers' indices one step to the right: + shiftFingerIndicesToRightOnce(0); + + // Make room for the new finger: + System.arraycopy(fingerArray, + 0, + fingerArray, + 1, + size + 1); + + } + + fingerArray[0] = finger; + size++; + } + + /** + * Pushes {@code numberOfFingersToPush} to the finger prefix. + * + * @param fromIndex the starting index of the range to delete. + * @param numberOfPrefixFingers the number of fingers in the prefix. + * @param numberOfFingersToPush the number of fingers to move to the prefix. + */ + void pushCoveredFingersToPrefix(int fromIndex, + int numberOfPrefixFingers, + int numberOfFingersToPush) { + if (numberOfPrefixFingers == 0) { + int index = fromIndex - 1; + Node node = getNodeNoFingersFix(index); + + for (int i = numberOfFingersToPush - 1; i >= 0; i--) { + Finger finger = getFinger(i); + finger.index = index--; + finger.node = node; + node = node.prev; + } + } else { + Finger rightmostPrefixFinger = + getFinger(numberOfPrefixFingers - 1); + + int index = rightmostPrefixFinger.index + 1; + Node node = rightmostPrefixFinger.node.next; + + for (int i = numberOfPrefixFingers; + i < numberOfPrefixFingers + numberOfFingersToPush; + i++) { + + Finger finger = getFinger(i); + finger.index = index++; + finger.node = node; + node = node.next; + } + } + } + + void pushCoveredFingersToSuffix(int toIndex, + int numberOfSuffixFingers, + int numberOfFingersToPush) { + if (numberOfSuffixFingers == 0) { + int index = toIndex; + Node node = getNodeNoFingersFix(index); + + for (int i = 0; i < numberOfFingersToPush; i++) { + Finger finger = getFinger(size - numberOfFingersToPush + i); + finger.index = index++; + finger.node = node; + node = node.next; + } + } else { + Finger leftmostSuffixFinger = + getFinger(size - numberOfSuffixFingers); + + int index = leftmostSuffixFinger.index; + Node node = leftmostSuffixFinger.node; + + for (int i = 0; i < numberOfFingersToPush; i++) { + Finger finger = + getFinger(size - numberOfSuffixFingers - 1 - i); + + node = node.prev; + finger.node = node; + finger.index = --index; + } + } + } + + /** + * Removes the last finger residing right before the end-of-finger-list + * sentinel finger. + */ + void removeFinger() { + contractFingerArrayIfNeeded(--size); + fingerArray[size] = fingerArray[size + 1]; + fingerArray[size + 1] = null; + fingerArray[size].index = list.size; + } + + /** + * This method is responsible for actual removal of the fingers. Run in + * worst-case \(\mathcal{O}(\sqrt{N})\) time. + * + * @param fromFingerIndex the index of the very first finger to + * remove. + * @param numberOfFingersToRemove the number of fingers to remove. + * @param removalRangeLength the length of the element range belonging + * to the range being removed. + */ + void removeFingersOnDeleteRange(int fromFingerIndex, + int numberOfFingersToRemove, + int removalRangeLength) { + + if (numberOfFingersToRemove != 0) { + // Push 'numberOfFingersToRemove' towards to the prefix: + int copyLength = size + - fromFingerIndex + - numberOfFingersToRemove + - list.numberOfCoveringFingersToPrefix + + 1; + + System.arraycopy( + fingerArray, + fromFingerIndex + + list.numberOfCoveringFingersToPrefix + + numberOfFingersToRemove, + fingerArray, + fromFingerIndex + list.numberOfCoveringFingersToPrefix, + copyLength); + + // Set all unused finger array positions to 'null' in order to get + // rid of junk: + Arrays.fill(fingerArray, + size - numberOfFingersToRemove + 1, + size + 1, + null); + + // Update the number of fingers: + this.size -= numberOfFingersToRemove; + } + + // Update the finger indices on the right: + shiftFingerIndicesToLeft( + fromFingerIndex + list.numberOfCoveringFingersToPrefix, + removalRangeLength); + + list.size -= removalRangeLength; + } + + /** + * Returns a node that is {@code steps} hops away from {@code node} to the + * left. + * + * @param node the starting node. + * @param steps the number of hops to make. + * @param the element type. + * + * @return the requested node. + */ + static Node scrollToLeft(Node node, int steps) { + for (int i = 0; i != steps; ++i) { + node = node.prev; + } + + return node; + } + + /** + * Returns a node that is {@code steps} hops away from {@code node} to the + * right. + * + * @param node the starting node. + * @param steps the number of hops to make. + * @param the element type. + * + * @return the requested node. + */ + static Node scrollToRight(Node node, int steps) { + for (int i = 0; i != steps; ++i) { + node = node.next; + } + + return node; + } + + /** + * Sets the finger {@code finger} to the finger array at index + * {@code index}. + * + * @param index the index of the finger list component. + * @param finger the target finger to set. + */ + void setFinger(int index, Finger finger) { + fingerArray[index] = finger; + } + + /** + * Sets all the leftmost {@code indices.length} fingers to the specified + * indices. + * + * @param indices the target indices. + */ + void setFingerIndices(int... indices) { + Arrays.sort(indices); + int fingerIndex = 0; + + for (final int index : indices) { + final Finger finger = fingerArray[fingerIndex++]; + finger.index = index; + finger.node = getNodeSequentially(index); + } + } + + /** + * Accesses the {@code index}th node sequentially without using fingers and + * modifying the fingers. + * + * @param index the index of the desired node. + * + * @return {@code index} node. + */ + private Node getNodeSequentially(final int index) { + return list.getNodeSequentially(index); + } + + /** + * Moves all the fingers in range {@code [startFingerIndex, size]} + * {@code shiftLength} positions to the left (towards smaller indices). + * + * @param startFingerIndex the index of the leftmost finger to shift. + * @param shiftLength the length of the shift operation. + */ + void shiftFingerIndicesToLeft(int startFingerIndex, int shiftLength) { + for (int i = startFingerIndex; i <= size; ++i) { + fingerArray[i].index -= shiftLength; + } + } + + /** + * Moves all the fingers in range {@code [startFingerIndex, size]} one + * position to the left (towards smaller indices). + * + * @param startFingerIndex the index of the leftmost finger to shift. + */ + void shiftFingerIndicesToLeftOnceAll(int startFingerIndex) { + for (int i = startFingerIndex; i <= size; ++i) { + fingerArray[i].index--; + } + } + + /** + * Moves all the fingers in range {@code [startFingerIndex, size]} + * {@code shiftLength} positions to the right (towards larger indices). + * + * @param startIndex the index of the leftmost finger to shift. + * @param shiftLength the length of the shift operation. + */ + void shiftFingerIndicesToRight(int startIndex, int shiftLength) { + for (int i = startIndex; i <= size; ++i) { + fingerArray[i].index += shiftLength; + } + } + + /** + * Moves all the fingers in range {@code [startFingerIndex, size]} one + * position to the right (towards larger indices). + * + * @param startIndex the index of the leftmost finger to shift. + */ + void shiftFingerIndicesToRightOnce(int startIndex) { + shiftFingerIndicesToRight(startIndex, 1); + } + + /** + * Returns the number of fingers in this finger list not counting the + * end-of-finger-list finger. + * + * @return the number of fingers in this finger list. + */ + int size() { + return size; + } + } + + /** + * The serial version UID. + */ + private static final long serialVersionUID = 54170828611556733L; + + /** + * The cached number of elements in this list. + */ + int size; + + /** + * The modification counter. Used to detect state changes during concurrent + * modifications. + */ + transient int modCount; + + /** + * The head node of the list. + */ + transient Node head; + + /** + * The tail node of the list. + */ + transient Node tail; + + /** + * The actual finger list. Without {@code private} keyword since it is + * accessed in unit tests. + */ + transient FingerList fingerList; + + /** + * Caches the start node of the range to remove. + */ + private transient Node removeRangeStartNode; + + /** + * Caches the end node of the range to remove. + */ + private transient Node removeRangeEndNode; + + /** + * Caches the number of fingers covering in the prefix. + */ + transient int numberOfCoveringFingersToPrefix; + + /** + * Caches the number of fingers covering in the suffix. + */ + transient int numberOfCoveringFingersToSuffix; + + /** + * Constructs an empty list. + */ + public IndexedLinkedList() { + this.fingerList = new FingerList<>(this); + } + + /** + * Constructs a new list and copies the data in {@code c} to it. Runs in + * \(\mathcal{O}(m + \sqrt{m})\) time, where \(m = |c|\). + * + * @param c the collection to copy. + */ + public IndexedLinkedList(Collection c) { + this(); + addAll(c); + } + + /** + * Appends the specified element to the end of this list. Runs in amortized + * constant time. + * + *

This method is equivalent to {@link #addLast}. + * + * @param e element to be appended to this list. + * @return {@code true} (as specified by {@link Collection#add}). + */ + @Override + public boolean add(E e) { + linkLast(e); + return true; + } + + /** + * Inserts the specified element at the specified position in this list. + * The affected finger indices will be incremented by one. A finger + * {@code F} is affected, if {@code F.index >= index}. Runs in + * \(\mathcal{O}(\sqrt{\mathrm{size}})\) time. + * + * @param index index at which the specified element is to be inserted. + * @param element element to be inserted. + * @throws IndexOutOfBoundsException if index is outside of the valid range. + */ + @Override + public void add(int index, E element) { + checkPositionIndex(index); + + if (index == size) { // Check push-back first as it is used more often. + linkLast(element); + } else if (index == 0) { + linkFirst(element); + } else { + linkBefore(element, + index, + fingerList.getNodeNoFingersFix(index)); + } + } + + /** + * Appends all of the elements in the specified collection to the end of + * this list, in the order they are returned by the specified collection's + * iterator. The behavior of this operation is undefined if the specified + * collection is modified while the operation is in progress. (Note that + * this will occur if the specified collection is this list, and it's + * nonempty.) Runs in \(\mathcal{O}(m + \sqrt{m + n} - \sqrt{n})\), where + * \(m = |c|\) and \(n\) is the size of this list. + * + * @param c collection containing elements to be added to this list. + * @return {@code true} if this list changed as a result of the call. + * @throws NullPointerException if the specified collection is null. + */ + @Override + public boolean addAll(Collection c) { + return addAll(size, c); + } + + /** + * Inserts all of the elements in the specified collection into this list, + * starting at the specified position. For each finger {@code F} with + * {@code F.index >= index} will increment {@code F.index} by 1. Runs in + * \(\Theta(m + \sqrt{m + n} - \sqrt{n}) + \mathcal{O}(\sqrt{n})\), where + * \(m = |c|\) and \(n\) is the size of this list. + * + * @param index index at which to insert the first element from the + * specified collection. + * @param c collection containing elements to be added to this list. + * @return {@code true} if this list changed as a result of the call. + * @throws IndexOutOfBoundsException if the index is outside of the valid + * range. + * @throws NullPointerException if the specified collection is null + */ + @Override + public boolean addAll(int index, Collection c) { + checkPositionIndex(index); + + if (c.isEmpty()) { + return false; + } + + if (size == 0) { + setAll(c); + } else if (index == size) { + appendAll(c); + } else if (index == 0) { + prependAll(c); + } else { + insertAll(c, getNode(index), index); + } + + return true; + } + + /** + * Adds the element {@code e} before the head of this list. Runs in + * \(\mathcal{O}(\sqrt{n})\) time. + * + * @param e the element to add. + */ + @Override + public void addFirst(E e) { + linkFirst(e); + } + + /** + * Adds the element {@code e} after the tail of this list. Runs in constant + * time. + * + * @param e the element to add. + */ + @Override + public void addLast(E e) { + linkLast(e); + } + + /** + * Checks the data structure invariant. Throws + * {@link java.lang.IllegalStateException} on invalid invariant. The + * invariant is valid if: + *

    + *
  1. All the fingers in the finger list are sorted by indices.
  2. + *
  3. There is no duplicate indices.
  4. + *
  5. The index of the leftmost finger is no less than zero.
  6. + *
  7. There must be an end-of-list sentinel finger {@code F}, such that + * {@code F.index = } size of linked list and {@code F.node} is + * {@code null}. + *
  8. + *
  9. Each finger {@code F} points to the {@code i}th linked list node, + * where {@code i = F.index}.
  10. + *
+ * Runs in worst case \(\mathcal{O}(n)\) time. + */ + public void checkInvarant() { + // The first finger spot can never be 'null': + if (fingerList.getFinger(0) == null) { + throw new IllegalStateException( + "fingerList[0] is null. " + + "Must be at least the end-of-finger-list sentinel."); + } + + if (fingerList.isEmpty()) { + // Here the finger list is empty (apart from the end-of-finger-list + // sentinel): + if (!this.isEmpty()) { + // This indexed list cannot be here empty: + throw new IllegalStateException( + "fingerList.size() === " + + fingerList.size() + + ", this.size() == " + + this.size() + + " != 0"); + } + + if (head != null) { + // The finger and actual lists are empty. 'head' must be 'null': + throw new IllegalStateException("head != null"); + } + + if (tail != null) { + // The finger and actual lists are empty. 'tail' must be 'null': + throw new IllegalStateException("tail != null"); + } + } + + if (fingerList.getFinger(0).index < 0) { + // Negative initial finger index: + throw new IllegalStateException( + "First finger index is negative: " + + fingerList.getFinger(0).index); + } + + // Check the fingers: + for (int i = 0; i < fingerList.size() - 1; ++i) { + Finger left = fingerList.getFinger(i); + Finger right = fingerList.getFinger(i + 1); + + // First left will be checked in the very beginning of this method: + if (right == null) { + // 'right' cannot be 'null': + throw new IllegalStateException( + "fingerList[" + (i + 1) + "] is null."); + } + + if (left.index >= right.index) { + // Here, indices are in opposite relative order: + throw new IllegalStateException( + "FingerList failed: fingerList[" + + i + + "].index = " + + left.index + + " >= " + + right.index + + " = fingerList[" + + (i + 1) + + "].index"); + } + } + + if (getRecommendedNumberOfFingers() != fingerList.size()) { + // The required and actual number of fingers mismatch: + throw new IllegalStateException( + "Number of fingers mismatch: required = " + + getRecommendedNumberOfFingers() + + ", actual = " + + fingerList.size()); + } + + Finger sentinelFinger = fingerList.getFinger(fingerList.size()); + + if (sentinelFinger == null) { + // Here, the end-of-finger-list sentinel is 'null': + throw new IllegalStateException( + "No sentinel finger (number of fingers = " + + fingerList.size() + + ")."); + } + + if (sentinelFinger.index != this.size) { + // Size mismatch: + throw new IllegalStateException( + "sentinelFinger.index != this.size. (" + + sentinelFinger.index + + " != " + + this.size + + ")"); + } + + if (sentinelFinger.node != null) { + // The sentinel finger may not have any node associated with it: + throw new IllegalStateException( + "sentinelFigner.node != null: " + sentinelFinger); + } + + // Check the finger and element counters: + Finger finger = fingerList.getFinger(0); + Node node = head; + int fingerCount = 0; + int tentativeSize = 0; + + while (node != null) { + // Count a new 'node': + tentativeSize++; + + if (finger.node == node) { + // 'node' is pointed by a finger: + finger = fingerList.getFinger(++fingerCount); + } + + node = node.next; + } + + if (size != tentativeSize) { + // The size recording in this indexed list and actual counted size + // do not match: + throw new IllegalStateException( + "Number of nodes mismatch: size = " + + size + + ", tentativeSize = " + + tentativeSize); + } + + // Check that there is no junk fingers in the rest of the finger array: + for (int i = fingerList.size() + 1; + i < fingerList.fingerArray.length; + i++) { + + finger = fingerList.getFinger(i); + + if (finger != null) { + // Found a junk finger: + throw new IllegalStateException( + "Junk finger " + finger + " at fingerList[" + i + "]"); + } + } + + int length = fingerList.fingerArray.length; + + // Finally, check that the finger list cannot be contracted: + if (length == FingerList.INITIAL_CAPACITY) { + // Nothing to contract: + return; + } + + if (size + 1 < (length / FingerList.THRESHOLD_FACTOR)) { + // The finger array capacity is too large. Should have contracted. + throw new IllegalStateException( + "The list has " + + (size() + 1) + + " elements in total. Capacity of the " + + "finger list is " + + fingerList.size() + + ". Must be contracted."); + } + } + + /** + * Completely clears this list. + */ + @Override + public void clear() { + fingerList.clear(); + size = 0; + + // Help GC: + for (Node node = head; node != null;) { + // Unlink 'node': + node.prev = null; + node.item = null; + Node next = node.next; + node.next = null; + node = next; + } + + // Repair the invariant: + head = tail = null; + // Signal that state was changed: + modCount++; + } + + /** + * Returns a clone list with same content as this list. + * + * @return the clone list. + */ + @Override + public Object clone() { + return new IndexedLinkedList<>(this); + } + + /** + * Returns {@code true} only if {@code o} is present in this list. Runs in + * worst-case linear time. + * + * @param o the query object. + */ + @Override + public boolean contains(Object o) { + return indexOf(o) >= 0; + } + + /** + * Returns {@code true} only if this list contains all the elements + * mentioned in the {@code c}. Runs in Runs in \(\mathcal{O}(mn)\) time, + * where \(m = |c|\). + * + * @param c the query object collection. + * @return {@code true} only if this list contains all the elements in + * {@code c}, or {@code false} otherwise. + */ + @Override + public boolean containsAll(Collection c) { + for (Object o : c) { + if (!contains(o)) { + return false; + } + } + + return true; + } + + /** + * Returns a deep copy of this list that mimics both the linked list and the + * finger list. (Used for unit testing. Normal usage of the class should not + * rely on this method.) + * + * @return a deep copy of this list. + */ + public IndexedLinkedList deepCopy() { + // First, copy the actual content: + IndexedLinkedList other = new IndexedLinkedList<>(this); + int fingerIndex = 0; + + // Copy the finger list: + for (int i = 0; i <= this.fingerList.size; i++) { + // Copy the finger: + other.fingerList.fingerArray[fingerIndex++] = + new Finger<>(fingerList.fingerArray[i]); + } + + return other; + } + + /** + * Packs half of fingers at the list prefix and the rest of fingers to the + * suffix of the list. Used for research. + */ + public void deoptimize() { + if (fingerList.isEmpty()) { + // Nothing to deoptimize. Return: + return; + } + + if (fingerList.size() == 1) { + // Handles a special case: + Finger finger = fingerList.getFinger(0); + finger.index = 0; + finger.node = head; + return; + } + + // Packs the fingers: + int leftFingers = fingerList.size() / 2; + int rightFingers = fingerList.size() - leftFingers; + int sz = fingerList.size(); + Node node = head; + + // Just pack all the fingers at the very beginning. + for (int i = 0; i < leftFingers; ++i) { + // Pack the current finger: + fingerList.getFinger(i).index = i; + fingerList.getFinger(i).node = node; + // Grab the reference to the next node: + node = node.next; + } + + node = tail; + + // Pack the remaining fingers at the end of the list: + for (int i = 0; i < rightFingers; ++i) { + fingerList.getFinger(sz - 1 - i).index = size - 1 - i; + fingerList.getFinger(sz - 1 - i).node = node; + // Grab the reference to the previous node: + node = node.prev; + } + } + + /** + * Returns the descending iterator. + * + * @return the descending iterator pointing to the tail of this list. + */ + @Override + public Iterator descendingIterator() { + return new DescendingIterator(); + } + + /** + * Distributes the fingers over the element list + * {@code [fromIndex, toIndex)} evenly. + * + * @param fromIndex the leftmost element index in the range over which to + * distribute the fingers. + * @param toIndex the one past the rightmost element index in the range + * over which to distribute the fingers. + */ + public void distributeFingers(int fromIndex, int toIndex) { + checkFromTo(fromIndex, toIndex); + + int rangeLength = toIndex - fromIndex; + + if (rangeLength == 0) { + return; + } + + int fingerPrefixLength = fingerList.getFingerIndexImpl(fromIndex); + int fingerSuffixLength = fingerList.size() + - fingerList.getFingerIndexImpl(toIndex); + + int numberOfRangeFingers = fingerList.size() + - fingerPrefixLength + - fingerSuffixLength; + + if (numberOfRangeFingers == 0) { + // Nothing to distribute. Return: + return; + } + + int numberOfElementsPerFinger = rangeLength / numberOfRangeFingers; + int index = fromIndex; + + // Grab the node: + Node node = getNode(fromIndex); + + for (int i = 0; i < numberOfRangeFingers - 1; ++i) { + // Grab the ith finger in the range: + Finger finger = fingerList.getFinger(i + fingerPrefixLength); + // Update its data: + finger.node = node; + finger.index = index; + // Advance both node and index to the next finger's node: + node = scrollNodeToRight(node, numberOfElementsPerFinger); + index += numberOfElementsPerFinger; + } + + // Since we cannot advance node to the right, we need to deal with the + // last (non-sentinel) finger manually: + Finger lastFinger = + fingerList.getFinger( + numberOfRangeFingers - 1 + fingerPrefixLength); + + lastFinger.node = node; + lastFinger.index = index; + } + + /** + * Returns the first element of this list. Runs in constant time. + * + * @return the first element of this list. + * @throws NoSuchElementException if this list is empty. + */ + @Override + public E element() { + return getFirst(); + } + + /** + * Returns {@code true} only if {@code o} is an instance of + * {@link java.util.List} and has the sane contents as this list. Runs in + * worst-case linear time. + * + * @return {@code true} only if {@code o} is a list with the same contents + * as this list. + */ + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + + if (!(o instanceof List)) { + return false; + } + + int expectedModCount = modCount; + + List otherList = (List) o; + boolean equal = equalsRange(otherList, 0, size); + + checkForComodification(expectedModCount); + return equal; + } + + /** + * Returns {@code index}th element. Runs in the worst-case + * \(\mathcal{O}(\sqrt{n})\) time, but may run in \(\mathcal{O}(\sqrt{n})\) + * if the entropy of this list is high. + * + * @return {@code index}th element. + * @throws IndexOutOfBoundsException if the index is out of range + * {@code 0, 1, ..., size - 1}, or if this list is empty. + */ + @Override + public E get(int index) { + checkElementIndex(index); + return getNode(index).item; + } + + /** + * Computes and returns the entropy of this list, which is defined as + * \[ + * H = 1 - \frac{1}{N} \sum_{i = 0}^{n - 1} \Bigg|f_{i + 1} - f_i - \sqrt{N}\Bigg|. + * \] + * The larger the entropy, the faster the single-element operations should + * run. + * + * @return the entropy of this list. + */ + public double getEntropy() { + double sum = 0.0; + + for (int i = 0; i < fingerList.size(); i++) { + double value = fingerList.getFinger(i + 1).index + - fingerList.getFinger(i).index + - fingerList.size(); + + sum += Math.abs(value); + } + + return Math.max(0.0, 1.0 - sum / size); + } + + /** + * Returns the first element of this list. Runs in constant time. + * + * @return the first element of this list. + * @throws NoSuchElementException if this list is empty. + */ + @Override + public E getFirst() { + if (head == null) { + throw new NoSuchElementException( + "Getting the head element from an empty list."); + } + + return head.item; + } + + /** + * Returns the last element of this list. Runs in constant time. + * + * @return the last element of this list. + * @throws NoSuchElementException if this list is empty. + */ + @Override + public E getLast() { + if (tail == null) { + throw new NoSuchElementException( + "Getting the tail element from an empty list."); + } + + return tail.item; + } + + /** + * Returns the hash code of this list. Runs in linear time. + * + * @return the hash code of this list. + */ + @Override + public int hashCode() { + int expectedModCount = modCount; + int hash = hashCodeRange(0, size); + checkForComodification(expectedModCount); + return hash; + } + + /** + * Returns the index of the leftmost {@code obj}, or {@code -1} if + * {@code obj} does not appear in this list. Runs in worst-case linear time. + * + * @param obj the object to search. + * + * @return the index of the leftmost {@code obj}, or {@code -1} if + * {@code obj} does not appear in this list. + * + * @see IndexedLinkedList#lastIndexOf(java.lang.Object) + */ + @Override + public int indexOf(Object obj) { + return indexOfRange(obj, 0, size); + } + + /** + * Returns {@code true} only if this list is empty. + * + * @return {@code true} only if this list is empty. + */ + @Override + public boolean isEmpty() { + return size == 0; + } + + /** + * Returns the iterator over this list. + * + * @return the iterator over this list. + */ + @Override + public Iterator iterator() { + return new BasicIterator(); + } + + /** + * Returns the index of the rightmost {@code obj}, or {@code -1} if + * {@code obj} does not appear in this list. Runs in worst-case linear time. + * + * @param obj the object to search. + * + * @return the index of the rightmost {@code obj}, or {@code -1} if + * {@code obj} does not appear in this list. + * + * @see IndexedLinkedList#lastIndexOf(java.lang.Object) + */ + @Override + public int lastIndexOf(Object obj) { + return lastIndexOfRange(obj, 0, size); + } + + /** + * Returns the list iterator pointing to the head element of this list. + * + * @return the list iterator. + * @see java.util.ListIterator + */ + @Override + public ListIterator listIterator() { + return new EnhancedIterator(0); + } + + /** + * Returns the list iterator pointing between {@code list[index - 1]} and + * {@code list[index]}. + * + * @param index the gap index. The value of zero will point before the head + * element. + * + * @return the list iterator pointing to the {@code index}th gap. + */ + @Override + public ListIterator listIterator(int index) { + return new EnhancedIterator(index); + } + + /** + * Adds {@code e} after the tail element of this list. Runs in constant + * time. + * + * @param e the element to add. + * @return always {@code true}. + */ + @Override + public boolean offer(E e) { + return add(e); + } + + /** + * Adds {@code e} before the head element of this list. Runs in + * \(\mathcal{O}(\sqrt{n})\) time. + * + * @param e the element to add. + * @return always {@code true}. + */ + @Override + public boolean offerFirst(E e) { + addFirst(e); + return true; + } + + /** + * Adds {@code e} after the tail element of this list. Runs in constant + * time. + * + * @param e the element to add. + * @return always {@code true}. + */ + @Override + public boolean offerLast(E e) { + addLast(e); + return true; + } + + /** + * Moves all the fingers such that they are evenly distributed. Runs in + * linear time. + */ + public void optimize() { + distributeAllFingers(); + } + + /** + * Takes a look at the first element in this list. + * + * @return the head element or {@code null} if this list is empty. + */ + @Override + public E peek() { + return head == null ? null : head.item; + } + + /** + * Takes a look at the first element in this list. + * + * @return the head element or {@code null} if this list is empty. + */ + @Override + public E peekFirst() { + return head == null ? null : head.item; + } + + /** + * Takes a look at the last element in this list. + * + * @return the tail element or {@code null} if this list is empty. + */ + @Override + public E peekLast() { + return tail == null ? null : tail.item; + } + + /** + * If this list is empty, does nothing else but return {@code null}. + * Otherwise, removes the first element and returns it. Runs in + * \(\mathcal{O}(\sqrt{n})\) time. + * + * @return the first element (which was removed due to the call to this + * method), or {@code null} if the list is empty. + */ + @Override + public E poll() { + return head == null ? null : removeFirst(); + } + + /** + * If this list is empty, does nothing else but return {@code null}. + * Otherwise, removes the first element and returns it. Runs in + * \(\mathcal{O}(\sqrt{n})\) time. + * + * @return the first element (which was removed due to the call to this + * method), or {@code null} if the list is empty. + */ + @Override + public E pollFirst() { + return head == null ? null : removeFirst(); + } + + /** + * If this list is empty, does nothing else but return {@code null}. + * Otherwise, removes the last element and returns it. Runs in constant + * time. + * + * @return the last element (which was removed due to the call to this + * method), or {@code null} if the list is empty. + */ + @Override + public E pollLast() { + return tail == null ? null : removeLast(); + } + + /** + * Removes the first element and returns it. + * Runs in \(\mathcal{O}(\sqrt{n})\) time. + * + * @return the first element. + * @throws NoSuchElementException if the list is empty. + */ + @Override + public E pop() { + return removeFirst(); + } + + /** + * Adds {@code e} before the head of this list. Runs in + * \(\mathcal{O}(\sqrt{n})\) time. + */ + @Override + public void push(E e) { + addFirst(e); + } + + /** + * Randomizes the fingers. Used primarily for research. Uses the current + * milliseconds timestamp as the seed. + */ + public void randomizeFingers() { + randomizeFingers(System.currentTimeMillis()); + } + + /** + * Randomizes the fingers. Used primarily for research. + * + * @param random the random number generator object. + */ + public void randomizeFingers(Random random) { + final Set indexFilter = new HashSet<>(); + // Load the set of valid, random integers of size 'fingerList.size()': + while (indexFilter.size() < fingerList.size) { + indexFilter.add(random.nextInt(size)); + } + + // Sort the finger indices: + Integer[] newFingerIndexArray = new Integer[fingerList.size]; + newFingerIndexArray = indexFilter.toArray(newFingerIndexArray); + Arrays.sort(newFingerIndexArray); + + // Update the finger list: + for (int i = 0; i < fingerList.size; i++) { + Finger finger = fingerList.fingerArray[i]; + finger.index = newFingerIndexArray[i]; + finger.node = getNodeSequentially(finger.index); + } + } + + /** + * Randomizes the fingers. Used primarily for research. + * + * @param seed the random seed. + */ + public void randomizeFingers(long seed) { + randomizeFingers(new Random(seed)); + + } + + /** + * Removes and returns the first element. Runs in + * \(\mathcal{O}(\sqrt{n})\) time. + * + * @return the head element of this list. + * @throws NoSuchElementException if this list is empty. + */ + @Override + public E remove() { + return removeFirst(); + } + + /** + * Removes the element residing at the given index. Runs in worst-case + * \(\mathcal{O}(\sqrt{n})\) time. + * + * @param index the index of the element to remove. + * @return the removed element. (The one that resided at the index + * {@code index}.) + */ + @Override + public E remove(int index) { + checkElementIndex(index); + + // Get the closest finger: + int closestFingerIndex = fingerList.getClosestFingerIndex(index); + Finger closestFinger = fingerList.getFinger(closestFingerIndex); + + E returnValue; + Node nodeToRemove; + + if (closestFinger.index == index) { + // Once here, element with index 'index' is pointed by a finger: + nodeToRemove = closestFinger.node; + // Move the pointing finger out of the node to be removed: + moveFingerOutOfRemovalLocation(closestFinger, + closestFingerIndex); + } else { + // Keep the fingers at their original position. + // Find the target node. Effectively, 'steps' communicates how much + // steps we must do to the left: + int steps = index - closestFinger.index; + + // Once here, the closest finger does not point to the node to be + // removed. Traverse to the actual removal node: + nodeToRemove = + rewindFinger( + closestFinger, + steps); + + // Shift all the indices starting from 'closestFingerIndex + 1'th + // finger o ne position to the left (smaller indices): + fingerList.shiftFingerIndicesToLeftOnceAll(closestFingerIndex + 1); + + if (steps < 0) { + // Once here, we need to fix the index also of the + // 'closestFingerIndex'th finger: + fingerList.getFinger(closestFingerIndex).index--; + } + } + + // Actually unlink the target node: + returnValue = nodeToRemove.item; + unlink(nodeToRemove); + decreaseSize(); + + if (mustRemoveFinger()) { + // Once here, we can safely remove the last finger: + removeFinger(); + } + + return returnValue; + } + + /** + * Removes the leftmost occurrence of {@code o} in this list. Runs in worst- + * case \(\mathcal{O}(n + \sqrt{n})\) time. \(\mathcal{O}(n)\) for iterating + * the list and \(\mathcal{O}(\sqrt{n})\) time for fixing the fingers. + * + * @param o the object to remove. + * + * @return {@code true} only if {@code o} was located in this list and, + * thus, removed. + */ + @Override + public boolean remove(Object o) { + int index = 0; + + for (Node x = head; x != null; x = x.next, index++) { + if (Objects.equals(o, x.item)) { + removeObjectImpl(x, index); + return true; + } + } + + return false; + } + + /** + * Removes from this list all the elements mentioned in {@code c}. Runs in + * \(\mathcal{O}(n\sqrt{n} + fn)\) time, where \(\mathcal{O}(f)\) is the + * time of checking for element inclusion in {@code c}. + * + * @param c the collection holding all the elements to remove. + * @return {@code true} only if at least one element in {@code c} was + * located and removed from this list. + */ + @Override + public boolean removeAll(Collection c) { + return batchRemove(c, true, 0, size); + } + + /** + * Removes the first element from this list. Runs in + * \(\mathcal{O}(\sqrt{n})\) time. + * + * @return the first element. + */ + @Override + public E removeFirst() { + if (size == 0) { + throw new NoSuchElementException( + "removeFirst from an empty IndexedLinkedList"); + } + + return removeFirstImpl(); + } + + /** + * Removes the leftmost occurrence of {@code o}. Runs in worst-case + * \(\mathcal{O}(n)\) time. + * + * @return {@code true} only if {@code o} was present in the list and was + * successfully removed. + */ + @Override + public boolean removeFirstOccurrence(Object o) { + int index = 0; + + for (Node x = head; x != null; x = x.next, index++) { + if (Objects.equals(o, x.item)) { + removeObjectImpl(x, index); + return true; + } + } + + return false; + } + + /** + * Removes from this list all the elements that satisfy the given input + * predicate. Runs in \(\mathcal{O}(n\sqrt{n})\) time. + * + * @param filter the filtering predicate. + * @return {@code true} only if at least one element was removed. + */ + @Override + public boolean removeIf(Predicate filter) { + return removeIf(filter, 0, size); + } + + /** + * Removes and returns the last element of this list. Runs in constant time. + * + * @return the removed head element. + * @throws NoSuchElementException if this list is empty. + */ + @Override + public E removeLast() { + if (size == 0) { + throw new NoSuchElementException( + "removeLast on empty IndexedLinkedList"); + } + + return removeLastImpl(); + } + + /** + * Removes the rightmost occurrence of {@code o}. Runs in + * \(\mathcal{O}(n)\) time. + * + * @param o the object to remove. + * @return {@code true} only if an element was actually removed. + */ + @Override + public boolean removeLastOccurrence(Object o) { + int index = size - 1; + + for (Node x = tail; x != null; x = x.prev, index--) { + if (Objects.equals(o, x.item)) { + if (index == size - 1) { + removeLast(); + } else { + removeObjectImpl(x, index); + } + + return true; + } + } + + return false; + } + + /** + * Replaces all the elements in this list by applying the given input + * operator to each of the elements. Runs in linear time. + * + * @param operator the operator mapping one element to another. + */ + @Override + public void replaceAll(UnaryOperator operator) { + replaceAllRange(operator, 0, size); + modCount++; + } + + /** + * Remove all the elements that do not appear in + * {@code c}. Runs in worst-case \(\mathcal{O}(nf + n\sqrt{n})\) time, where + * the inclusion check in {@code c} is run in \(\mathcal{O}(f)\) time. + * + * @param c the collection of elements to retain. + * @return {@code true} only if at least one element was removed. + */ + @Override + public boolean retainAll(Collection c) { + return batchRemove(c, false, 0, size); + } + + /** + * Sets the element at index {@code index} to {@code element} and returns + * the old element. Runs in worst-case \(\mathcal{O}(\sqrt{n})\) time. + * + * @param index the target index. + * @param element the element to set. + * @return the previous element at the given index. + */ + @Override + public E set(int index, E element) { + checkElementIndex(index); + Node node = getNode(index); + E oldElement = node.item; + node.item = element; + return oldElement; + } + + /** + * Returns the number of elements in this list. + * + * @return the size of this list. + */ + @Override + public int size() { + return size; + } + + /** + * Sorts stably this list into non-descending order. Runs in + * \(\mathcal{O}(n \log n)\). + * + * @param c the element comparator. + */ + @Override + public void sort(Comparator c) { + if (size == 0) { + return; + } + + // Convert to an array and sort the array: + Object[] array = toArray(); + Arrays.sort((E[]) array, c); + + Node node = head; + + // Rearrange the items over the linked list nodes: + for (int i = 0; i < array.length; ++i, node = node.next) { + E item = (E) array[i]; + node.item = item; + } + + // Distribute all the fingers evenly: + distributeAllFingers(); + // Update the modification count: + modCount++; + } + + /** + * Returns the spliterator over this list. + */ + @Override + public Spliterator spliterator() { + return new LinkedListSpliterator<>(this, head, size, 0, modCount); + } + + /** + * Verifies that the contents of this indexed list and the {@code otherList} + * are the same, and their respective fingers lists are identical. Used for + * debugging. + * + * @param otherList the other indexed list. + * + * @return {@code true} if and only if the both lists are identical in + * structure. + */ + public boolean strongEquals(final IndexedLinkedList otherList) { + return equals(otherList) && fingerList.equals(otherList.fingerList); + } + + /** + * Returns a sublist view + * {@code list[fromIndex, fromIndex + 1, ..., toIndex - 1]}. + * + * @param fromIndex the smallest index, inclusive. + * @param toIndex the largest index, exclusive. + * @return the sublist view. + */ + @Override + public List subList(int fromIndex, int toIndex) { + subListRangeCheck(fromIndex, toIndex, size); + return new EnhancedSubList(this, fromIndex, toIndex); + } + + /** + * Returns the {@link Object} array containing all the elements in this + * list, in the same order as they appear in the list. + * + * @return the list contents in an {@link Object} array. + */ + @Override + public Object[] toArray() { + Object[] arr = new Object[size]; + int index = 0; + + for (Node node = head; node != null; node = node.next) { + arr[index++] = node.item; + } + + return arr; + } + + /** + * If {@code a} is sufficiently large, returns the same array holding all + * the contents of this list. Also, if {@code a} is larger than the input + * array, sets {@code a[s] = null}, where {@code s} is the size of this + * list. However, if {@code a} is smaller than this list, allocates a new + * array of the same length, populates it with the list contents and returns + * it. + * + * @param the element type. + * @param a the input array. + * @return an array holding the contents of this list. + */ + @SuppressWarnings("unchecked") + @Override + public T[] toArray(T[] a) { + if (a.length < size) { + // Once here, we need a larger array: + a = (T[]) Array.newInstance(a.getClass().getComponentType(), size); + } + + int index = 0; + + // Copy the contents into the array: + for (Node node = head; node != null; node = node.next) { + a[index++] = (T) node.item; + } + + if (a.length > size) { + // Once here, mark the end of the data as 'null': + a[size] = null; + } + + return a; + } + + /** + * Returns the string representation of this list, listing all the elements. + * + * @return the string representation of this list. + */ + @Override + public String toString() { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("["); + + // Indicates that we are before the first iteration: + boolean firstIteration = true; + + for (E element : this) { + if (firstIteration) { + firstIteration = false; + } else { + // Once here, this iteration is not the first one. Add a comma: + stringBuilder.append(", "); + } + + stringBuilder.append(element); + } + + return stringBuilder.append("]").toString(); + } + + /** + * Implements the batch remove. If {@code complement} is {@code true}, this + * operation removes all the elements appearing in {@code c}. Otherwise, it + * will retain all the elements present in {@code c}. + * + * @param c the target collection to operate on. + * @param complement the operation choice flag. + * @param from the starting, inclusive index of the range to consider. + * @param end the ending, exclusive index of the range to consider. + * + * @return {@code true} if and only if the list was modified. + */ + boolean batchRemove(Collection c, + boolean complement, + int from, + int end) { + Objects.requireNonNull(c); + + if (c.isEmpty()) { + // Once here, there is nothing to remove. Return false: + return false; + } + + boolean modified = false; + + // The number of nodes to process: + int numberOfNodesToIterate = end - from; + int i = 0; + int nodeIndex = from; + + for (Node node = getNode(from); i < numberOfNodesToIterate; ++i) { + Node nextNode = node.next; + + if (c.contains(node.item) == complement) { + // Once here, we have a match. Remove it and mark 'modified' as + // 'true': + modified = true; + removeObjectImpl(node, nodeIndex); + } else { + // Omit the element. We need this in order for + // 'removeObjectImpl(node, nodeIndex)' to work properly: + nodeIndex++; + } + + node = nextNode; + } + + return modified; + } + + /** + * Checks that the input {@code expectedModCount} equals the list's + * internal, cached modification count. + * + * @param expectedModCount the modification count to check. + * @throws ConcurrentModificationException if the cached and the input + * modification counts differ. + */ + void checkForComodification(int expectedModCount) { + if (modCount != expectedModCount) { + throw new ConcurrentModificationException(); + } + } + + /** + * Validates the range indices. + * + * @param fromIndex the starting, inclusive index of the range. + * @param toIndex the ending, exclusive index of the range. + */ + void checkFromTo(int fromIndex, int toIndex) { + if (fromIndex < 0) { + throw new IndexOutOfBoundsException( + String.format("fromIndex(%d) < 0", fromIndex)); + } + + if (toIndex > size) { + throw new IndexOutOfBoundsException( + String.format("toIndex(%d) > size(%d)", toIndex, size)); + } + + if (fromIndex > toIndex) { + throw new IllegalArgumentException( + String.format( + "fromIndex(%d) > toIndex(%d)", + fromIndex, + toIndex)); + } + } + + /** + * Checks that the input index is within correct bounds. + * + * @param index the index to check. + * @param size the size of the list. + */ + static void checkIndex(int index, int size) { + + if (size < 0) { + throw new IllegalArgumentException( + String.format("size(%d) < 0", size)); + } + + if (index < 0) { + throw new IndexOutOfBoundsException( + String.format("index(%d) < 0", index)); + } + + if (index >= size) { + throw new IndexOutOfBoundsException( + String.format("index(%d) >= size(%d)", index, size)); + } + } + + /** + * Returns the number of fingers in the finger list. Does not count the + * end-of-finger-list sentinel finger. Used in unit tests. + * + * @return the size of the finger list. + */ + int getFingerListSize() { + return fingerList.size(); + } + + // TODO: Benchmark this! + /** + * Accesses the {@code index}th node sequentially without relying on + * fingers. Used in {@link #randomizeFingers()}. + * + * @param index the index of the desired node. + * + * @return the {@code index}th node. + */ + Node getNodeSequentially(int index) { + // This is the so called "nearest neighbor optimization": + if (index < size / 2) { + // Once here, 'index'th element is closer to the head node. Iterate + // starting from the head node forward: + Node node = head; + + for (int i = 0; i < index; i++) { + node = node.next; + } + + return node; + } else { + // Once here, 'index'th element is closer to the tail node. Iterate + // starting from the tail node backwards: + Node node = tail; + + for (int i = 0; i < size - 1 - index; i++) { + node = node.prev; + } + + return node; + } + } + + /** + * Loads the two counters representing how much fingers we should push to + * left and how much fingers to push to right. + * + * @param fromFingerIndex the lower finger index. + * @param toFingerIndex the upper, exclusive finger index. + * @param fromIndex the starting, inclusive element index of the + * range. + * @param toIndex the ending, exclusive element index of the range. + * @param fingersToRemove the number of fingers to remove. + */ + void loadFingerCoverageCounters(int fromFingerIndex, + int toFingerIndex, + int fromIndex, + int toIndex, + int fingersToRemove) { + + // Compute the number of fingers in the finger list prefix and suffix: + int fingerPrefixLength = fromFingerIndex; + int fingerSuffixLength = fingerList.size() - toFingerIndex; + + // Compute the lengths of elements in the acutual list prefix and suffx: + int listPrefixFreeSpots = fromIndex; + int listSuffixFreeSpots = size - toIndex; + + // Compute the number of free spots for fingers in both list prefix and + // suffix: + int freeFingerPrefixSpots = listPrefixFreeSpots + - fingerPrefixLength; + + int freeFingerSuffixSpots = listSuffixFreeSpots + - fingerSuffixLength; + + // Compute the total number of free spots for fingers: + int freeSpots = freeFingerPrefixSpots + + freeFingerSuffixSpots; + + // Compute the ratio between free prefix finger spots and the toal + // number of free spots: + float leftRatio = (float)(freeFingerPrefixSpots) / + (float)(freeSpots); + + // Compute the length of the finger list that will be removed: + int removalRangeLength = toFingerIndex + - fromFingerIndex; + + // Compute the number of fingers left to distribute to the finger list + // prefix/suffix: + int remainingFingers = removalRangeLength + - fingersToRemove; + + // Finally, compute the number of fingers going to prefix/suffix: + int leftCoveredFingers = (int)(leftRatio * remainingFingers); + int rightCoveredFingers = remainingFingers - leftCoveredFingers; + + this.numberOfCoveringFingersToPrefix = leftCoveredFingers; + this.numberOfCoveringFingersToSuffix = rightCoveredFingers; + } + + /** + * Moves the {@code finger} out of the element with index + * {@code finger.index}. + * + * @param finger the finger to move. + * @param fingerIndex the index of {@code finger}. + */ + void moveFingerOutOfRemovalLocation(Finger finger, int fingerIndex) { + + if (fingerList.size() == size()) { + // Here, fingerList.size() is 1 or 2 and the size of the list is the + // same: + if (fingerList.size() == 1) { + // The only finger will be removed in 'remove(int)'. Return: + return; + } + + if (fingerIndex == 0) { + // Shift 2nd and the sentinal fingers one position to the + // left: + fingerList.setFinger(0, fingerList.getFinger(1)); + fingerList.getFinger(0).index = 0; + fingerList.setFinger(1, fingerList.getFinger(2)); + fingerList.getFinger(1).index = 1; + fingerList.setFinger(2, null); + fingerList.size = 1; + } else { + // Here, fingerIndex == 1: + // Just remove the (last) finger: + fingerList.removeFinger(); + fingerList.getFinger(1).index = 1; + } + + return; + } + + // Try push the fingers to the right: + if (tryPushFingersToRight(fingerIndex)) { + // Once here, pushing to the right was successful. Return: + return; + } + + // Could not push the fingers to the right. Try push to the left: + if (tryPushFingersToLeft(fingerIndex)) { + // Once here, pushing to the left was successful. Return: + return; + } + + // Once here, the only free spots are at the very beginning of the + // finger list: + for (int i = 0; i <= fingerIndex; ++i) { + Finger fngr = fingerList.getFinger(i); + // Move 'fngr' one spot to the left: + fngr.index--; + fngr.node = fngr.node.prev; + } + + // Fix the remaining indices: + fingerList.shiftFingerIndicesToLeftOnceAll(fingerIndex + 1); + } + + /** + * Removes the list range {@code [fromIndex, ..., toIndex - 1]}. + * + * @param fromIndex the staring, inclusive range index. + * @param toIndex the ending, exclusive range index. + */ + void removeRange(int fromIndex, int toIndex) { + int removalLength = toIndex - fromIndex; + + if (removalLength == 0) { + // Once here, nothing to remove: + return; + } + + if (removalLength == 1) { + // Delegate to the single-element removal method: + remove(fromIndex); + return; + } + + if (removalLength == size) { + // Can simply clear all the contents: + clear(); + return; + } + + // Compute the bounding finger indices: + int fromFingerIndex = fingerList.getFingerIndexImpl(fromIndex); + int toFingerIndex = fingerList.getFingerIndexImpl(toIndex); + + // Compute the number of fingers to remove: + int fingersToRemove = getRecommendedNumberOfFingers() + - getRecommendedNumberOfFingers( + size - removalLength); + + // Load the end nodes of the actual range removal area: + loadRemoveRangeEndNodes(fromIndex, + toIndex); + + // Do the actual finger list magic: + removeRangeImpl(fromIndex, + toIndex, + fromFingerIndex, + toFingerIndex, + fingersToRemove); + + // Unlink the actual nodes: + unlinkNodeRange(this.removeRangeStartNode, + this.removeRangeEndNode); + modCount++; + + // Attempt to contract the finger array: + fingerList.contractFingerArrayIfNeeded(size); + } + + /** + * Replaces all the elements from range {@code [i, end - 1]}. + * + * @param operator the replacement operator. + * @param i the starting, inclusive index of the range to replace. + * @param end the ending, exclusive index of the range to replace. + */ + void replaceAllRange(UnaryOperator operator, int i, int end) { + Objects.requireNonNull(operator); + int expectedModCount = modCount; + Node node = getNode(i); + + while (modCount == expectedModCount && i < end) { + node.item = operator.apply(node.item); + node = node.next; + i++; + } + + if (modCount != expectedModCount) { + throw new ConcurrentModificationException(); + } + } + + /** + * Checks the indices for a list of size {@code size}. + * + * @param fromIndex the starting, inclusive index. + * @param toIndex the ending, exclusive index. + * @param size the size of the target list. + */ + static void subListRangeCheck(int fromIndex, + int toIndex, + int size) { + if (fromIndex < 0) { + throw new IndexOutOfBoundsException("fromIndex = " + fromIndex); + } + + if (toIndex > size){ + throw new IndexOutOfBoundsException( + "toIndex(" + toIndex + ") > size(" + size + ")"); + } + + if (fromIndex > toIndex) + throw new IllegalArgumentException( + "fromIndex(" + fromIndex + ") > toIndex(" + + toIndex + ")"); + } + + /** + * Adds the fingers for the range just appended. + * + * @param firstNode the first node of the added collection. + * @param firstIndex the index of {@code first}. + * @param collectionSize the size of the added collection. + */ + private void addFingersAfterAppendAll( + Node firstNode, + int firstIndex, + int collectionSize) { + + int numberOfNewFingers = + getRecommendedNumberOfFingers() - fingerList.size(); + + if (numberOfNewFingers == 0) { + fingerList.getFinger(fingerList.size()).index += collectionSize; + return; + } + + int fingerIndex = fingerList.size(); + + fingerList.makeRoomAtIndex(fingerIndex, + numberOfNewFingers, + collectionSize); + + int distance = collectionSize / numberOfNewFingers; + + spreadFingers(firstNode, + firstIndex, + fingerIndex, + numberOfNewFingers, + distance); + } + + /** + * Adds fingers after inserting a collection in this list. + * + * @param headNodeOfInsertedRange the head node of the inserted range. + * @param indexOfInsertedRangeHead the index of + * {@code headNodeOfInsertedRange}. + * @param collectionSize the size of the inserted collection. + */ + private void addFingersAfterInsertAll(Node headNodeOfInsertedRange, + int indexOfInsertedRangeHead, + int collectionSize) { + + // The number of new fingers to add: + int numberOfNewFingers = + getRecommendedNumberOfFingers() - fingerList.size(); + + int startFingerIndex = + fingerList.getFingerIndexImpl(indexOfInsertedRangeHead); + + if (numberOfNewFingers == 0) { + // TODO + // Once here, nothing to do. Just add 'collectionSize' to all the + // indices of the affected fingers, starting from the + // 'fingerIndex'th finger in the finger list: + fingerList.shiftFingerIndicesToRight(startFingerIndex, + collectionSize); + return; + } + + // Make room for 'numberOfNewFingers' fingers starting from + // 'startFingerIndex': + fingerList.makeRoomAtIndex(startFingerIndex, + numberOfNewFingers, + collectionSize); + + // Compute the distance between new fingers to add: + int distance = collectionSize / numberOfNewFingers; + + // Distribute the new fingers: + spreadFingers(headNodeOfInsertedRange, + indexOfInsertedRangeHead, + startFingerIndex, + numberOfNewFingers, + distance); + } + + /** + * Adds fingers after prepending a collection to this list. + * + * @param collectionSize the size of the prepended collection. + */ + private void addFingersAfterPrependAll(int collectionSize) { + + // The number of new fingers to add: + int numberOfNewFingers = + getRecommendedNumberOfFingers() - fingerList.size(); + + if (numberOfNewFingers == 0) { + // Updates all the indices of all the fingers by adding + // 'collectionSize' to the each index: + fingerList.shiftFingerIndicesToRight(0, collectionSize); + return; + } + + // Makes room for 'numberOfNewFingers' fingers at the very beginning of + // the finger list: + fingerList.makeRoomAtIndex(0, + numberOfNewFingers, + collectionSize); + + // Compute the distance between new fingers to add: + int distance = collectionSize / numberOfNewFingers; + + // Distribute the new fingers: + spreadFingers(head, + 0, + 0, + numberOfNewFingers, + distance); + } + + /** + * Adds fingers after setting a collection as a list. + * + * @param collectionSize the size of the collection being set. + */ + private void addFingersAfterSetAll(int collectionSize) { + int numberOfNewFingers = getRecommendedNumberOfFingers(); + + // Here, we never have 'numberOfNewFingers == 0' since we set a + // non-empty collection that will require at least one finger! + fingerList.enlargeFingerArrayWithEmptyRange(numberOfNewFingers + 1, + 0, + numberOfNewFingers, + collectionSize); + int distance = size / numberOfNewFingers; + + spreadFingers(head, + 0, + 0, + numberOfNewFingers, + distance); + } + + /** + * Appends the input collection to the tail of this list. + * + * @param c the collection to append. + */ + private void appendAll(Collection c) { + Node prev = tail; + Node oldLast = tail; + + for (E item : c) { + Node newNode = new Node<>(item); + newNode.prev = prev; + prev.next = newNode; + prev = newNode; + } + + tail = prev; + int sz = c.size(); + size += sz; + modCount++; + + addFingersAfterAppendAll(oldLast.next, + size - sz, + sz); + } + + /** + * Adds the finger to the tail of the finger list and before the + * end-of-finger-list sentinel. + * + * @param node the node of the newly created finger. + * @param index the index of {@code node}. + */ + private void appendFinger(Node node, int index) { + Finger finger = new Finger<>(node, index); + fingerList.appendFingerImpl(finger); + } + + /** + * Checks the element index. In the case of non-empty list, valid indices + * are {@code { 0, 1, ..., size - 1 }}. + * + * @param index the index to validate. + * @throws IndexOutOfBoundsException if the {@code index} is not a valid + * list index. + */ + private void checkElementIndex(int index) { + if (!isElementIndex(index)) { + throw new IndexOutOfBoundsException(getOutOfBoundsMessage(index)); + } + } + + /** + * Checks that the input index is a valid position index for + * {@link #add(int, java.lang.Object)} operation or iterator position. In + * other words, checks that {@code index} is in the set + * {@code {0, 1, ..., size}}. + * + * @param index the index to validate. + */ + private void checkPositionIndex(int index) { + if (!isPositionIndex(index)) { + throw new IndexOutOfBoundsException(getOutOfBoundsMessage(index)); + } + } + + /** + * Decreases the size counter and increments the modification count. + */ + private void decreaseSize() { + size--; + modCount++; + } + + /** + * Distributes evenly all the fingers over this list. + */ + private void distributeAllFingers() { + distributeFingers(0, size); + } + + /** + * Checks that the list {@code other} matches {@code this[from ... to - 1]}. + * + * @param other the target list to compare to. + * @param from the starting, inclusive index of the range in this list. + * @param to the ending, exclusive index of the range in this list. + * + * @return {@code true} if and only if {@code other} equals + * {@code this[from ... to - 1]}. + */ + private boolean equalsRange(List other, int from, int to) { + int rangeLength = to - from; + + if (rangeLength != other.size()) { + return false; + } + + Iterator otherIterator = other.iterator(); + + for (Node node = nodeNoFingerFixing(from); + from < to; + from++, node = node.next) { + + if (!Objects.equals(node.item, otherIterator.next())) { + return false; + } + } + + return true; + } + + /** + * Constructs an IndexOutOfBoundsException detail message. + * + * @param index the target index. + * @return the detail message. + */ + private String getOutOfBoundsMessage(int index) { + return "Index: " + index + ", Size: " + size; + } + + /** + * Computes the recommended number of fingers. + * + * @return the recommended number of fingers. Equals + * \(\Bigg\lceil\sqrt{N}\Bigg\rceil\), where \(N\) is {@code size}. + */ + private int getRecommendedNumberOfFingers() { + return (int) Math.ceil(Math.sqrt(this.size)); + } + + /** + * Computes the recommended number of fingers for {@code size} elements. + * Equals \(\Bigg\lceil \sqrt{N} \Bigg\rceil\), where \(N = \) {@code size}. + * + * @param size the size for which we want to compute the recommended number + * of fingers. + * + * @return the recommended number of fingers. + */ + private static int getRecommendedNumberOfFingers(int size) { + return (int) Math.ceil(Math.sqrt(size)); + } + + /** + * Computes the hash code for the range {@code this[from, to - 1]} and + * returns it. + * + * @param from the starting, inclusive index. + * @param to the ending, exclusive index. + * @return the hash value of the range. + */ + private int hashCodeRange(int from, int to) { + int hashCode = 1; + + Node node = getNode(from); + + while (from++ < to) { + // Same arithmetics as in ArrayList. + hashCode = + 31 * hashCode + + (node.item == null ? 0 : node.item.hashCode()); + + node = node.next; + } + + return hashCode; + } + + /** + * Increases the size of the list and its modification count. + */ + private void increaseSize() { + ++size; + ++modCount; + } + + /** + * Returns the index of the leftmost occurrence of the object {@code o} in + * the range {@code this[start ... end - 1]}. + * + * @param o the object to search. May be {@code null}. + * @param start the starting, inclusive index of the range to search. + * @param end the ending, exclusive index of the range to search. + * @return the leftmost occurrence index. + */ + private int indexOfRange(Object o, int start, int end) { + int index = start; + + if (o == null) { + for (Node node = getNode(start); + index < end; + index++, node = node.next) { + if (node.item == null) { + return index; + } + } + } else { + for (Node node = getNode(start); + index < end; + index++, node = node.next) { + if (o.equals(node.item)) { + return index; + } + } + } + + return -1; + } + + /** + * Inserts the input collection right before the node {@code succ}. + * + * @param c the collection to insert. + * @param succ the node that is right before the end of the inserted + * collection. + * @param succIndex the appearance index of {@code succ} in the list. + */ + private void insertAll(Collection c, + Node succ, + int succIndex) { + + Node pred = succ.prev; + Node prev = pred; + + for (E item : c) { + // Keep inserting: + Node newNode = new Node<>(item); + newNode.prev = prev; + prev.next = newNode; + prev = newNode; + } + + // Postprocess the insertion: + prev.next = succ; + succ.prev = prev; + + int sz = c.size(); + modCount++; + size += sz; + + // Add possibly fingers: + addFingersAfterInsertAll(pred.next, + succIndex, + sz); + } + + /** + * Tells if the argument is the index of an existing element. The index is + * valid if it is in the set \(\{0, 1, ..., \) {@code size}\( - 1\}\). + * + * @param index the index to validate. + * @return {@code true} if and only if the index is valid. + */ + private boolean isElementIndex(int index) { + return index >= 0 && index < size; + } + + /** + * Tells if the argument is the index of a valid position for an iterator or + * an add operation. The index is valid if it is in set + * \(\{ 0, 1, ..., \) {@code size}\(\}\). + * + * @param index the index to validate. + * @return {@code true} if and only if the index is valid. + */ + private boolean isPositionIndex(int index) { + return 0 <= index && index <= size; + } + + /** + * Returns the last appearance index of {@code obj} or {@code -1} if the + * {@code o} is not in this list. + * + * @param o the object to search for. + * @param start the starting, inclusive index of the range to search. + * @param end the ending, exclusive index of the range to search. + * @return the index of the rightmost appearance of {@code o} or {@code -1} + * if there is no such. + */ + private int lastIndexOfRange(Object o, int start, int end) { + int index = end - 1; + + if (o == null) { + for (Node node = getNode(index); + index >= start; + index--, node = node.prev) { + if (node.item == null) { + return index; + } + } + } else { + for (Node node = getNode(index); + index >= start; + index--, node = node.prev) { + if (o.equals(node.item)) { + return index; + } + } + } + + return -1; + } + + /** + * This inner class implements a basic iterator over this list. + */ + final class BasicIterator implements Iterator { + + /** + * Caches the most recently returned node. + */ + private Node lastReturned; + + /** + * Caches the next node to iterate over. + */ + private Node next = head; + + /** + * The index of the next node to iterate over. + */ + private int nextIndex; + + /** + * Caches the expected modification count. We use this value in order to + * detect the concurrent modifications as early as possible. + */ + int expectedModCount = IndexedLinkedList.this.modCount; + + /** + * Returns {@code true} if and only if this iterator has more elements + * to offer. + * + * @return {@code true} if and only if iteration may continue. + */ + @Override + public boolean hasNext() { + return nextIndex < size; + } + + /** + * Attempts to return the next element in the iteration order. + * + * @return the next element. + * @throws ConcurrentModificationException if the list was modified + * outside the iterator API. + */ + @Override + public E next() { + checkForComodification(); + + if (!hasNext()) { + throw new NoSuchElementException(); + } + + lastReturned = next; + next = next.next; + nextIndex++; + return lastReturned.item; + } + + /** + * Removes the most recently iterated element from the list. + * + * @throws IllegalStateException if there is no most recent element. + * This can happen if there was no + * {@link DescendingIterator#next()} + * on this iterator, or the previous + * operation was + * {@link DescendingIterator#remove()}. + */ + @Override + public void remove() { + if (lastReturned == null) { + throw new IllegalStateException(); + } + + checkForComodification(); + + int removalIndex = nextIndex - 1; + removeObjectImpl(lastReturned, removalIndex); + nextIndex--; + lastReturned = null; + expectedModCount++; + } + + /** + * Runs {@code action} on every remaining elements. + * + * @param action the action to perform on each remaining element. + * @throws ConcurrentModificationException if the list was modified + * outside the iterator API. + */ + @Override + public void forEachRemaining(Consumer action) { + Objects.requireNonNull(action); + + while (modCount == expectedModCount && nextIndex < size) { + action.accept(next.item); + next = next.next; + nextIndex++; + } + + checkForComodification(); + } + + /** + * Makes sure that the list was not modified outside of the iterator API + * while iterating. + */ + void checkForComodification() { + if (modCount != expectedModCount) { + throw new ConcurrentModificationException(); + } + } + } + + /** + * This inner class implements the descending list iterator over this list. + */ + final class DescendingIterator implements Iterator { + + /** + * The most recently returned element. + */ + private Node lastReturned; + + /** + * The next node to iterate. + */ + private Node nextToIterate = tail; + + /** + * The index of the node next to iterate. + */ + private int nextIndex = IndexedLinkedList.this.size - 1; + + /** + * The cached expected modification count. Used to detect the situations + * where the list is modified outside iterator API during iteration. + */ + int expectedModCount = IndexedLinkedList.this.modCount; + + /** + * Returns {@code true} if and only if this iterator has more elements + * to offer. + * + * @return {@code true} if and only if there is more to iterate. + */ + @Override + public boolean hasNext() { + return nextIndex > -1; + } + + /** + * Returns the next element in the iteration order. + * + * @return the next element. + * @throws ConcurrentModificationException if the underlying list was + * modified outside the iterator + * API. + */ + @Override + public E next() { + checkForComodification(); + + if (!hasNext()) { + throw new NoSuchElementException(); + } + + lastReturned = nextToIterate; + nextToIterate = nextToIterate.prev; + nextIndex--; + return lastReturned.item; + } + + /** + * Removes the most recently returned element from the list. + * + * @throws IllegalStateException if there is no most recent element. + * This can happen if there was no + * {@link DescendingIterator#next()} + * on this iterator, or the previous + * operation was + * {@link DescendingIterator#remove()}. + */ + @Override + public void remove() { + if (lastReturned == null) { + throw new IllegalStateException(); + } + + checkForComodification(); + + removeObjectImpl(lastReturned, nextIndex + 1); + lastReturned = null; + expectedModCount++; + } + + /** + * Iterates over all remaining elements in the descendinig iteration + * order. + * + * @param action the action to call for each iterated element. + */ + @Override + public void forEachRemaining(Consumer action) { + Objects.requireNonNull(action); + + while (modCount == expectedModCount && hasNext()) { + action.accept(nextToIterate.item); + nextToIterate = nextToIterate.prev; + nextIndex--; + } + + checkForComodification(); + } + + /** + * Makes sure that the expected modification count equals the iterator's + * modification count, and throws an exception if that is not the case. + * + * @throws ConcurrentModificationException if the modification counts do + * not match. + */ + private void checkForComodification() { + if (modCount != expectedModCount) { + throw new ConcurrentModificationException(); + } + } + } + + /** + * This inner class implements the enhanced list iterator over this list. + */ + final class EnhancedIterator implements ListIterator { + + /** + * The most recently iterated node. + */ + private Node lastReturned; + + /** + * The next node to iterate. + */ + private Node next; + + /** + * The index of {@code next} in the list. + */ + private int nextIndex; + + /** + * The flag indicating that the previous iterator move to the left. + */ + private boolean previousMoveToLeft; + + /** + * The expected modification count. Package-private for the sake of unit + * testing. + */ + int expectedModCount = modCount; + + /** + * Constructs a new enhanced list iterator starting from the node with + * index {@code index}. + * + * @param index the starting node index. + */ + EnhancedIterator(int index) { + next = (index == size) ? null : getNode(index); + nextIndex = index; + } + + /** + * Returns {@code true} if and only if there is more to iterate. + * + * @return {@code true} if and only if there is more to iterate. + */ + @Override + public boolean hasNext() { + return nextIndex < size; + } + + /** + * Returns the next element in the forward iteration order. + * + * @return the next element in forward direction. + * @throws ConcurrentModificationException if the list was modified + * outside the iterator API. + * @throws NoSuchElementException if there is no elements to iterate. + */ + @Override + public E next() { + checkForComodification(); + + if (!hasNext()) { + throw new NoSuchElementException(); + } + + lastReturned = next; + next = next.next; + nextIndex++; + previousMoveToLeft = false; + return lastReturned.item; + } + + /** + * Returns {@code true} if and only if there is a previous element in + * the forward iteration order. + * + * @return {@code true} if and only if there is a previous element. + */ + @Override + public boolean hasPrevious() { + return nextIndex > 0; + } + + /** + * Returns the previous element in the forward iteration order. + * + * @return the previous element in forward direction. + * @throws ConcurrentModificationException if the list was modified + * outside the iterator API. + * @throws NoSuchElementException if there is no elements to iterate. + */ + @Override + public E previous() { + checkForComodification(); + + if (!hasPrevious()) { + throw new NoSuchElementException(); + } + + lastReturned = next = (next == null) ? tail : next.prev; + nextIndex--; + previousMoveToLeft = true; + return lastReturned.item; + } + + /** + * Returns the index of the element next to iterate. + * + * @return the index of the next element. + */ + @Override + public int nextIndex() { + return nextIndex; + } + + /** + * Returns the index of the element previously iterated. + * + * @return the index of the previous element. + */ + @Override + public int previousIndex() { + return nextIndex - 1; + } + + /** + * Removes the most recently iterated element from the list. + * + * @throws ConcurrentModificationException if the underlying list was + * was modified outside the + * iterator API. + * @throws IllegalStateException if there is no previously iterated + * element. This can happen if + * {@link ListIterator#previous()} or + * {@link ListIterator#next()} was not + * called yet, or the previous operation + * was + * {@link ListIterator#add(java.lang.Object)} + * or {@link ListIterator#remove()}. + * + */ + @Override + public void remove() { + checkForComodification(); + + if (lastReturned == null) { + throw new IllegalStateException(); + } + + Node lastNext = lastReturned.next; + int removalIndex = previousMoveToLeft ? nextIndex : nextIndex - 1; + removeObjectImpl(lastReturned, removalIndex); + + if (next == lastReturned) { + next = lastNext; + } else { + nextIndex = removalIndex; + } + + lastReturned = null; + expectedModCount++; + } + + /** + * Sets the element currently pointed by this iterator. + * + * @param e the element to set. + * @throws IllegalStateException if there is no current element. + * @throws ConcurrentModificationException if the underlying list was + * modified outside the iterator + * API. + */ + @Override + public void set(E e) { + if (lastReturned == null) { + throw new IllegalStateException(); + } + + checkForComodification(); + lastReturned.item = e; + } + + /** + * Adds the element {@code e} right after the previously iterated + * element. + * + * @param e the element to add. + */ + @Override + public void add(E e) { + checkForComodification(); + + lastReturned = null; + + if (next == null) { + linkLast(e); + } else if (next.prev == null) { + linkFirst(e); + } else { + linkBefore(e, nextIndex, next); + } + + nextIndex++; + expectedModCount++; + } + + /** + * Iterates over the remaining elements in the forward iteration order. + * + * @param action the action to apply to each remaining element. + */ + @Override + public void forEachRemaining(Consumer action) { + Objects.requireNonNull(action); + + while (modCount == expectedModCount && nextIndex < size) { + action.accept(next.item); + next = next.next; + nextIndex++; + } + + checkForComodification(); + } + + /** + * Checks that the expected modification count matches the modification + * count of the underlying list. + */ + private void checkForComodification() { + if (modCount != expectedModCount) { + throw new ConcurrentModificationException(); + } + } + } + + /** + * Links the input element right before the node {@code succ}. + * + * @param e the element to link. + * @param index the index of {@code e}. + * @param succ the node before which to link the {@code e}'s node. + */ + private void linkBefore(E e, int index, Node succ) { + Node pred = succ.prev; + Node newNode = new Node<>(e); + + // Link: + newNode.next = succ; + newNode.prev = pred; + succ.prev = newNode; + pred.next = newNode; + + increaseSize(); + + if (mustAddFinger()) { + // Insert the new finger pointing to 'newNode' to the finger list: + fingerList.insertFingerAndShiftOnceToRight( + new Finger<>(newNode, index)); + } else { + // Get the closest, upper finger's index: + int fingerIndex = fingerList.getFingerIndexImpl(index); + // Use the above finger index to add one (1) to the index of each + // finger residing starting from 'fingerIndex': + fingerList.shiftFingerIndicesToRightOnce(fingerIndex); + } + } + + /** + * Prepends the input element to the head of this list. + * + * @param e the element to prepend. + */ + private void linkFirst(E e) { + // Link to the head: + Node oldFirst = head; + Node newNode = new Node<>(e); + newNode.next = oldFirst; + head = newNode; + + if (oldFirst == null) { + // Once here, the list was empty before calling this method. Update + // the 'null' tail: + tail = newNode; + } else { + // Otherwise, just link the old head node to the new node: + oldFirst.prev = newNode; + } + + increaseSize(); + + if (mustAddFinger()) { + // Prepend a new finger indicating to the 'newNode': + fingerList.prependFingerForNode(newNode); + } else { + // Just update all the finger indices: + fingerList.shiftFingerIndicesToRightOnce(0); + } + } + + /** + * Appends the input element to the tail of this list. + * + * @param e the element to append. + */ + private void linkLast(E e) { + // Link: + Node oldTail = tail; + Node newNode = new Node<>(e); + newNode.prev = oldTail; + tail = newNode; + + if (oldTail == null) { + // Once here, the list was empty prior to calling this method. + // Update the 'null' head: + head = newNode; + } else { + // Otherwise, just link the old tail node to the new node: + oldTail.next = newNode; + } + + increaseSize(); + + if (mustAddFinger()) { + // Once here, just append the new finger: + appendFinger(newNode, size - 1); + } else { + // Once here, just increment the index of the end-of-finger-list + // sentinel fingers index: + fingerList.getFinger(fingerList.size()).index++; + } + } + + /** + * Loads the endpoint nodes for the range to remove. + * + * @param fromIndex the left index, inclusive. + * @param toIndex the right index, exclusive. + */ + private void loadRemoveRangeEndNodes(int fromIndex, int toIndex) { + // Compute the end-point nodes: + Node startNode = fingerList.getNodeNoFingersFix(fromIndex); + Node endNode = fingerList.getNodeNoFingersFix(toIndex); + + if (endNode == null) { + // We may get here, if the index 'toIndex' points after the last + // finger in the finger list: + endNode = tail; + } else { + // Move to the node that is the very last node of the range that is + // about to be removed: + endNode = endNode.prev; + } + + this.removeRangeStartNode = startNode; + this.removeRangeEndNode = endNode; + } + + /** + * Returns {@code true} if and only if this list requires more fingers. + * + * @return {@code true} if and only if this list requires more fingers. + */ + private boolean mustAddFinger() { + // Here, fingerStack.size() == getRecommendedFingerCount(), or, + // fingerStack.size() == getRecommendedFingerCount() - 1 + return fingerList.size() != getRecommendedNumberOfFingers(); + } + + /** + * Returns {@code true} if and only if this list requires less fingers. + * + * @return {@code true} if and only if this list requires less fingers. + */ + private boolean mustRemoveFinger() { + // Here, fingerStack.size() == getRecommendedFingerCount(), or, + // fingerStack.size() == getRecommendedFingerCount() + 1 + return fingerList.size() != getRecommendedNumberOfFingers(); + } + + /** + * Returns the node at index {@code elementIndex}. + * + * @param elementIndex the index of the target element. + * @return the node containing the target element. + */ + private Node getNode(int elementIndex) { + return fingerList.getNode(elementIndex); + } + + /** + * Returns the node at index {@code elementIndex}. Unlike + * {@link #getNode(int)}, this method does no relocate fingers. + * + * @param elementIndex the index of the target element. + * @return the node containing the target element. + */ + private Node nodeNoFingerFixing(int elementIndex) { + return fingerList.getNodeNoFingersFix(elementIndex); + } + + /** + * Prepends the input collection to the head of this list. + * + * @param c the collection to prepend. + */ + private void prependAll(Collection c) { + // Special case: initialize the very first node: + Iterator iterator = c.iterator(); + Node oldHead = head; + Node newNode = new Node<>(iterator.next()); + head = newNode; + + Node prevNode = head; + + for (int i = 1, sz = c.size(); i < sz; i++) { + // Keep prepending: + newNode = new Node<>(iterator.next()); + newNode.prev = prevNode; + prevNode.next = newNode; + prevNode = newNode; + } + + // Link the two sublists together: + prevNode.next = oldHead; + oldHead.prev = prevNode; + + // Update state: + int sz = c.size(); + modCount++; + size += sz; + + // Now, add the missing fingers: + addFingersAfterPrependAll(sz); + } + + /** + * Reconstitutes this {@code LinkedList} instance from a stream (that is, + * deserializes it). + * + * @param s the object input stream. + * + * @serialData first, the size of the list is read. Then all the node items + * are read and stored in the deserialization order, that is the + * same order as in serialization. + * + * @throws java.io.IOException if I/O fails. + * @throws ClassNotFoundException if the class is not found. + */ + private void readObject(java.io.ObjectInputStream s) + throws java.io.IOException, ClassNotFoundException { + + // Read in any hidden serialization magic: + s.defaultReadObject(); + + int sz = s.readInt(); + this.size = sz; + this.fingerList = new FingerList<>(this); + + switch (sz) { + case 0: + // Nothing to read. Return: + return; + + case 1: + // Just read one element: + Node newNode = new Node<>((E) s.readObject()); + // Set the end-points: + head = tail = newNode; + // Add one finger: + fingerList.appendFingerImpl(new Finger<>(newNode, 0)); + return; + } + + Node rightmostNode = new Node<>((E) s.readObject()); + head = rightmostNode; + + fingerList.appendFingerImpl(new Finger<>(rightmostNode, 0)); + + // Get the total number of required fingers in order to accommodate 'sz' + // elements: + int numberOfRequestedFingers = getRecommendedNumberOfFingers(sz); + + // The distance between consecutive finers: + int distance = sz / numberOfRequestedFingers; + + // Read in all elements in the proper order. + for (int i = 1; i < sz; i++) { + // Read the next node: + Node node = new Node<>((E) s.readObject()); + + if (i % distance == 0) { + // Append a new finger: + fingerList.appendFingerImpl(new Finger<>(node, i)); + } + + // Link in the next node: + rightmostNode.next = node; + node.prev = rightmostNode; + rightmostNode = node; + } + + // Finally, set the tail reference: + tail = rightmostNode; + } + + /** + * Removes the last non-sentinel finger from the finger list. + */ + private void removeFinger() { + fingerList.removeFinger(); + } + + /** + * Implements the actual removal of the first/head element. + * + * @return the removed element. + */ + private E removeFirstImpl() { + E returnValue = head.item; + decreaseSize(); + + // Unlink the first node: + head = head.next; + + if (head == null) { + // Once here, the new list is empty. Set 'tail' to 'null': + tail = null; + } else { + // Update the head reference: + head.prev = null; + } + + fingerList.adjustOnRemoveFirst(); + + if (mustRemoveFinger()) { + removeFinger(); + } + + // Update the index of the end-of-finger-list sentinel finger: + fingerList.getFinger(fingerList.size()).index = size; + return returnValue; + } + + /** + * Removes all the items that satisfy the given predicate. + * + * @param filter the filter object. + * @param fromIndex the starting, inclusive index of the range to crawl. + * @param toIndex the ending, exclusive index of the range to crawl. + * + * @return {@code true} if and only if this list was modified. + */ + private boolean removeIf(Predicate filter, + int fromIndex, + int toIndex) { + subListRangeCheck(fromIndex, toIndex, size); + + boolean modified = false; + int numberOfNodesToIterate = toIndex - fromIndex; + int i = 0; + int nodeIndex = fromIndex; + + for (Node node = getNode(fromIndex); + i < numberOfNodesToIterate; + i++) { + + Node nextNode = node.next; + + if (filter.test(node.item)) { + // Filter test passed. Remove and set the modification state to + // 'true': + modified = true; + removeObjectImpl(node, nodeIndex); + } else { + // Omit the node. + nodeIndex++; + } + + node = nextNode; + } + + return modified; + } + + /** + * Implements the actual removal of the last/tail element. + * + * @return the removed element. + */ + private E removeLastImpl() { + E returnValue = tail.item; + decreaseSize(); + + // Unlink the tail node: + tail = tail.prev; + + if (tail == null) { + // Once here, the new list is empty. Set 'head' to 'null': + head = null; + } else { + // Update the tail reference: + tail.next = null; + } + + if (mustRemoveFinger()) { + removeFinger(); + } + + // Update the index of the end-of-finger-list sentinel finger: + fingerList.getFinger(fingerList.size()).index = size; + return returnValue; + } + + /** + * Implements the node removal. + * + * @param node the node to remove. + * @param index the index of {@code node}. + */ + private void removeObjectImpl(Node node, int index) { + int closestFingerIndex = fingerList.getClosestFingerIndex(index); + Finger closestFinger = fingerList.getFinger(closestFingerIndex); + + if (closestFinger.index == index) { + // Make sure no finger is pointing to 'node': + moveFingerOutOfRemovalLocation(closestFinger, + closestFingerIndex); + } else { + // Update the finger indices: + fingerList.shiftFingerIndicesToLeftOnceAll(closestFingerIndex + 1); + + // The number of steps between 'closestFinger' and the 'index'th + // element. If positive, the 'index'th element is on the left of + // 'closestFinger': + int steps = closestFinger.index - index; + + if (steps > 0) { + // Once here, we must also decrement the index of the + // 'closestFingerIndex': + fingerList.getFinger(closestFingerIndex).index--; + } + } + + // Unlinking: + unlink(node); + decreaseSize(); + + if (mustRemoveFinger()) { + removeFinger(); + } + } + + /** + * Implements the range removal procedure. + * + * @param fromIndex the starting, inclusive index of the target range. + * @param toIndex the ending, exclusive index of the target range. + * @param fromFingerIndex the start bound finger index. + * @param toFingerIndex the upper bound finger index. + * @param fingersToRemove the number of fingers to remove. + */ + private void removeRangeImpl(int fromIndex, + int toIndex, + int fromFingerIndex, + int toFingerIndex, + int fingersToRemove) { + + // Compute the range length of the fingers to remove: + int removalFingerRangeLength = toFingerIndex + - fromFingerIndex; + + // Compute the length of the list range to be removed: + int removalRangeLength = toIndex + - fromIndex; + + if (removalFingerRangeLength <= fingersToRemove) { + // Once here, there is more fingers to remove than there is fingers + // within the removal list range. We will going to push some fingers + // to the prefix and suffix of the finger list: + removeRangeImplCaseA(fromFingerIndex, + toFingerIndex, + fromIndex, + toIndex, + fingersToRemove); + modCount++; + return; + } + + // Load the coverage counters 'numberOfCoveringFingersToPrefix' and + // 'numberOfCoveringFingersToSuffix': + this.loadFingerCoverageCounters( + fromFingerIndex, + toFingerIndex, + fromIndex, + toIndex, + fingersToRemove); + + int numberOfFingersInPrefix = fromFingerIndex; + int numberOfFingersInSuffix = fingerList.size() - toFingerIndex; + + // Process the left fingers: + fingerList.arrangePrefix(fromIndex, + numberOfFingersInPrefix, + this.numberOfCoveringFingersToPrefix); + + // Process the right fingers: + fingerList.arrangeSuffix(toIndex, + toFingerIndex, + numberOfFingersInSuffix, + this.numberOfCoveringFingersToSuffix); + + // Do the actual removal of all the fingers eligible for removal: + fingerList.removeFingersOnDeleteRange(fromFingerIndex, + fingersToRemove, + removalRangeLength); + } + + /** + * Unlinks the node range {@code [startNode, ..., endNode]}, both inclusive + * from this indexed list. + * ' + * @param startNode the start node of the range. + * @param endNode the end node of the range. + */ + private void unlinkNodeRange(Node startNode, Node endNode) { + Node currentNode = startNode; + Node nextNode; + + Node prevStartNode = startNode.prev; + Node nextEndNode = endNode.next; + + // Get rid of all the nodes in the removed range: + do { + nextNode = currentNode.next; + currentNode.item = null; + currentNode.prev = null; + currentNode.next = null; + currentNode = nextNode; + } while (currentNode != nextEndNode); + + // Stitch the list: + if (prevStartNode == null) { + head = nextEndNode; + nextEndNode.prev = null; + } else if (nextEndNode == null) { + prevStartNode.next = null; + tail = prevStartNode; + } else { + prevStartNode.next = nextEndNode; + nextEndNode.prev = prevStartNode; + } + } + + /** + * Handles a special case of range removal. + * + * @param fromFingerIndex the starting finger index. + * @param toFingerIndex the ending finger index. + * @param fromIndex the starting node index. + * @param toIndex the ending node index. + * @param fingersToRemove the number of fingers to remove. + */ + private void removeRangeImplCaseA(int fromFingerIndex, + int toFingerIndex, + int fromIndex, + int toIndex, + int fingersToRemove) { + + // The number of elements to copy in the below 'System.arraycopy': + int copyLength = Math.min(fingerList.size() - toFingerIndex, + fingerList.size() - fingersToRemove) + 1; + + // Compute the target index: + int targetIndex = + Math.max( + 0, + Math.min(fromFingerIndex, + toFingerIndex - fingersToRemove)); + + // Compute the source index: + int sourceIndex = targetIndex + fingersToRemove; + + // Do the actual copy: + System.arraycopy(fingerList.fingerArray, + sourceIndex, + fingerList.fingerArray, + targetIndex, + copyLength); + + // Set the old finger array slots to 'null' in order to get rid of junk: + Arrays.fill(fingerList.fingerArray, + fingerList.size() + 1 - fingersToRemove, + fingerList.size() + 1, + null); + + // Update the finger list size. We need this updated value for + // 'fingerList.shiftFingerIndicesToLeft()': + fingerList.size -= fingersToRemove; + + int removalRangeLength = toIndex - fromIndex; + + fingerList.shiftFingerIndicesToLeft(targetIndex, + removalRangeLength); + size -= removalRangeLength; + } + + /** + * Scrolls the input node {@code scrolls} positions towards the tail of the + * linked list and returns the reached node. + * + * @param startNode the node from which to start the scrolling. + * @param scrolls the number of positions to scroll. + * @return the reached node. + */ + private Node scrollNodeToRight(Node startNode, int scrolls) { + for (int i = 0; i < scrolls; i++) { + startNode = startNode.next; + } + + return startNode; + } + + /** + * Sets the input collection as a list. + * + * @param c the collection to set. + */ + private void setAll(Collection c) { + Iterator iterator = c.iterator(); + + head = new Node<>(iterator.next()); + Node prevNode = head; + + for (int i = 1, sz = c.size(); i < sz; i++) { + Node newNode = new Node<>(iterator.next()); + prevNode.next = newNode; + newNode.prev = prevNode; + prevNode = newNode; + } + + tail = prevNode; + size = c.size(); + modCount++; + + addFingersAfterSetAll(c.size()); + } + + /** + * Spreads the fingers over the range starting from {@code node}. + * + * @param node the starting node. + * @param index the starting index. + * @param fingerIndex the starting finger index. + * @param numberOfNewFingers the total number of fingers to spread. + * @param distance the distance between two consecutive fingers. + */ + private void spreadFingers(Node node, + int index, + int fingerIndex, + int numberOfNewFingers, + int distance) { + + // Initializing: + fingerList.setFinger(fingerIndex++, new Finger<>(node, index)); + + for (int i = 1; i < numberOfNewFingers; i++) { + // Keep spreading fingers: + index += distance; + node = scrollNodeToRight(node, distance); + fingerList.setFinger(fingerIndex++, new Finger<>(node, index)); + } + } + + /** + * If {@code steps} > 0, rewinds the finger {@code finger} to the right. + * Otherwise, rewinds to the right. + * + * @param finger the finger to rewind. + * @param steps the number of steps to rewind the {@code finger}. + * @param the list element type. + * @return the reached node. + */ + static Node rewindFinger(Finger finger, int steps) { + Node node = finger.node; + + if (steps < 0) { + return FingerList.scrollToLeft(node, -steps); + } else { + return FingerList.scrollToRight(node, steps); + } + } + + /** + * Attempts to move the finger with index {@code fingerIndex} to the left. + * + * @param fingerIndex the index of the finger to move to the left. + * + * @return {@code true} if a free spot is found, {@code false} otherwise. + */ + private boolean tryPushFingersToLeft(int fingerIndex) { + if (fingerIndex == 0) { + Finger finger = fingerList.getFinger(0); + + finger.index--; + finger.node = finger.node.prev; + fingerList.shiftFingerIndicesToLeftOnceAll(1); + return true; + } + + // Attempt to push to the left: + for (int j = fingerIndex; j > 0; --j) { + Finger fingerLeft = fingerList.getFinger(j - 1); + Finger fingerRight = fingerList.getFinger(j); + + if (fingerLeft.index + 1 < fingerRight.index) { + // We have a free spot between 'fingerLeft' and 'fingerRight' of + // length at least one (1). Now push all the fingers from the + // range [j, fingerIndex] one position to the left: + for (int k = j; k <= fingerIndex; k++) { + Finger fngr = fingerList.getFinger(k); + fngr.node = fngr.node.prev; + fngr.index--; + } + + // Update all the finger indices residing after the target + // finger with index 'fingerIndex': + fingerList.shiftFingerIndicesToLeftOnceAll(fingerIndex + 1); + return true; + } + } + + // Once here, the prefix is tightly packed and does not allow shifting + // to the left: + return false; + } + + /** + * Attempts to move the finger with index {@code fingerIndex} to the right. + * + * @param fingerIndex the index of the finger to move to the right. + * + * @return {@code true} if a free spot is found, {@code false} otherwise. + */ + private boolean tryPushFingersToRight(int fingerIndex) { + // Attempt to push to the right: + for (int j = fingerIndex; j < fingerList.size(); ++j) { + Finger fingerLeft = fingerList.getFinger(j); + Finger fingerRight = fingerList.getFinger(j + 1); + + if (fingerLeft.index + 1 < fingerRight.index) { + // Once here, we have an opportunity for pushing to the right. + // Move nodes one spot to the right: + for (int i = j; i >= fingerIndex; --i) { + Finger fngr = fingerList.getFinger(i); + fngr.node = fngr.node.next; + } + + fingerList.shiftFingerIndicesToLeftOnceAll(j + 1); + return true; + } + } + + return false; + } + + /** + * Unlinks the input node from the actual doubly-linked list. + * + * @param x the node to unlink from the underlying linked list. + */ + private void unlink(Node x) { + Node next = x.next; + Node prev = x.prev; + + // Unlink from the predecessor: + if (prev == null) { + head = next; + } else { + prev.next = next; + x.prev = null; + } + + // Unlink from the ancestor: + if (next == null) { + tail = prev; + } else { + next.prev = prev; + x.next = null; + } + } + + /** + * Saves the state of this {@code LinkedList} instance to a stream (that is, + * serializes it). + * + * @param s the object output stream. + * + * @serialData The size of the list (the number of elements it + * contains) is emitted (int), followed by all of its + * elements (each an Object) in the proper order. + * + * @throws java.io.IOException if the I/O fails. + */ + private void writeObject(java.io.ObjectOutputStream s) + throws java.io.IOException { + // Write out any hidden serialization magic: + s.defaultWriteObject(); + + // Write out size: + s.writeInt(size); + + // Write out all elements in the proper order: + for (Node x = head; x != null; x = x.next) { + s.writeObject(x.item); + } + } + + + + /** + * This inner class implements a sublist view over the compassing list. + */ + final class EnhancedSubList implements List, Cloneable { + + /** + * The root list. + */ + private final IndexedLinkedList root; + + /** + * The parent view. This view cannot be wider than its parent view. + */ + private final EnhancedSubList parent; + + /** + * The offset with regard to the parent view or the root list. + */ + private final int offset; + + /** + * The length of this view. + */ + private int size; + + /** + * The modification count. + */ + private int modCount; + + /** + * Constructs a new sublist view over a {@code IndexedLinkedList}. + * + * @param root the root list. + * @param fromIndex the starting, inclusive index of this view. + * @param toIndex the ending, exclusive index of this view. + */ + EnhancedSubList(IndexedLinkedList root, + int fromIndex, + int toIndex) { + + this.root = root; + this.parent = null; + this.offset = fromIndex; + this.size = toIndex - fromIndex; + this.modCount = root.modCount; + } + + /** + * Constructs a new sublist view over a parent view. + * + * @param parent the parent view. + * @param fromIndex the starting, inclusive index of the new subview. + * Starts from the beginning of {@code parent}. + * @param toIndex the ending, exclusive index of the new subview. + * Starts from the beginning of {@code parent}. + */ + private EnhancedSubList(EnhancedSubList parent, + int fromIndex, + int toIndex) { + + this.root = parent.root; + this.parent = parent; + this.offset = parent.offset + fromIndex; + this.size = toIndex - fromIndex; + this.modCount = root.modCount; + } + + /** + * Appends {@code e} to the end of this view. + * + * @param e the element to add. + * @return always {@code true}. + */ + @Override + public boolean add(E e) { + checkInsertionIndex(size); + checkForComodification(); + root.add(offset + size, e); + updateSizeAndModCount(1); + return true; + } + + /** + * Inserts the element {@code element} before view element at index + * {@code index}. + * + * @param index the insertion index. + * @param element the element to add. + */ + @Override + public void add(int index, E element) { + checkInsertionIndex(index); + checkForComodification(); + root.add(offset + index, element); + updateSizeAndModCount(1); + } + + /** + * Appends the entire collection {@code c} to the end of this view. The + * elements from {@code c} are appended in the iteration order of + * {@code c}. + * + * @param c the collection to append. + * @return {@code true} if and only if this view changed due to the + * call. + */ + @Override + public boolean addAll(Collection c) { + return addAll(this.size, c); + } + + /** + * Inserts the collection {@code collection} before the {@code index}th + * element of this view. The elements are inserted in the iteration + * order of {@code collection}. + * + * @param index the index of the element before which to insert. + * @param collection the collection to insert. + * @return {@code true} if and only if this view changed. + */ + @Override + public boolean addAll(int index, Collection collection) { + checkInsertionIndex(index); + int collectionSize = collection.size(); + + if (collectionSize == 0) { + return false; + } + + checkForComodification(); + root.addAll(offset + index, collection); + updateSizeAndModCount(collectionSize); + return true; + } + + /** + * Cleares the entire view. This will delegate down the parent chain to + * the actual root list, effectively removing a subrange from the root + * list. + */ + @Override + public void clear() { + checkForComodification(); + root.removeRange(offset, offset + size); + updateSizeAndModCount(-size); + } + + /** + * Creates a {@link IndexedLinkedList} and loads the contents of this + * view to it in iterative order. + * + * @return the clone object of this subview. + */ + @Override + public Object clone() { + List list = new IndexedLinkedList<>(); + + for (E element : this) { + list.add(element); + } + + return list; + } + + /** + * Return {@code true} if and only if the given object appears in this + * view. + * + * @param o the object to locate. + * @return {@code true} if and only if {@code o} is in this view. + */ + @Override + public boolean contains(Object o) { + return indexOf(o) >= 0; + } + + /** + * Checks that all the elements in {@code c} are in this view. + * + * @param c the collection to test for inclusion. + * @return {@code true} if and only if all the elements in {@code c} + * appear in this view. + */ + @Override + public boolean containsAll(Collection c) { + for (Object o : c) { + if (!contains(o)) { + return false; + } + } + + return true; + } + + /** + * Checks that the given object is a {@link java.util.List} and has the + * same content as this view. + * + * @param o the object to check. + * @return {@code true} if and only if the input object is a list with + * the same contents as this view. + */ + @Override + public boolean equals(Object o) { + return root.equalsRange((List) o, offset, offset + size); + } + + /** + * Applies the input action to each element in this view. + * + * @param action the action to apply. + */ + @Override + public void forEach(Consumer action) { + Objects.requireNonNull(action); + int expectedModCount = modCount; + int iterated = 0; + + for (Node node = getNode(offset); + modCount == expectedModCount && iterated < size; + node = node.next, ++iterated) { + + action.accept(node.item); + } + + if (modCount != expectedModCount) { + throw new ConcurrentModificationException(); + } + } + + /** + * Returns the {@code index}th element of this view. + * + * @param index the index of the target element. + * @return the {@code index}th element. + */ + @Override + public E get(int index) { + checkIndex(index, size); + checkForComodification(); + return root.get(offset + index); + } + + /** + * Returns the hash code of this view. + * + * @return the hash code of this view. + */ + @Override + public int hashCode() { + return root.hashCodeRange(offset, offset + size); + } + + /** + * Returns the index of the leftmost appearance of {@code o}, or + * {@code -1} if there is no such. + * + * @param o the target object. + * @return the index of the input object, or {@code -1} if there is no + * match. + */ + @Override + public int indexOf(Object o) { + int index = root.indexOfRange(o, offset, offset + size); + checkForComodification(); + return index >= 0 ? index - offset : -1; + } + + /** + * Returns {@code true} if and only if this view is empty. + * + * @return {@code true} if and only if this view is empty. + */ + @Override + public boolean isEmpty() { + return size == 0; + } + + /** + * Returns an iterator over this view. + * + * @return an iterator. + */ + @Override + public Iterator iterator() { + return listIterator(); + } + + /** + * Returns the index of the rightmost occurrence of {@code o}, or + * {@code -1} if there is no such. + * + * @param o the targe object. + * @return the index of {@code o} in this view. + */ + @Override + public int lastIndexOf(Object o) { + int index = root.lastIndexOfRange(o, offset, offset + size); + checkForComodification(); + return index >= 0 ? index - offset : -1; + } + + /** + * Returns a list iterator over this view. + * + * @return a list iterator. + */ + @Override + public ListIterator listIterator() { + return listIterator(0); + } + + /** + * Returns a list iterator over this view starting from the specified + * position. + * + * @param index the starting position. + * @return a list iterator. + */ + @Override + public ListIterator listIterator(int index) { + checkForComodification(); + checkInsertionIndex(index); + + return new ListIterator() { + private final ListIterator i = + root.listIterator(offset + index); + + @Override + public boolean hasNext() { + return nextIndex() < size; + } + + @Override + public E next() { + if (hasNext()) { + return i.next(); + } + + throw new NoSuchElementException(); + } + + @Override + public boolean hasPrevious() { + return previousIndex() >= 0; + } + + @Override + public E previous() { + if (hasPrevious()) { + return i.previous(); + } + + throw new NoSuchElementException(); + } + + @Override + public int nextIndex() { + return i.nextIndex() - offset; + } + + @Override + public int previousIndex() { + return i.previousIndex() - offset; + } + + @Override + public void remove() { + i.remove(); + updateSizeAndModCount(-1); + } + + @Override + public void set(E e) { + i.set(e); + } + + @Override + public void add(E e) { + i.add(e); + updateSizeAndModCount(1); + } + }; + } + + /** + * Removes the leftmost occurrence of {@code o} from this view. + * + * @param o the object to remove. May be {@code null}. + * @return {@code true} if and only if {@code o} was removed. + */ + @Override + public boolean remove(Object o) { + ListIterator iterator = listIterator(); + + if (o == null) { + while (iterator.hasNext()) { + if (iterator.next() == null) { + iterator.remove(); + return true; + } + } + } else { + while (iterator.hasNext()) { + if (o.equals(iterator.next())) { + iterator.remove(); + return true; + } + } + } + + return false; + } + + /** + * Removes the {@code index}th element from this view. + * + * @param index the index of the element to remove. + * @return the removed element. + */ + @Override + public E remove(int index) { + checkIndex(index, size); + checkForComodification(); + E result = root.remove(offset + index); + updateSizeAndModCount(-1); + return result; + } + + /** + * Removes all the elements appearing in {@code c}. + * + * @param c the collection of elements to remove from this view. + * @return {@code true} if and only if any element in {@code c} was + * removed. + */ + @Override + public boolean removeAll(Collection c) { + return batchRemove(c, true); + } + + /** + * Removes all the elements in this view that are filtered by + * {@code filter}. + * + * @param filter the filter to apply. + * @return {@code true} if and only if any element was removed. + */ + @Override + public boolean removeIf(Predicate filter) { + checkForComodification(); + int oldSize = root.size; + boolean modified = root.removeIf(filter, offset, offset + size); + + if (modified) { + updateSizeAndModCount(root.size - oldSize); + } + + return modified; + } + + /** + * Replaces all the elements in this view with another values dictated + * by {@code operator}. + * + * @param operator the replacement operator to apply. + */ + @Override + public void replaceAll(UnaryOperator operator) { + root.replaceAllRange(operator, offset, offset + size); + } + + /** + * Remove all the elements from this view that are not + * present in {@code c}. + * + * @param c the collection to retain. + * @return {@code true} if and only if this view changed. + */ + @Override + public boolean retainAll(Collection c) { + return batchRemove(c, false); + } + + /** + * Sets the {@code index}th element of this view to {@code element}. + * + * @param index the index of the target element. + * @param element the element to set instead of the target element. + * @return the old value of the {@code index}th element of this view. + */ + @Override + public E set(int index, E element) { + checkIndex(index, size); + checkForComodification(); + return root.set(offset + index, element); + } + + /** + * Returns the size of this view. + * + * @return the size of this view. + */ + @Override + public int size() { + checkForComodification(); + return size; + } + + /** + * Sorts this view. + * + * @param c the comparator object. + */ + @Override + @SuppressWarnings("unchecked") + public void sort(Comparator c) { + if (size == 0) { + return; + } + + int expectedModCount = modCount; + Object[] array = toArray(); + Node node = getNode(offset); + + Arrays.sort((E[]) array, c); + + // Rearrange the items over the linked list nodes: + for (int i = 0; i < array.length; ++i, node = node.next) { + E item = (E) array[i]; + node.item = item; + } + + if (expectedModCount != modCount) { + throw new ConcurrentModificationException(); + } + + distributeFingers(offset, offset + size); + modCount++; + } + + /** + * Returns the spliterator over this view. + * + * @return the spliterator over this view. + */ + @Override + public Spliterator spliterator() { + return new LinkedListSpliterator(root, + getNode(offset), + size, + offset, + modCount); + } + + /** + * Returns the subview of this view. The subview in question is + * {@code this[fromIndex ... toIndex - 1]}. + * + * @param fromIndex the starting, inclusive index of the new subview. + * @param toIndex the ending, exclusive index of the new subview. + * @return the subview {@code this[fromIndex ... toIndex - 1}. + */ + @Override + public List subList(int fromIndex, int toIndex) { + subListRangeCheck(fromIndex, toIndex, size); + return new EnhancedSubList(this, fromIndex, toIndex); + } + + /** + * Returns the array of elements of this view. + * + * @return the element array in the same order they appear in the + * underlying linked list. + */ + @Override + public Object[] toArray() { + checkForComodification(); + int expectedModCount = root.modCount; + Object[] array = new Object[this.size]; + Node node = getNode(offset); + + for (int i = 0; + i < array.length && expectedModCount == root.modCount; + i++, node = node.next) { + array[i] = node.item; + } + + if (expectedModCount != root.modCount) { + throw new ConcurrentModificationException(); + } + + return array; + } + + /** + * Returns the array of elements of this view. If the input array is too + * small, an array of size {@code size} is created and filled with the + * view contents. Otherwise, the elements in this view are copied to + * {@code a}. Also, if {@code a.length > size}, we set {@code a[size]} + * to {@code null} in order to signal the end-of-view. + * + * @param the element data type. + * @param a the input array. + * @return an array of all view elements. + */ + @Override + public T[] toArray(T[] a) { + checkForComodification(); + + int expectedModCount = root.modCount; + + if (a.length < size) { + a = (T[]) Array.newInstance( + a.getClass().getComponentType(), + size); + } + + int index = 0; + + for (Node node = getNode(offset); + expectedModCount == root.modCount && index < size; + ++index, node = node.next) { + + a[index] = (T) node.item; + } + + if (a.length > size) { + a[size] = null; + } + + return a; + } + + /** + * Returns the textual representation of this view. + * + * @return the textual representation. + */ + @Override + public String toString() { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("["); + + boolean firstIteration = true; + + for (E element : this) { + if (firstIteration) { + firstIteration = false; + } else { + stringBuilder.append(", "); + } + + stringBuilder.append(element); + } + + return stringBuilder.append("]").toString(); + } + + /** + * Checks that the input finger {@code index} is in the set + * \(\{ 0, 1, \ldots, N\}\), where \(N = \){@code size}. + * + * @param index the insertion index to validate. + */ + void checkInsertionIndex(int index) { + if (index < 0) { + throw new IndexOutOfBoundsException("Negative index: " + index); + } + + if (index > this.size) { + throw new IndexOutOfBoundsException( + "index(" + index + ") > size(" + size + ")"); + } + } + + /** + * Operates in batch over all the elements in {@code c}. + * + * @param c the target collection to operate on. + * @param complement the mode flag. If {@code true}, all elements in + * {@code c} will be removed from this view, and if + * {@code false}, all elements not in + * {@code c}, will be removed from this view. + * @return {@code true} if and only if this view was modified. + */ + private boolean batchRemove(Collection c, boolean complement) { + checkForComodification(); + int oldSize = root.size; + + // Run the batch remove and obtain the modification flag: + boolean modified = root.batchRemove(c, + complement, + offset, + offset + size); + + if (modified) { + // Update the size and modification counters: + updateSizeAndModCount(root.size - oldSize); + } + + return modified; + } + + /** + * Makes sure that the root list and this view are in sync. + * + * @throws ConcurrentModificationException if there is modification + * mismatch. + */ + private void checkForComodification() { + if (root.modCount != this.modCount) + throw new ConcurrentModificationException(); + } + + /** + * Updates the sizes and modification counters up the subview chain. + * + * @param sizeDelta the value to add to the chain. If this view grew in + * size, {@code sizeDelta} is positive. Otherwise, if + * the view shrinked in size, {@code sizeDelta} will be + * negative. + */ + private void updateSizeAndModCount(int sizeDelta) { + EnhancedSubList subList = this; + + do { + subList.size += sizeDelta; + subList.modCount = root.modCount; + subList = subList.parent; + } while (subList != null); + } + } + + /** + * This static inner class implements the spliterator over this list. + * + * @param the node datum type. + */ + static final class LinkedListSpliterator implements Spliterator { + + /** + * The minimum batch size. + */ + static final long MINIMUM_BATCH_SIZE = 1024L; + + /** + * The target list. + */ + private final IndexedLinkedList list; + + /** + * The current node. + */ + private Node node; + + /** + * The length of this spliterator. + */ + private long lengthOfSpliterator; + + /** + * The number of processed elements so far. + */ + private long numberOfProcessedElements; + + /** + * The offset of this spliterator. + */ + private long offsetOfSpliterator; + + /** + * The expected modification count. We rely on this in order to catch + * the modifications from outside the spliterator API. + */ + private final int expectedModCount; + + /** + * Constructs a new spliterator. + * + * @param list the target list to split. + * @param node the initial node of this spliterator. + * @param lengthOfSpliterator the length of this spliterator. + * @param offsetOfSpliterator the offset of this spliterator. + * @param expectedModCount the expected modification count. + */ + private LinkedListSpliterator(IndexedLinkedList list, + Node node, + long lengthOfSpliterator, + long offsetOfSpliterator, + int expectedModCount) { + this.list = list; + this.node = node; + this.lengthOfSpliterator = lengthOfSpliterator; + this.offsetOfSpliterator = offsetOfSpliterator; + this.expectedModCount = expectedModCount; + } + + /** + * Returns characteristics masks. + * + * @return characteristics masks. + */ + @Override + public int characteristics() { + return Spliterator.ORDERED | + Spliterator.SUBSIZED | + Spliterator.SIZED; + } + + /** + * Returns the estimated size left. This method, however, returns the + * exact size estimate. + * + * @return the number of elements in this spliterator not yet advanced + * over. + */ + @Override + public long estimateSize() { + return (long)(lengthOfSpliterator - numberOfProcessedElements); + } + + /** + * Applies {@code action} to all remaining elements in this spliterator. + * + * @param action the action to apply to each remaining element. + */ + @Override + public void forEachRemaining(Consumer action) { + Objects.requireNonNull(action); + + // Iterate for each remaining element: + for (long i = numberOfProcessedElements; + i < lengthOfSpliterator; + i++) { + + E item = node.item; + action.accept(item); + node = node.next; + } + + // Update the cached element counter: + numberOfProcessedElements = lengthOfSpliterator; + + if (list.modCount != expectedModCount) { + throw new ConcurrentModificationException(); + } + } + + /** + * Just like {@link #estimateSize()}, returns the exact number of + * elements not yet advanced over via this spliterator. + * + * @return the number of elements in this spliterator not yet advanced + * over. + */ + @Override + public long getExactSizeIfKnown() { + return estimateSize(); + } + + /** + * Queries for particular characteristics. + * + * @param characteristics the characteristic flag. + * @return {@code true} if and only if this spliterator supports the + * {@code characteristics}. + */ + @Override + public boolean hasCharacteristics(int characteristics) { + switch (characteristics) { + case Spliterator.ORDERED: + case Spliterator.SIZED: + case Spliterator.SUBSIZED: + return true; + + default: + return false; + } + } + + /** + * Makes an attempt to advance in this spliterator. + * + * @param action the action which to apply to the advanced element. + * @return {@code true} if and only if there were an element to advance. + */ + @Override + public boolean tryAdvance(Consumer action) { + Objects.requireNonNull(action); + + if (numberOfProcessedElements == lengthOfSpliterator) { + // Once here, this spliterator instance has iterated over all + // available data: + return false; + } + + // Process an element: + numberOfProcessedElements++; + E item = node.item; + action.accept(item); + node = node.next; + + if (list.modCount != expectedModCount) { + throw new ConcurrentModificationException(); + } + + return true; + } + + /** + * Attempts to split this spliterator. Upon success, it returns the rest + * of this spliterator, and a child spliterator working on the another + * half of the range. + * + * @return another spliterator, or {@code null} if splitting was not + * possible. + */ + @Override + public Spliterator trySplit() { + long sizeLeft = estimateSize(); + + if (sizeLeft == 0) { + // Once here, there is no more data available: + return null; + } + + // New length: + long thisSpliteratorNewLength = sizeLeft / 2L; + + if (thisSpliteratorNewLength < MINIMUM_BATCH_SIZE) { + // New length is too small: + return null; + } + + // New spliterator parameters: + long newSpliteratorLength = sizeLeft - thisSpliteratorNewLength; + long newSpliteratorOffset = this.offsetOfSpliterator; + + // Update this spliterator: + this.offsetOfSpliterator += newSpliteratorLength; + this.lengthOfSpliterator -= newSpliteratorLength; + + // Set the new spliterators current node: + Node newSpliteratorNode = this.node; + + // Advance the current node of this spliterator: + this.node = list.getNode((int) this.offsetOfSpliterator); + + // Once here, we can create a new spliterator: + return new LinkedListSpliterator<>(list, + newSpliteratorNode, + newSpliteratorLength, // length + newSpliteratorOffset, // offset + expectedModCount); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/apache/commons/collections4/list/jmh/TreeListVsIndexedLinkedListBenchmark.java b/src/main/java/org/apache/commons/collections4/list/jmh/TreeListVsIndexedLinkedListBenchmark.java new file mode 100644 index 0000000000..d53ccc2da9 --- /dev/null +++ b/src/main/java/org/apache/commons/collections4/list/jmh/TreeListVsIndexedLinkedListBenchmark.java @@ -0,0 +1,238 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.collections4.list.jmh; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Random; +import java.util.concurrent.TimeUnit; +import org.apache.commons.collections4.list.ExtendedTreeList; +import org.apache.commons.collections4.list.IndexedLinkedList; + +/** + * JMH benchmark comparing IndexedLinkedList vs ExtendedTreeList. + * + * Benchmarks: + * - prepend (add at beginning) + * - append (add at end) + * - insert in the middle + * - prepend collection + * - append collection + * - insert collection in the middle + * - random access + * - pop front + * - pop back + * - pop at random location + * - remove range via subList(a, b).clear() + */ +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@Fork(1) +public class TreeListVsIndexedLinkedListBenchmark { + + @State(Scope.Thread) + public static class BenchmarkState { + + /** + * Which implementation to benchmark. + * JMH will run every @Benchmark method once for each value. + */ + @Param({"IndexedLinkedList", "ExtendedTreeList"}) + public String impl; + + /** + * Initial size of the list before performing the operation under test. + */ + @Param({"1000", "10000", "100000"}) + public int size; + + /** + * Size of the batch collection used in addAll benchmarks. + */ + @Param({"10", "100"}) + public int batchSize; + + /** + * The list under test for this invocation. + * This is rebuilt before each benchmark invocation. + */ + public List list; + + /** + * Collection used for prepend/append/insert collection benchmarks. + */ + public Collection batch; + + /** + * Random used for random index selection. + */ + public Random random; + + /** + * Range for subList(a, b).clear() + */ + public int fromIndex; + public int toIndex; + + @Setup(Level.Trial) + public void setupTrial() { + random = new Random(42); + + batch = new ArrayList<>(batchSize); + for (int i = 0; i < batchSize; i++) { + batch.add(-i - 1); // arbitrary distinct values + } + + // Fixed middle range for remove-range benchmark + fromIndex = Math.max(0, size / 4); + toIndex = Math.max(fromIndex + 1, size / 2); + } + + @Setup(Level.Invocation) + public void setupInvocation() { + list = createList(); + + // Fill list with deterministic contents [0, 1, 2, ..., size-1] + for (int i = 0; i < size; i++) { + list.add(i); + } + } + + private List createList() { + switch (impl) { + case "IndexedLinkedList": + return new IndexedLinkedList<>(); + case "ExtendedTreeList": + return new ExtendedTreeList<>(); + default: + throw new IllegalStateException("Unknown impl: " + impl); + } + } + } + + // ------------------------------------------------------------------ + // Single-element operations + // ------------------------------------------------------------------ + + /** add at beginning (prepend) */ + @Benchmark + public void prepend(BenchmarkState state) { + state.list.add(0, -1); + } + + /** add at end (append) */ + @Benchmark + public void append(BenchmarkState state) { + state.list.add(-1); + } + + /** insert in the middle */ + @Benchmark + public void insertMiddle(BenchmarkState state) { + int mid = state.list.size() / 2; + state.list.add(mid, -1); + } + + // ------------------------------------------------------------------ + // Collection operations + // ------------------------------------------------------------------ + + /** prepend collection */ + @Benchmark + public void prependCollection(BenchmarkState state) { + state.list.addAll(0, state.batch); + } + + /** append collection */ + @Benchmark + public void appendCollection(BenchmarkState state) { + state.list.addAll(state.batch); + } + + /** insert collection in the middle */ + @Benchmark + public void insertCollectionMiddle(BenchmarkState state) { + int mid = state.list.size() / 2; + state.list.addAll(mid, state.batch); + } + + // ------------------------------------------------------------------ + // Random access and removals + // ------------------------------------------------------------------ + + /** access random element */ + @Benchmark + public void randomAccess(BenchmarkState state, Blackhole bh) { + int idx = state.random.nextInt(state.list.size()); + Integer value = state.list.get(idx); + bh.consume(value); + } + + /** pop front */ + @Benchmark + public void popFront(BenchmarkState state, Blackhole bh) { + Integer value = state.list.remove(0); + bh.consume(value); + } + + /** pop back */ + @Benchmark + public void popBack(BenchmarkState state, Blackhole bh) { + int lastIndex = state.list.size() - 1; + Integer value = state.list.remove(lastIndex); + bh.consume(value); + } + + /** pop at random location */ + @Benchmark + public void popRandom(BenchmarkState state, Blackhole bh) { + int idx = state.random.nextInt(state.list.size()); + Integer value = state.list.remove(idx); + bh.consume(value); + } + + /** remove range via subList(a, b).clear() */ + @Benchmark + public void removeRangeSubListClear(BenchmarkState state, Blackhole bh) { + state.list.subList(state.fromIndex, state.toIndex).clear(); + // consume size so JIT can't completely ignore the mutation + bh.consume(state.list.size()); + } + + // ------------------------------------------------------------------ + // Main method for running from IDE (optional) + // ------------------------------------------------------------------ + + public static void main(String[] args) throws Exception { + Options opt = new OptionsBuilder() + .include(TreeListVsIndexedLinkedListBenchmark + .class.getSimpleName()) + .detectJvmArgs() // lets JMH reuse your IDE JVM args + .build(); + + new Runner(opt).run(); + } +} diff --git a/src/test/java/org/apache/commons/collections4/list/FingerListTest.java b/src/test/java/org/apache/commons/collections4/list/FingerListTest.java new file mode 100644 index 0000000000..94cb2c7f3e --- /dev/null +++ b/src/test/java/org/apache/commons/collections4/list/FingerListTest.java @@ -0,0 +1,501 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.collections4.list; + +import org.apache.commons.collections4.list.IndexedLinkedList.Finger; +import org.apache.commons.collections4.list.IndexedLinkedList.FingerList; +import org.apache.commons.collections4.list.IndexedLinkedList.Node; +import java.util.Arrays; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeEach; +import static org.junit.jupiter.api.Assertions.*; + +/** + * The test for finger list of a + * {@link org.apache.commons.collections4.list.IndexedLinkedList}. + */ +public class FingerListTest { + + private final IndexedLinkedList list = new IndexedLinkedList<>(); + private final FingerList fl = list.fingerList; + + @BeforeEach + public void setUp() { + fl.clear(); + } + + @Test + public void enlargeFingerArrayWithEmptyRangeNonExpansion() { + list.addAll(Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)); + list.fingerList.setFingerIndices(0, 1, 2, 3); + list.fingerList.enlargeFingerArrayWithEmptyRange(8, 1, 2, 2); + + assertEquals(new Finger<>(new Node<>(0), 0), + list.fingerList.fingerArray[0]); + + assertEquals(new Finger<>(new Node<>(1), 3), + list.fingerList.fingerArray[1]); + + assertEquals(new Finger<>(new Node<>(2), 4), + list.fingerList.fingerArray[2]); + + assertEquals(new Finger<>(new Node<>(1), 3), + list.fingerList.fingerArray[3]); + + assertEquals(new Finger<>(new Node<>(2), 4), + list.fingerList.fingerArray[4]); + + assertEquals(new Finger<>(new Node<>(3), 5), + list.fingerList.fingerArray[5]); + + assertEquals(new Finger<>(null, 14), + list.fingerList.fingerArray[6]); + } + + @Test + public void toStringImpl() { + list.addAll(Arrays.asList(9, 10)); + + assertEquals("[FingerList (size = 3) | " + + "[index = 0, item = 9], " + + "[index = 1, item = 10], " + + "[index = 2, item = null]]", + list.fingerList.toString()); + } + + @Test + public void appendGetFinger() { + fl.appendFingerImpl(new Finger<>(new Node<>(Integer.valueOf(0)), 0)); + fl.appendFingerImpl(new Finger<>(new Node<>(Integer.valueOf(1)), 1)); + fl.appendFingerImpl(new Finger<>(new Node<>(Integer.valueOf(3)), 3)); + fl.appendFingerImpl(new Finger<>(new Node<>(Integer.valueOf(6)), 6)); + fl.fingerArray[4].index = 8; + fl.fingerArray[4].node = new Node<>(Integer.valueOf(1000)); + + Finger finger = fl.getFinger(fl.getClosestFingerIndex(0)); + assertEquals(0, finger.index); + assertEquals(Integer.valueOf(0), finger.node.item); + + finger = fl.getFinger(fl.getClosestFingerIndex(1)); + assertEquals(1, finger.index); + assertEquals(Integer.valueOf(1), finger.node.item); + + finger = fl.getFinger(fl.getClosestFingerIndex(2)); + assertEquals(3, finger.index); + assertEquals(Integer.valueOf(3), finger.node.item); + + finger = fl.getFinger(fl.getClosestFingerIndex(3)); + assertEquals(3, finger.index); + assertEquals(Integer.valueOf(3), finger.node.item); + + finger = fl.getFinger(fl.getClosestFingerIndex(4)); + assertEquals(3, finger.index); + assertEquals(Integer.valueOf(3), finger.node.item); + + finger = fl.getFinger(fl.getClosestFingerIndex(5)); + assertEquals(6, finger.index); + assertEquals(Integer.valueOf(6), finger.node.item); + + finger = fl.getFinger(fl.getClosestFingerIndex(6)); + assertEquals(6, finger.index); + assertEquals(Integer.valueOf(6), finger.node.item); + } + + @Test + public void insertFingerAtFront() { + fl.appendFingerImpl(new Finger<>(new Node<>(Integer.valueOf(0)), 0)); + fl.appendFingerImpl(new Finger<>(new Node<>(Integer.valueOf(1)), 1)); + fl.appendFingerImpl(new Finger<>(new Node<>(Integer.valueOf(3)), 3)); + fl.appendFingerImpl(new Finger<>(new Node<>(Integer.valueOf(6)), 6)); + + Finger insertionFinger = new Finger<>(new Node<>(null), 0); + + fl.insertFingerAndShiftOnceToRight(insertionFinger); + + Finger finger = fl.getFinger(fl.getClosestFingerIndex(0)); + assertEquals(insertionFinger.index, finger.index); + + assertEquals(5, fl.size()); + } + + @Test + public void insertFingerAtTail() { + fl.list.size = 1; + fl.appendFingerImpl(new Finger<>(new Node<>(Integer.valueOf(2)), 0)); + fl.list.size = 2; + fl.appendFingerImpl(new Finger<>(new Node<>(Integer.valueOf(4)), 1)); + fl.list.size = 3; + fl.appendFingerImpl(new Finger<>(new Node<>(Integer.valueOf(5)), 2)); + + // Add end of finger list sentinel: + fl.fingerArray[3] = new Finger<>(new Node<>(null), 10); + + fl.list.size = 4; + Finger insertionFinger = new Finger<>(new Node<>(13), 1); + + fl.insertFingerAndShiftOnceToRight(insertionFinger); + + assertEquals(4, fl.size()); + } + + @Test + public void insertFingerInBetween1() { + fl.appendFingerImpl(new Finger<>(new Node<>(Integer.valueOf(2)), 2)); + fl.appendFingerImpl(new Finger<>(new Node<>(Integer.valueOf(4)), 4)); + fl.appendFingerImpl(new Finger<>(new Node<>(Integer.valueOf(5)), 5)); + + Finger insertionFinger = new Finger<>(new Node<>(null), 4); + + fl.insertFingerAndShiftOnceToRight(insertionFinger); + + assertEquals(insertionFinger, fl.getFinger(1)); + } + + @Test + public void insertFingerInBetween2() { + fl.appendFingerImpl(new Finger<>(new Node<>(Integer.valueOf(2)), 2)); + fl.appendFingerImpl(new Finger<>(new Node<>(Integer.valueOf(4)), 4)); + fl.appendFingerImpl(new Finger<>(new Node<>(Integer.valueOf(5)), 5)); + + Finger insertionFinger = new Finger<>(new Node<>(null), 3); + + fl.insertFingerAndShiftOnceToRight(insertionFinger); + + assertEquals(insertionFinger, fl.getFinger(1)); + } + + @Test + public void makeRoomAtPrefix1Old() { + for (int i = 0; i < 10; i++) { + list.add(i); + } + + list.fingerList.setFingerIndices(6, 7, 8, 9); + + list.fingerList.makeRoomAtPrefix(5, 0, 3); + list.fingerList.pushCoveredFingersToPrefix(6, 0, 3); + + Finger finger0 = list.fingerList.fingerArray[0]; + Finger finger1 = list.fingerList.fingerArray[1]; + Finger finger2 = list.fingerList.fingerArray[2]; + + assertEquals(3, finger0.index); + assertEquals(4, finger1.index); + assertEquals(5, finger2.index); + } + + @Test + public void makeRoomAtPrefix1() { + + loadList(10); + + list.fingerList.setFingerIndices(1, 3, 4, 6); + list.fingerList.makeRoomAtPrefix(4, 2, 2); + + IndexedLinkedList expectedList = new IndexedLinkedList<>(list); + expectedList.fingerList.setFingerIndices(0, 1, 4, 6); + + assertTrue(list.strongEquals(expectedList)); + } + + @Test + public void makeRoomAtPrefix2() { + + loadList(10); + + list.fingerList.setFingerIndices(1, 3, 5, 8); + list.fingerList.makeRoomAtPrefix(4, 2, 1); + + IndexedLinkedList expectedList = new IndexedLinkedList<>(list); + expectedList.fingerList.setFingerIndices(1, 2, 5, 8); + + assertTrue(list.strongEquals(expectedList)); + } + + @Test + public void makeRoomAtPrefix3() { + + loadList(10); + + list.fingerList.setFingerIndices(1, 4, 6, 9); + list.fingerList.makeRoomAtPrefix(5, 2, 2); + + IndexedLinkedList expectedList = new IndexedLinkedList<>(list); + expectedList.fingerList.setFingerIndices(1, 2, 6, 9); + + assertTrue(list.strongEquals(expectedList)); + } + + @Test + public void makeRoomAtPrefix4() { + + loadList(10); + + list.fingerList.setFingerIndices(0, 1, 7, 8); + list.fingerList.makeRoomAtPrefix(4, 2, 2); + + IndexedLinkedList expectedList = new IndexedLinkedList<>(list); + expectedList.fingerList.setFingerIndices(0, 1, 7, 8); + + assertTrue(list.strongEquals(expectedList)); + } + + @Test + public void makeRoomAtPrefix5() { + + loadList(10); + + list.fingerList.setFingerIndices(0, 2, 6, 8); + list.fingerList.makeRoomAtPrefix(5, 2, 2); + + IndexedLinkedList expectedList = new IndexedLinkedList<>(list); + expectedList.fingerList.setFingerIndices(0, 2, 6, 8); + + assertTrue(list.strongEquals(expectedList)); + } + + @Test + public void makeRoomAtPrefix6() { + + loadList(101); + + list.fingerList.setFingerIndices( + 1, 3, 6, 10, 15, 29, 54, 57, 69, 72, 100 + ); + + list.fingerList.makeRoomAtPrefix(12, 4, 5); + + IndexedLinkedList expectedList = new IndexedLinkedList<>(list); + expectedList.fingerList.setFingerIndices( + 1, 3, 4, 5, 15, 29, 54, 57, 69, 72, 100 + ); + + assertTrue(list.strongEquals(expectedList)); + } + + @Test + public void makeRoomAtSuffix1() { + loadList(10); + + list.fingerList.setFingerIndices(3, 4, 8, 9); + list.fingerList.makeRoomAtSuffix(6, 2, 2, 2); + + IndexedLinkedList expectedList = new IndexedLinkedList<>(list); + expectedList.fingerList.setFingerIndices(3, 4, 8, 9); + + assertTrue(list.strongEquals(expectedList)); + } + + @Test + public void makeRoomAtSuffix2() { + loadList(10); + + list.fingerList.setFingerIndices(3, 4, 7, 9); + list.fingerList.makeRoomAtSuffix(6, 2, 2, 2); + + IndexedLinkedList expectedList = new IndexedLinkedList<>(list); + expectedList.fingerList.setFingerIndices(3, 4, 8, 9); + + assertTrue(list.strongEquals(expectedList)); + } + + @Test + public void makeRoomAtSuffix3() { + loadList(10); + + list.fingerList.setFingerIndices(3, 4, 7, 8); + list.fingerList.makeRoomAtSuffix(6, 2, 2, 2); + + IndexedLinkedList expectedList = new IndexedLinkedList<>(list); + expectedList.fingerList.setFingerIndices(3, 4, 8, 9); + + assertTrue(list.strongEquals(expectedList)); + } + + @Test + public void makeRoomAtSuffix4() { + loadList(10); + + list.fingerList.setFingerIndices(3, 4, 7, 8); + list.fingerList.makeRoomAtSuffix(6, 2, 0, 2); + + IndexedLinkedList expectedList = new IndexedLinkedList<>(list); + expectedList.fingerList.setFingerIndices(3, 4, 7, 8); + + assertTrue(list.strongEquals(expectedList)); + } + + @Test + public void arrangePrefix1() { + loadList(10); + + list.fingerList.setFingerIndices(1, 3, 5, 8); + list.fingerList.arrangePrefix(4, 2, 2); + + IndexedLinkedList expectedList = new IndexedLinkedList<>(list); + expectedList.fingerList.setFingerIndices(0, 1, 2, 3); + + assertTrue(list.strongEquals(expectedList)); + } + + @Test + public void arrangePrefix2() { + loadList(10); + + list.fingerList.setFingerIndices(5, 6, 8, 9); + list.fingerList.arrangePrefix(5, 0, 4); + + IndexedLinkedList expectedList = new IndexedLinkedList<>(list); + expectedList.fingerList.setFingerIndices(1, 2, 3, 4); + + assertTrue(list.strongEquals(expectedList)); + } + + @Test + public void add() { + list.clear(); + + for (int i = 0; i < 100; ++i) { + list.add(i); + list.checkInvarant(); + } + } + + @Test + public void arrangePrefix3() { + + loadList(101); + + list.fingerList.setFingerIndices( + 1, 3, 6, 10, 15, 29, 54, 57, 69, 72, 100 + ); + + list.fingerList.arrangePrefix(12, 4, 5); + + IndexedLinkedList expectedList = new IndexedLinkedList<>(list); + expectedList.fingerList.setFingerIndices( + 1, 3, 4, 5, 6, 7, 8, 9, 10, 72, 100 + ); + + assertTrue(list.strongEquals(expectedList)); + } + + @Test + public void pushCoveredFingersToSuffix1() { + + loadList(10); + + list.fingerList.setFingerIndices(1, 3, 4, 5); + list.fingerList.pushCoveredFingersToSuffix(6, 0, 3); + + IndexedLinkedList expectedList = new IndexedLinkedList<>(list); + expectedList.fingerList.setFingerIndices(1, 6, 7, 8); + + assertTrue(list.strongEquals(expectedList)); + } + + @Test + public void pushCoveredFingersToSuffix2() { + + loadList(10); + + list.fingerList.setFingerIndices(1, 3, 6, 9); + list.fingerList.pushCoveredFingersToSuffix(4, 2, 2); + + IndexedLinkedList expectedList = new IndexedLinkedList<>(list); + expectedList.fingerList.setFingerIndices(4, 5, 6, 9); + + assertTrue(list.strongEquals(expectedList)); + } + + @Test + public void arrange1() { + loadList(100); + + list.fingerList.setFingerIndices(5, 14, 15, 25, 26, 39, 49, 85, 86, 99); + list.fingerList.arrangePrefix(17, 3, 2); + list.fingerList.arrangeSuffix(83, 7, 3, 2); + + IndexedLinkedList expectedList = new IndexedLinkedList<>(list); + expectedList.fingerList.setFingerIndices( + 5, 6, 7, 8, 9, 83, 84, 85, 86, 99 + ); + + assertTrue(list.strongEquals(expectedList)); + } + + @Test + public void removeFingersOnDeleteRange1() { + loadList(100); + + list.fingerList.removeFingersOnDeleteRange(3, 4, 20); + FingerList expectedFingerList = new FingerList<>(null); + + expectedFingerList.size = 6; + + expectedFingerList.fingerArray[0] = new Finger<>(new Node<>(0), 0 ); + expectedFingerList.fingerArray[1] = new Finger<>(new Node<>(1), 1 ); + expectedFingerList.fingerArray[2] = new Finger<>(new Node<>(4), 4 ); + expectedFingerList.fingerArray[3] = new Finger<>(new Node<>(49), 29 ); + expectedFingerList.fingerArray[4] = new Finger<>(new Node<>(64), 44 ); + expectedFingerList.fingerArray[5] = new Finger<>(new Node<>(81), 61 ); + expectedFingerList.fingerArray[6] = new Finger<>(new Node<>(null), 80); + + assertEquals(expectedFingerList, list.fingerList); + } + + @Test + public void equals() { + IndexedLinkedList list1 = new IndexedLinkedList<>(); + IndexedLinkedList list2 = new IndexedLinkedList<>(); + FingerList fingerList1 = new FingerList<>(list1); + FingerList fingerList2 = new FingerList<>(list2); + + assertTrue(fingerList1.equals(fingerList1)); + assertFalse(fingerList1.equals(null)); + + fingerList2.size = 1; + + assertFalse(fingerList1.equals(fingerList2)); + assertFalse(fingerList1.equals(new Object())); + + fingerList1.size = 0; + fingerList1.appendFingerImpl(new Finger<>(new Node(1), 0)); + fingerList1.appendFingerImpl(new Finger<>(new Node(2), 1)); + + fingerList2.size = 0; + fingerList2.appendFingerImpl(new Finger<>(new Node(1), 0)); + + assertFalse(fingerList1.equals(fingerList2)); + + fingerList2.appendFingerImpl(new Finger<>(new Node(2), 1)); + + assertTrue(fingerList1.equals(fingerList2)); + + fingerList2.fingerArray[1] = null; + + assertFalse(fingerList1.equals(fingerList2)); + assertFalse(fingerList2.equals(fingerList1)); + } + + private void loadList(int size) { + for (int i = 0; i < size; i++) { + list.add(i); + list.checkInvarant(); + } + } +} diff --git a/src/test/java/org/apache/commons/collections4/list/IndexedLinkedListTest.java b/src/test/java/org/apache/commons/collections4/list/IndexedLinkedListTest.java new file mode 100644 index 0000000000..d8c083721b --- /dev/null +++ b/src/test/java/org/apache/commons/collections4/list/IndexedLinkedListTest.java @@ -0,0 +1,5048 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.collections4.list; + + +import org.apache.commons.collections4.list.IndexedLinkedList.BasicIterator; +import org.apache.commons.collections4.list.IndexedLinkedList.DescendingIterator; +import org.apache.commons.collections4.list.IndexedLinkedList.EnhancedIterator; +import org.apache.commons.collections4.list.IndexedLinkedList.EnhancedSubList; +import org.apache.commons.collections4.list.IndexedLinkedList.Finger; +import org.apache.commons.collections4.list.IndexedLinkedList.Node; +import static org.apache.commons.collections4.list.IndexedLinkedList.checkIndex; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.ConcurrentModificationException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Random; +import java.util.Set; +import java.util.Spliterator; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import org.junit.Assert; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeEach; +import static org.junit.jupiter.api.Assertions.*; + +public class IndexedLinkedListTest { + + private final IndexedLinkedList list = new IndexedLinkedList<>(); + private final List referenceList = new ArrayList<>(); + + @BeforeEach + public void setUp() { + list.clear(); + referenceList.clear(); + } + + @Test + public void debugAddAtIndex() { + Random random = new Random(665L); + + list.add(0, 0); + referenceList.add(0, 0); + + for (int i = 1; i < 100; ++i) { + int index = random.nextInt(list.size()); + referenceList.add(index, i); + list.add(index, i); + list.checkInvarant(); + assertEquals(referenceList, list); + } + } + + @Test + public void debugTryPushFingersToRight1() { + list.addAll(getIntegerList(17)); + list.fingerList.setFingerIndices(2, 3, 4, 6, 7); + list.remove(2); + list.checkInvarant(); + } + + @Test + public void onEmptyFingerListNonNullHead() { + list.head = new Node<>(3); + assertThrows(IllegalStateException.class, () -> list.checkInvarant()); + } + + @Test + public void onEmptyFingerListNonNullTail() { + list.tail = new Node<>(3); + assertThrows(IllegalStateException.class, () -> list.checkInvarant()); + } + + @Test + public void onEmptyFingerListNonEmptyList() { + list.addAll(getIntegerList(13)); + list.fingerList.size = 0; + assertThrows(IllegalStateException.class, () -> list.checkInvarant()); + } + + @Test + public void onNegativeFirstFingerNodeIndex() { + list.addAll(getIntegerList(13)); + list.fingerList.getFinger(0).index = -1; + assertThrows(IllegalStateException.class, () -> list.checkInvarant()); + } + + @Test + public void onTooMissingSecondFinger() { + list.addAll(getIntegerList(5)); + list.fingerList.setFingerIndices(0, 1, 2); + list.fingerList.fingerArray[1] = null; + assertThrows(IllegalStateException.class, () -> list.checkInvarant()); + } + + @Test + public void onTooMissingFirstFinger() { + list.addAll(getIntegerList(5)); + list.fingerList.setFingerIndices(0, 1, 2); + list.fingerList.fingerArray[0] = null; + assertThrows(IllegalStateException.class, () -> list.checkInvarant()); + } + + @Test + public void onInvalidListsize() { + list.addAll(getIntegerList(5)); + list.size = 6; + list.fingerList.getFinger(3).index = 6; + assertThrows(IllegalStateException.class, () -> list.checkInvarant()); + } + + @Test + public void onBadRecommendation() { + list.addAll(getIntegerList(5)); + list.size = 4; + assertThrows(IllegalStateException.class, () -> list.checkInvarant()); + } + + @Test + public void onNullSentinel() { + list.addAll(getIntegerList(16)); + list.fingerList.setFinger(4, null); + assertThrows(IllegalStateException.class, () -> list.checkInvarant()); + } + + @Test + public void onFingerListMismatch() { + list.addAll(getIntegerList(16)); + list.fingerList.size = 5; + assertThrows(IllegalStateException.class, () -> list.checkInvarant()); + } + + @Test + public void onWrongFingerListSize() { + list.addAll(getIntegerList(16)); + list.fingerList.size = 5; + assertThrows(IllegalStateException.class, () -> list.checkInvarant()); + } + + @Test + public void onJunkFingers() { + list.addAll(getIntegerList(16)); + list.fingerList.fingerArray[6] = new Finger<>(new Node<>(666), -13); + assertThrows(IllegalStateException.class, () -> list.checkInvarant()); + } + + @Test + public void onNonContractedFingerArray() { + list.addAll(getIntegerList(16)); + list.fingerList.fingerArray = + Arrays.copyOf(list.fingerList.fingerArray, 100); + + assertThrows(IllegalStateException.class, () -> list.checkInvarant()); + } + + @Test + public void getEntropy() { + list.addAll(getIntegerList(25)); + list.fingerList.setFingerIndices(0, 5, 10, 15, 20); + assertEquals(1.0, list.getEntropy(), 0.001); + } + + @Test + public void randomizeFingers() { + list.addAll(getIntegerList(10)); + list.randomizeFingers(); + list.randomizeFingers(new Random(3L)); + list.randomizeFingers(4L); + } + + @Test + public void strongEquals() { + list.addAll(getIntegerList(10)); + IndexedLinkedList other = new IndexedLinkedList<>(); + + for (int i = 0; i < list.size(); ++i) { + assertNotEquals(other, list); + assertFalse(list.strongEquals(other)); + other.add(i); + } + + assertEquals(other, list); + assertFalse(list.strongEquals(other)); + + list .fingerList.setFingerIndices(2, 4, 7, 8); + other.fingerList.setFingerIndices(2, 4, 7, 8); + + assertTrue(list.strongEquals(other)); + } + + @Test + public void deoptimizeOnEmptyList() { + list.deoptimize(); + assertTrue(list.fingerList.isEmpty()); + assertEquals(0, list.fingerList.getFinger(0).index); + assertNull(list.fingerList.getFinger(0).node); + } + + @Test + public void deoptimizeOnOneFinger() { + list.add(10); + assertEquals(1, list.fingerList.size()); + assertEquals(Integer.valueOf(10), + list.fingerList.getFinger(0).node.item); + + assertEquals(0, list.fingerList.getFinger(0).index); + + assertNull(list.fingerList.getFinger(1).node); + assertEquals(1, list.fingerList.getFinger(1).index); + } + + @Test + public void deoptimize2Fingers() { + list.addAll(getIntegerList(4)); + list.deoptimize(); + + IndexedLinkedList other = new IndexedLinkedList<>(list); + + other.fingerList.setFingerIndices(0, 3); + + assertEquals(other.fingerList, list.fingerList); + } + + @Test + public void deoptimize3Fingers() { + list.addAll(getIntegerList(8)); + list.deoptimize(); + + IndexedLinkedList other = new IndexedLinkedList<>(list); + + other.fingerList.setFingerIndices(0, 6, 7); + + assertEquals(other.fingerList, list.fingerList); + } + + @Test + public void deoptimize4Fingers() { + list.addAll(getIntegerList(11)); + list.deoptimize(); + + IndexedLinkedList other = new IndexedLinkedList<>(list); + + other.fingerList.setFingerIndices(0, 1, 9, 10); + + assertEquals(other.fingerList, list.fingerList); + } + + @Test + public void removeIntAtZeroIndex() { + referenceList.addAll(Arrays.asList(1, 2, 3, 4, 5)); + list.addAll(referenceList); + int iteration = 0; + + while (!referenceList.isEmpty()) { + referenceList.remove(0); + list.remove(0); + list.checkInvarant(); + assertEquals(referenceList, list); + } + + list.checkInvarant(); + assertEquals(referenceList, list); + } + + @Test + public void tryGetFingerListContract() { + list.addAll(getIntegerList(10)); + assertEquals(4, list.fingerList.size()); + list.remove(5); + assertEquals(3, list.fingerList.size()); + } + + @Test + public void deepCopy() { + list.addAll(getIntegerList(26)); + list.randomizeFingers(11L); + final IndexedLinkedList copy = list.deepCopy(); + assertTrue(listsEqual(list, copy)); + assertEquals(list.fingerList, copy.fingerList); + } + + @Test + public void removeIntAtLastIndex() { + referenceList.addAll(Arrays.asList(1, 2, 3, 4, 5)); + list.addAll(referenceList); + + while (!referenceList.isEmpty()) { + referenceList.remove(referenceList.size() - 1); + list.remove(list.size() - 1); + list.checkInvarant(); + assertEquals(referenceList, list); + } + + list.checkInvarant(); + assertEquals(referenceList, list); + } + + @Test + public void singleElementListIterator() { + ListIterator referenceListIterator = + referenceList.listIterator(); + + ListIterator myListIterator = list.listIterator(); + + assertEquals(referenceListIterator.previousIndex(), + myListIterator.previousIndex()); + + assertEquals(referenceListIterator.nextIndex(), + myListIterator.nextIndex()); + + referenceListIterator.add(1); + myListIterator.add(1); + + assertEquals(referenceList, list); + assertEquals(referenceListIterator.previousIndex(), + myListIterator.previousIndex()); + + assertEquals(referenceListIterator.nextIndex(), + myListIterator.nextIndex()); + + assertEquals(referenceListIterator.previous(), + myListIterator.previous()); + + try { + referenceListIterator.previous(); + fail(); + } catch (Exception ex) { + + } + + try { + myListIterator.previous(); + fail(); + } catch (Exception ex) { + + } + + assertEquals(referenceListIterator.next(), myListIterator.next()); + assertEquals(referenceListIterator.previous(), + myListIterator.previous()); + + assertEquals(referenceListIterator.next(), myListIterator.next()); + + try { + referenceListIterator.next(); + fail(); + } catch (Exception ex) { + + } + + try { + myListIterator.next(); + fail(); + } catch (Exception ex) { + + } + + assertEquals(referenceListIterator.previous(), + myListIterator.previous()); + + referenceListIterator.remove(); + myListIterator.remove(); + + assertEquals(referenceList, list); + + list.checkInvarant(); + } + + @Test + public void twoElementsListIterator() { + ListIterator referenceListIterator = + referenceList.listIterator(); + + ListIterator myListIterator = list.listIterator(); + + referenceListIterator.add(1); + referenceListIterator.add(2); + + myListIterator.add(1); + myListIterator.add(2); + + try { + myListIterator.next(); + fail(); + } catch (Exception ex) { + + } + + try { + referenceListIterator.next(); + fail(); + } catch (Exception ex) { + + } + + assertEquals(referenceListIterator.nextIndex(), + myListIterator.nextIndex()); + + assertEquals(referenceListIterator.previousIndex(), + myListIterator.previousIndex()); + + assertEquals(referenceListIterator.hasNext(), myListIterator.hasNext()); + assertEquals(referenceListIterator.hasPrevious(), + myListIterator.hasPrevious()); + + assertEquals(referenceListIterator.previous(), + myListIterator.previous()); + + assertEquals(referenceListIterator.hasNext(), myListIterator.hasNext()); + assertEquals(referenceListIterator.hasPrevious(), + myListIterator.hasPrevious()); + + assertEquals(referenceListIterator.previous(), + myListIterator.previous()); + + assertEquals(referenceListIterator.hasNext(), myListIterator.hasNext()); + assertEquals(referenceListIterator.hasPrevious(), + myListIterator.hasPrevious()); + + try { + myListIterator.previous(); + fail(); + } catch (Exception ex) { + + } + + try { + referenceListIterator.previous(); + fail(); + } catch (Exception ex) { + + } + + assertEquals(referenceList, list); + + list.clear(); + referenceList.clear(); + + myListIterator = list.listIterator(); + referenceListIterator = referenceList.listIterator(); + + myListIterator.add(2); + referenceListIterator.add(2); + + assertEquals(referenceList, list); + + assertEquals(referenceListIterator.nextIndex(), + myListIterator.nextIndex()); + + assertEquals(referenceListIterator.previousIndex(), + myListIterator.previousIndex()); + + assertEquals(referenceListIterator.previous(), + myListIterator.previous()); + + referenceListIterator.add(1); + myListIterator.add(1); + + assertEquals(referenceList, list); + + assertEquals(referenceListIterator.nextIndex(), + myListIterator.nextIndex()); + + assertEquals(referenceListIterator.previousIndex(), + myListIterator.previousIndex()); + + assertEquals(referenceListIterator.next(), myListIterator.next()); + + assertEquals(referenceListIterator.hasNext(), + myListIterator.hasNext()); + + assertEquals(referenceListIterator.hasPrevious(), + myListIterator.hasPrevious()); + + assertEquals(referenceListIterator.previous(), + myListIterator.previous()); + + assertEquals(referenceListIterator.hasNext(), + myListIterator.hasNext()); + + assertEquals(referenceListIterator.hasPrevious(), + myListIterator.hasPrevious()); + + assertEquals(referenceListIterator.previous(), + myListIterator.previous()); + + assertEquals(referenceListIterator.hasNext(), + myListIterator.hasNext()); + + assertEquals(referenceListIterator.hasPrevious(), + myListIterator.hasPrevious()); + + list.checkInvarant(); + } + + @Test + public void breakInvariant1() { + list.add(11); + list.add(12); + list.add(13); + + list.fingerList.fingerArray[0].index = 1; + list.fingerList.fingerArray[1].index = 0; + + assertThrows(IllegalStateException.class, () -> list.checkInvarant()); + } + + @Test + public void breakInvariant2() { + list.add(11); + list.add(12); + list.add(13); + + list.fingerList.fingerArray[list.fingerList.size()].index = + list.fingerList.size() + 10; + + assertThrows(IllegalStateException.class, () -> list.checkInvarant()); + } + + @Test + public void breakInvariant3() { + list.add(11); + list.add(12); + list.add(13); + + list.fingerList.fingerArray[list.fingerList.size()].node = + new Node<>(null); + + assertThrows(IllegalStateException.class, () -> list.checkInvarant()); + } + + @Test + public void breakInvariant4() { + list.add(11); + list.add(12); + list.add(13); + list.size = 2; + assertThrows(IllegalStateException.class, () -> list.checkInvarant()); + } + + @Test + public void breakInvariant5() { + list.add(11); + list.add(12); + list.add(13); + + list.fingerList.fingerArray[2] = new Finger<>(new Node<>(100), 2); + list.fingerList.fingerArray[3] = new Finger<>(null, 3); + list.fingerList.size = 3; + + assertThrows(IllegalStateException.class, () -> list.checkInvarant()); + } + + @Test + public void nodeToString() { + Node node = new Node<>(12); + assertEquals("", node.toString()); + } + + @Test + public void debugAdjustOnRemoveFirst() { + list.addAll(Arrays.asList(0, 1, 2, 3, 4)); + list.fingerList.fingerArray[0].index = 0; + list.fingerList.fingerArray[1].index = 1; + list.fingerList.fingerArray[2].index = 3; + list.fingerList.fingerArray[3].index = 5; + + list.fingerList.fingerArray[0].node = list.head; + list.fingerList.fingerArray[1].node = list.head.next; + list.fingerList.fingerArray[2].node = list.head.next.next.next; + + list.removeFirst(); + list.checkInvarant(); + } + + @Test + public void addFirstLarge() { + List l = getIntegerList(1000); + + for (int i = 0; i < l.size(); i++) { + list.addFirst(l.get(i)); + list.checkInvarant(); + } + + Collections.reverse(l); + assertTrue(listsEqual(list, l)); + list.checkInvarant(); + } + + @Test + public void addAllAtIndexLarge() { + Random random = new Random(1003L); + referenceList.clear(); + + for (int i = 0; i < 100; ++i) { + int index = random.nextInt(list.size() + 1); + List coll = getIntegerList(random.nextInt(100)); + list.addAll(index, coll); + list.checkInvarant(); + referenceList.addAll(index, coll); + } + + list.checkInvarant(); + assertTrue(listsEqual(list, referenceList)); + } + + @Test + public void constructAdd() { + List l = new IndexedLinkedList<>(Arrays.asList("a", "b", "c")); + + assertEquals(3, l.size()); + + assertEquals("a", l.get(0)); + assertEquals("b", l.get(1)); + assertEquals("c", l.get(2)); + } + + @Test + public void contains() { + assertFalse(list.contains(1)); + assertFalse(list.contains(2)); + assertFalse(list.contains(3)); + + assertEquals(0, list.size()); + assertTrue(list.isEmpty()); + + list.checkInvarant(); + list.addAll(Arrays.asList(1, 2, 3)); + + assertEquals(3, list.size()); + assertFalse(list.isEmpty()); + + assertTrue(list.contains(1)); + assertTrue(list.contains(2)); + assertTrue(list.contains(3)); + + list.checkInvarant(); + } + + @Test + public void descendingIterator() { + list.addAll(Arrays.asList(1, 2, 3)); + Iterator iterator = list.descendingIterator(); + + assertTrue(iterator.hasNext()); + assertEquals(Integer.valueOf(3), iterator.next()); + + assertTrue(iterator.hasNext()); + assertEquals(Integer.valueOf(2), iterator.next()); + + assertTrue(iterator.hasNext()); + assertEquals(Integer.valueOf(1), iterator.next()); + + assertFalse(iterator.hasNext()); + } + + @Test + public void descendingIteratorRemove1() { + list.addAll(Arrays.asList(1, 2, 3)); + Iterator iterator = list.descendingIterator(); + + list.checkInvarant(); + iterator.next(); + iterator.remove(); + list.checkInvarant(); + + assertEquals(2, list.size()); + + assertTrue(iterator.hasNext()); + assertEquals(Integer.valueOf(2), iterator.next()); + + assertTrue(iterator.hasNext()); + assertEquals(Integer.valueOf(1), iterator.next()); + + assertFalse(iterator.hasNext()); + } + + @Test + public void descendingIteratorForEachRemaining() { + list.addAll(Arrays.asList(1, 2, 3, -1, -2, -3)); + Iterator iterator = list.descendingIterator(); + + assertEquals(Integer.valueOf(-3), iterator.next()); + assertEquals(Integer.valueOf(-2), iterator.next()); + assertEquals(Integer.valueOf(-1), iterator.next()); + + class LocalConsumer implements Consumer { + + final List list = new ArrayList<>(); + + @Override + public void accept(Integer i) { + list.add(i); + } + } + + LocalConsumer consumer = new LocalConsumer(); + iterator.forEachRemaining(consumer); + + assertEquals(Integer.valueOf(3), consumer.list.get(0)); + assertEquals(Integer.valueOf(2), consumer.list.get(1)); + assertEquals(Integer.valueOf(1), consumer.list.get(2)); + } + + @Test + public void subListClearOnEmptyPrefix() { + list.addAll(getIntegerList(100)); + list.checkInvarant(); + list.get(10); + list.subList(5, 100).clear(); + list.checkInvarant(); + assertEquals(Arrays.asList(0, 1, 2, 3, 4), list); + } + + @Test + public void removeFirstUntilEmpty() { + list.addAll(getIntegerList(10)); + + while (!list.isEmpty()) { + list.removeFirst(); + list.checkInvarant(); + } + + list.checkInvarant(); + } + + @Test + public void moveFingerOutOfRemovalLocation() { + list.addAll(getIntegerList(16)); + list.fingerList.fingerArray[0] = + new Finger<>(list.tail.prev.prev.prev, 12); + + list.fingerList.fingerArray[1] = new Finger<>(list.tail.prev.prev, 13); + list.fingerList.fingerArray[2] = new Finger<>(list.tail.prev, 14); + list.fingerList.fingerArray[3] = new Finger<>(list.tail, 15); + + list.checkInvarant(); + list.remove(12); + list.checkInvarant(); + + Finger finger = list.fingerList.fingerArray[0]; + + assertEquals(Integer.valueOf(11), finger.node.item); + assertEquals(11, finger.index); + } + + @Test + public void removeIf() { + list.addAll(getIntegerList(10)); + list.removeIf((i) -> { + return i % 2 == 1; + }); + + list.checkInvarant(); + assertEquals(Arrays.asList(0, 2, 4, 6, 8), list); + } + + @Test + public void descendingIteratorRemove2() { + list.addAll(Arrays.asList(1, 2, 3, 4, 5)); + + Iterator iter = list.descendingIterator(); + + iter.next(); + iter.remove(); + list.checkInvarant(); + + assertEquals(Integer.valueOf(4), iter.next()); + iter.remove(); + list.checkInvarant(); + + assertEquals(3, list.size()); + + assertEquals(Integer.valueOf(3), iter.next()); + iter.remove(); + list.checkInvarant(); + + assertEquals(2, list.size()); + + assertEquals(Integer.valueOf(2), iter.next()); + assertEquals(Integer.valueOf(1), iter.next()); + + iter.remove(); + list.checkInvarant(); + + assertEquals(1, list.size()); + assertEquals(Integer.valueOf(2), list.get(0)); + } + + @Test + public void elementThrowsOnEmptyList() { + assertThrows(NoSuchElementException.class, () -> list.element()); + } + + @Test + public void removeRangeBug() { + for (int i = 0; i < 40_000; i++) { + list.add(Integer.MIN_VALUE); + } + + list.subList(0, 500).clear(); + list.checkInvarant(); + list.subList(20411, 20911).clear(); + list.checkInvarant(); + assertEquals(39000, list.size()); + } + + @Test + public void removeRangeBug2() { + list.addAll(Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)); + list.subList(0, 7).clear(); + list.checkInvarant(); + assertEquals(Arrays.asList(7, 8, 9, 10), list); + list.subList(0, 2).clear(); + list.checkInvarant(); + assertEquals(Arrays.asList(9, 10), list); + } + + @Test + public void element() { + list.add(1); + list.add(2); + + assertEquals(Integer.valueOf(1), list.element()); + + list.remove(); + + assertEquals(Integer.valueOf(2), list.element()); + } + + @Test + public void listEquals() { + list.addAll(Arrays.asList(1, 2, 3, 4)); + List otherList = Arrays.asList(1, 2, 3, 4); + + assertTrue(list.equals(otherList)); + + list.remove(Integer.valueOf(3)); + list.checkInvarant(); + + assertFalse(list.equals(otherList)); + + assertFalse(list.equals(null)); + assertTrue(list.equals(list)); + + Set set = new HashSet<>(list); + + assertFalse(list.equals(set)); + + list.clear(); + list.checkInvarant(); + list.addAll(Arrays.asList(0, 1, 2, 3)); + list.checkInvarant(); + otherList = Arrays.asList(0, 1, 4, 3); + + assertFalse(list.equals(otherList)); + } + + class DummyList extends ArrayList { + private final class DummyIterator implements Iterator { + + @Override + public boolean hasNext() { + return true; + } + + @Override + public Integer next() { + return 0; + } + } + + public Iterator iterator() { + return new DummyIterator(); + } + + public int size() { + return 2; + } + } + + @Test + public void sublistClear1() { + list.addAll(getIntegerList(100)); + list.checkInvarant(); + List sublist = list.subList(49, 51); + assertEquals(2, sublist.size()); + sublist.clear(); + list.checkInvarant(); + assertEquals(98, list.size()); + assertEquals(0, sublist.size()); + } + + @Test + public void sublistClear2() { + int fromIndex = 10; + int toIndex = 990; + + List referenceList = getIntegerList(1000); + list.addAll(referenceList); + referenceList.subList(fromIndex, toIndex).clear(); + list.subList(fromIndex, toIndex).clear(); + list.checkInvarant(); + assertEquals(referenceList, list); + } + + private void checkSubList(int size, int fromIndex, int toIndex) { + List referenceList = getIntegerList(size); + list.addAll(referenceList); + list.checkInvarant(); + referenceList.subList(fromIndex, toIndex).clear(); + list.subList(fromIndex, toIndex).clear(); + list.checkInvarant(); + assertEquals(referenceList, list); + } + + @Test + public void sublistClear3() { + int size = 1_000_000; + int fromIndex = 10; + int toIndex = 999_990; + checkSubList(size, fromIndex, toIndex); + } + + @Test + public void sublistClear4() { + int size = 1_000; + int fromIndex = 10; + int toIndex = 500; + checkSubList(size, fromIndex, toIndex); + } + + @Test + public void sublistClear5() { + int size = 100; + int fromIndex = 10; + int toIndex = 90; + checkSubList(size, fromIndex, toIndex); + } + + @Test + public void sublistClearLeftOfSmall() { + list.add(1); + list.add(2); + + list.subList(0, 1).clear(); + + assertEquals(1, list.size()); + assertEquals(Integer.valueOf(2), list.get(0)); + list.checkInvarant(); + } + + @Test + public void sublistClearRightOfSmall() { + list.add(1); + list.add(2); + + list.subList(1, 2).clear(); + list.checkInvarant(); + + assertEquals(1, list.size()); + assertEquals(Integer.valueOf(1), list.get(0)); + } + + @Test + public void sublistClearRightOfSmall2() { + List referenceList = new ArrayList<>(getIntegerList(20)); + list.addAll(referenceList); + + list.subList(0, 5).clear(); + list.checkInvarant(); + referenceList.subList(0, 5).clear(); + + assertEquals(referenceList, list); + } + + @Test + public void debugClear1() { + list.addAll(getIntegerList(12)); + list.subList(4, 9).clear(); + list.checkInvarant(); + assertEquals(Arrays.asList(0, 1, 2, 3, 9, 10, 11), list); + } + + @Test + public void bruteforceCollectionSetAll() { + for (int sz = 0; sz < 10; ++sz) { + list.clear(); + list.checkInvarant(); + referenceList.clear(); + + List tmpList = getIntegerList(sz); + + list.addAll(tmpList); + list.checkInvarant(); + referenceList.addAll(tmpList); + + assertEquals(referenceList, list); + } + } + + @Test + public void bruteforceCollectionAppend() { + for (int sz = 0; sz < 10; ++sz) { + for (int sz2 = 0; sz2 < 10; ++sz2) { + list.clear(); + list.addAll(getIntegerList(sz)); + list.checkInvarant(); + + referenceList.clear(); + referenceList.addAll(list); + + List tmpList = getIntegerList(sz2); + + list.addAll(tmpList); + list.checkInvarant(); + + referenceList.addAll(tmpList); + + assertEquals(referenceList, list); + } + } + } + + @Test + public void bruteforceCollectionPrepend() { + for (int sz = 0; sz < 10; ++sz) { + for (int sz2 = 0; sz2 < 10; ++sz2) { + list.clear(); + list.addAll(getIntegerList(sz)); + list.checkInvarant(); + + referenceList.clear(); + referenceList.addAll(list); + + List tmpList = getIntegerList(sz2); + + list.addAll(0, tmpList); + list.checkInvarant(); + + referenceList.addAll(0, tmpList); + + assertEquals(referenceList, list); + } + } + } + + @Test + public void bruteforceCollectionInsert() { + for (int sz = 0; sz < 10; ++sz) { + for (int sz2 = 0; sz2 < 10; ++sz2) { + for (int index = 0; index <= sz; ++index) { + list.clear(); + list.addAll(getIntegerList(sz)); + list.checkInvarant(); + + referenceList.clear(); + referenceList.addAll(list); + + List tmpList = getIntegerList(sz2); + + list.addAll(index, tmpList); + list.checkInvarant(); + + referenceList.addAll(index, tmpList); + + assertEquals(referenceList, list); + } + } + } + } + + @Test + public void debugClear2() { + list.addAll(getIntegerList(10)); + list.subList(0, 4).clear(); + list.checkInvarant(); + referenceList.clear(); + referenceList.addAll(Arrays.asList(4, 5, 6, 7, 8, 9)); + assertEquals(referenceList, list); + + list.clear(); + list.addAll(getIntegerList(10)); + list.subList(6, 10).clear(); + list.checkInvarant(); + referenceList.clear(); + referenceList.addAll(Arrays.asList(0, 1, 2, 3, 4, 5)); + assertEquals(referenceList, list); + } + + @Test + public void subListClear2Fingers3Nodes_1() { + list.addAll(Arrays.asList(0, 1, 2)); + list.subList(0, 1).clear(); + list.checkInvarant(); + assertEquals(Arrays.asList(1, 2), list); + } + + @Test + public void sublistClear6() { + list.addAll(getIntegerList(1000)); + list.subList(70, 1000).clear(); + list.checkInvarant(); + } + + @Test + public void removeRangeNodes() { + list.addAll(getIntegerList(20)); + list.subList(5, 10).clear(); + + assertEquals(15, list.size()); + list.checkInvarant(); + } + + @Test + public void bruteForceSubListClearFromTo() { + List data = getIntegerList(100); + int iteration = 0; + + for (int fromIndex = 0; fromIndex <= 100; fromIndex++) { + for (int toIndex = fromIndex; toIndex <= 100; toIndex++) { + iteration++; + + list.clear(); + list.addAll(data); + referenceList.clear(); + referenceList.addAll(data); + + list.subList(fromIndex, toIndex).clear(); + list.checkInvarant(); + + referenceList.subList(fromIndex, toIndex).clear(); + assertEquals(referenceList, list); + } + } + } + + @Test + public void removeRangeSmall1() { + list.addAll(getIntegerList(9)); + list.fingerList.setFingerIndices(0, 1, 7); + list.checkInvarant(); + referenceList.addAll(list); + + list.subList(4, 7).clear(); + list.checkInvarant(); + referenceList.subList(4, 7).clear(); + + assertEquals(referenceList, list); + } + + @Test + public void removeRangeSmall2() { + list.addAll(getIntegerList(9)); + list.fingerList.setFingerIndices(4, 6, 7); + list.checkInvarant(); + referenceList.addAll(list); + + list.subList(0, 3).clear(); + list.checkInvarant(); + referenceList.subList(0, 3).clear(); + + assertEquals(referenceList, list); + } + + @Test + public void removeRangeSmall3() { + list.addAll(getIntegerList(9)); + list.fingerList.setFingerIndices(1, 2, 4); + list.checkInvarant(); + referenceList.addAll(list); + + list.subList(6, 9).clear(); + list.checkInvarant(); + referenceList.subList(6, 9).clear(); + + assertEquals(referenceList, list); + } + + @Test + public void removeRangeSmall4() { + list.addAll(getIntegerList(9)); + list.fingerList.setFingerIndices(2, 4, 7); + list.checkInvarant(); + referenceList.addAll(list); + + list.subList(3, 6).clear(); + list.checkInvarant(); + referenceList.subList(3, 6).clear(); + + assertEquals(referenceList, list); + } + + @Test + public void removeRangeMedium1() { + list.addAll(getIntegerList(100)); + list.fingerList.setFingerIndices(1, 10, 11, 12, 13, 40, 50, 90, 92, 93); + list.checkInvarant(); + referenceList.addAll(list); + + list.subList(11, 13).clear(); + list.checkInvarant(); + referenceList.subList(11, 13).clear(); + + assertEquals(referenceList, list); + } + + @Test + public void removeRangeMedium2() { + list.addAll(getIntegerList(100)); + list.fingerList.setFingerIndices(5, 15, 25, 35, 45, 55, 60, 61, 62, 63); + list.checkInvarant(); + referenceList.addAll(list); + + list.subList(61, 63).clear(); + list.checkInvarant(); + referenceList.subList(61, 63).clear(); + + assertEquals(referenceList, list); + } + + @Test + public void removeRangePrefix1() { + list.addAll(getIntegerList(100)); + list.fingerList.setFingerIndices(0, 1, 2, 3, 10, 20, 30, 40, 50, 70); + list.checkInvarant(); + + referenceList.addAll(list); + + list.subList(0, 3).clear(); + list.checkInvarant(); + + referenceList.subList(0, 3).clear(); + + assertEquals(referenceList, list); + } + + @Test + public void removeRangeSuffix1() { + list.addAll(getIntegerList(100)); + list.fingerList.setFingerIndices(1, 10, 20, 25, 30, 31, 95, 96, 97, 98); + list.checkInvarant(); + + referenceList.addAll(list); + + list.subList(97, 100).clear(); + list.checkInvarant(); + + referenceList.subList(97, 100).clear(); + + assertEquals(referenceList, list); + } + + @Test + public void removeRangeEqualFingersCoveringFingers() { + list.addAll(getIntegerList(25)); + list.fingerList.setFingerIndices(1, 10, 11, 21, 23); + list.checkInvarant(); + + referenceList.addAll(list); + + list.subList(5, 21).clear(); + list.checkInvarant(); + + referenceList.subList(5, 21).clear(); + + assertEquals(referenceList, list); + } + + @Test + public void removeRangeCase4() { + list.addAll(getIntegerList(100)); + referenceList.addAll(list); + referenceList.subList(10, 80).clear(); + + list.fingerList.setFingerIndices(1, 3, 5, 50, 51, 52, 95, 96, 98, 99); + list.checkInvarant(); + + list.subList(10, 80).clear(); + list.checkInvarant(); + + assertEquals(referenceList, list); + } + + @Test + public void removeRangeCase5() { + list.addAll(getIntegerList(100)); + referenceList.addAll(list); + referenceList.subList(6, 90).clear(); // rangelen = 84. 4 fingers, 6 + // off away. + list.fingerList.setFingerIndices( + 10, 20, 30, 40, 50, 51, 52, 60, 93, 94); + + list.checkInvarant(); + + list.subList(6, 90).clear(); + list.checkInvarant(); + + assertEquals(referenceList, list); + } + + @Test + public void bruteForceSubListClearFromToWithRandomization666() { + Random random = new Random(666L); + + List data = getIntegerList(100); + int iteration = 0; + + for (int fromIndex = 0; fromIndex <= 100; fromIndex++) { + for (int toIndex = fromIndex; toIndex <= 100; toIndex++) { + iteration++; + + list.clear(); + list.addAll(data); + list.randomizeFingers(random); + + referenceList.clear(); + referenceList.addAll(data); + + list.subList(fromIndex, toIndex).clear(); + referenceList.subList(fromIndex, toIndex).clear(); + + assertEquals(referenceList, list); + list.checkInvarant(); + } + } + } + + @Test + public void bruteForceSubListClearFromToWithRandomization13() { + Random random = new Random(13L); + + List data = getIntegerList(100); + int iteration = 0; + + for (int fromIndex = 0; fromIndex <= 100; fromIndex++) { + for (int toIndex = fromIndex; toIndex <= 100; toIndex++) { + iteration++; + + list.clear(); + list.addAll(data); + list.randomizeFingers(random); + + referenceList.clear(); + referenceList.addAll(data); + + list.subList(fromIndex, toIndex).clear(); + referenceList.subList(fromIndex, toIndex).clear(); + + assertEquals(referenceList, list); + list.checkInvarant(); + } + } + } + + @Test + public void bruteForceSublistClearOnSmallLists() { + long seed = 1662121251795L; + Random random = new Random(seed); + + for (int i = 0; i < 1000; ++i) { + int size = 1 + random.nextInt(400); + List referenceList = new ArrayList<>(getIntegerList(size)); + list.clear(); + list.addAll(referenceList); + + int fromIndex = random.nextInt(size); + int toIndex = Math.min(size, fromIndex + random.nextInt(size)); + + list.subList(fromIndex, toIndex).clear(); + referenceList.subList(fromIndex, toIndex).clear(); + + list.checkInvarant(); + assertEquals(referenceList, list); + } + } + + @Test + public void debugRemoveRange() { + list.addAll(getIntegerList(15)); + list.subList(6, 11).clear(); + list.checkInvarant(); + } + + @Test + public void debugSmallSublistClear1() { + list.addAll(getIntegerList(14)); + list.subList(3, 7).clear(); + list.checkInvarant(); + } + + @Test + public void optmize() { + list.addAll(getIntegerList(100)); + Random random = new Random(100L); + + for (int i = 0; i < 50; ++i) { + list.get(random.nextInt(list.size())); + } + + list.checkInvarant(); + list.optimize(); + list.checkInvarant(); + } + + @Test + public void removeFromRange() { + list.addAll(getIntegerList(10)); + List referenceList = new ArrayList<>(list); + + List subList1 = list.subList(1, 9); + List subList2 = referenceList.subList(1, 9); + + assertEquals(subList2, subList1); + + // Remove from ArrayList: + subList2.remove(Integer.valueOf(0)); + + // Remove from IndexedLinkedList: + list.checkInvarant(); + subList1.remove(Integer.valueOf(0)); + list.checkInvarant(); + + assertEquals(subList2, subList1); + + assertEquals(8, subList1.size()); + assertEquals(10, list.size()); + + subList1.remove(Integer.valueOf(5)); + subList2.remove(Integer.valueOf(5)); + + assertEquals(subList2, subList1); + + assertEquals(7, subList1.size()); + assertEquals(9, list.size()); + + assertEquals(referenceList, list); + } + + @Test + public void sort() { + Random random = new Random(1L); + + for (int i = 0; i < 100; ++i) { + list.add(random.nextInt(70)); + } + + List referenceList = new ArrayList<>(list); + + Comparator comp = (i1, i2) -> { + return Integer.compare(i1, i2); + }; + + list.checkInvarant(); + list.sort(comp); + list.checkInvarant(); + + referenceList.sort(comp); + assertEquals(referenceList, list); + } + + @Test + public void distributeFingersOnSmallerList() { + list.addAll(getIntegerList(20)); + final Random random = new Random(13); + Collections.shuffle(list, random); + list.randomizeFingers(random); + + list.distributeFingers(5, list.size() - 3); + list.checkInvarant(); + } + + @Test + public void sortSubLists() { + Random random = new Random(12L); + + for (int i = 0; i < 10; ++i) { + list.clear(); + list.addAll(getIntegerList(500)); + Collections.shuffle(list, random); + List referenceList = new ArrayList<>(list); + + int f = random.nextInt(list.size() + 1); + int t = random.nextInt(list.size() - 1); + + int fromIndex = Math.min(f, t); + int toIndex = Math.max(f, t); + + Comparator cmp = Integer::compare; + list.checkInvarant(); + list.subList(fromIndex, toIndex).sort(cmp); + list.checkInvarant(); + referenceList.subList(fromIndex, toIndex).sort(cmp); + + assertEquals(referenceList, list); + } + } + + @Test + public void sortSubListOfSubList() { + list.addAll(Arrays.asList(4, 1, 0, 2, 6, 8, 4, 1, 3)); + List referenceList = new ArrayList<>(list); + Comparator cmp = Integer::compare; + list.checkInvarant(); + list.subList(1, 7).subList(1, 4).sort(cmp); + list.checkInvarant(); + referenceList.subList(1, 7).subList(1, 4).sort(cmp); + assertEquals(referenceList, list); + } + + @Test + public void subListAdd() { + list.addAll(Arrays.asList(3, 2, 1, 4, 5)); + list.subList(1, 4).add(Integer.valueOf(1000)); + list.checkInvarant(); + assertEquals(Arrays.asList(3, 2, 1, 4, 1000, 5), list); + } + + @Test + public void subListAddInt() { + list.addAll(Arrays.asList(3, 2, 1, 4, 5)); + list.subList(1, 4).add(1, Integer.valueOf(1000)); + list.checkInvarant(); + assertEquals(Arrays.asList(3, 2, 1000, 1, 4, 5), list); + } + + @Test + public void subListAddAll() { + list.addAll(Arrays.asList(3, 2, 1, 4, 5)); + list.subList(1, 4).addAll(Arrays.asList(10, 11)); + list.checkInvarant(); + assertEquals(Arrays.asList(3, 2, 1, 4, 10, 11, 5), list); + } + + @Test + public void subListAddAllInt() { + list.addAll(Arrays.asList(3, 2, 1, 4, 5)); + list.subList(1, 4).addAll(0, Arrays.asList(10, 11)); + list.checkInvarant(); + assertEquals(Arrays.asList(3, 10, 11, 2, 1, 4, 5), list); + } + + @Test + public void subListContains() { + list.addAll(Arrays.asList(3, 2, 1, 4, 5, 8, 7)); + + assertTrue(list.subList(1, 5).contains(2)); + assertTrue(list.subList(1, 5).contains(1)); + assertTrue(list.subList(1, 5).contains(4)); + assertTrue(list.subList(1, 5).contains(5)); + + assertFalse(list.subList(1, 5).contains(3)); + assertFalse(list.subList(1, 5).contains(8)); + assertFalse(list.subList(1, 5).contains(7)); + } + + @Test + public void subListContainsAll() { + list.addAll(Arrays.asList(3, 2, 1, 4, 5, 8, 7)); + + assertTrue(list.subList(1, 5).containsAll(Arrays.asList(2, 4, 1, 5))); + + assertFalse(list.subList(1, 5) + .containsAll(Arrays.asList(2, 4, 1, 5, 3))); + + assertFalse(list.subList(1, 5) + .containsAll(Arrays.asList(2, 4, 1, 5, 8))); + + assertFalse(list.subList(1, 5) + .containsAll(Arrays.asList(2, 4, 1, 5, 7))); + } + + @Test + public void containsAll() { + list.addAll(Arrays.asList(4, 1, 8, 7, 5, 6)); + + assertFalse(list.containsAll(Arrays.asList(8, 7, 3))); + assertFalse(list.containsAll(Arrays.asList(1, 4, 3))); + assertFalse(list.containsAll(Arrays.asList(-1))); + + list.addAll(Arrays.asList(4, 1, 8, 7, 5, 6)); + + assertTrue(list.containsAll(Arrays.asList(8, 7))); + assertTrue(list.containsAll(Arrays.asList())); + assertTrue(list.containsAll(Arrays.asList(8, 1, 4, 7, 6, 5))); + } + + @Test + public void hashCode2() { + List referenceList = new ArrayList<>(); + + list.add(null); + referenceList.add(null); + assertEquals(referenceList.hashCode(), list.hashCode()); + + list.add(1); + referenceList.add(1); + assertEquals(referenceList.hashCode(), list.hashCode()); + + list.add(5); + referenceList.add(5); + assertEquals(referenceList.hashCode(), list.hashCode()); + + list.add(7); + referenceList.add(7); + assertEquals(referenceList.hashCode(), list.hashCode()); + + list.add(null); + referenceList.add(null); + assertEquals(referenceList.hashCode(), list.hashCode()); + } + + @Test + public void removeAll() { + list.addAll(Arrays.asList(4, 1, 8, 9, 5, 1, -1, 5, 2, 3, 0)); + list.removeAll(Arrays.asList(1, -1, 5)); + list.checkInvarant(); + // list = <4, 8, 9, 2, 3, 0> + assertEquals(Arrays.asList(4, 8, 9, 2, 3, 0), list); + + list.removeAll(Arrays.asList(-2, 8, 0)); + list.checkInvarant(); + // list = <4, 9, 2, 3> + assertEquals(Arrays.asList(4, 9, 2, 3), list); + } + + @Test + public void replaceAll() { + list.addAll(Arrays.asList(3, 2, 1)); + list.replaceAll((i) -> { + return i * 3 + 1; + }); + + assertEquals(Arrays.asList(10, 7, 4), list); + } + + @Test + public void noNextItemEnhancedSubList() { + list.addAll(Arrays.asList(1, 2, 3, 4)); + List subList = list.subList(0, 3); + ListIterator iter = subList.listIterator(3); + + assertThrows(NoSuchElementException.class, () -> iter.next()); + } + + @Test + public void noPreviousItemEnhancedSubList() { + list.addAll(Arrays.asList(1, 2, 3, 4)); + List subList = list.subList(0, 3); + ListIterator iter = subList.listIterator(0); + assertThrows(NoSuchElementException.class, () -> iter.previous()); + } + + @Test + public void removeIfOnNothing() { + Predicate predicate = (i) -> false; + list.addAll(Arrays.asList(1, 2, 4, 8)); + assertFalse(list.subList(0, 3).removeIf(predicate)); + } + + @Test + public void checkInsertionIndexOnNegative() { + list.addAll(Arrays.asList(1, 3, 5)); + EnhancedSubList sl = (EnhancedSubList) list.subList(0, 3); + + assertThrows(IndexOutOfBoundsException.class, + () -> sl.checkInsertionIndex(-1)); + } + + @Test + public void checkInsertionIndexOnTooLarge() { + list.addAll(Arrays.asList(1, 3, 5)); + EnhancedSubList sl = (EnhancedSubList) list.subList(0, 3); + assertThrows(IndexOutOfBoundsException.class, + () -> sl.checkInsertionIndex(-1)); + } + + @Test + public void returnsFalseIfDidNotRemoveObjects() { + list.add(1); + list.add(2); + EnhancedSubList subList = (EnhancedSubList) list.subList(0, 2); + + assertFalse(subList.removeAll(Arrays.asList(4, 3))); + } + + @Test + public void retainAll() { + list.addAll(Arrays.asList(1, 2, 3, 4, 5, 6)); + list.checkInvarant(); + list.retainAll(Arrays.asList(2, 3, 5, 7)); + list.checkInvarant(); + + assertEquals(Arrays.asList(2, 3, 5), list); + + list.checkInvarant(); + list.retainAll(Arrays.asList(3)); + list.checkInvarant(); + + assertEquals(Arrays.asList(3), list); + + list.checkInvarant(); + list.retainAll(Arrays.asList(0)); + list.checkInvarant(); + + assertTrue(list.isEmpty()); + } + + @Test + public void toArrayGeneric() { + list.addAll(Arrays.asList(3, 1, 2, 5, 4)); + + Integer[] array = new Integer[7]; + + array[5] = 10; + array[6] = 11; + + Integer[] cloneArray = list.toArray(array); + + assertTrue(cloneArray == array); + assertNull(cloneArray[5]); + assertEquals(Integer.valueOf(11), cloneArray[6]); + + array = new Integer[3]; + + cloneArray = list.toArray(array); + + assertFalse(cloneArray == array); + + assertEquals(5, cloneArray.length); + + for (int i = 0; i < cloneArray.length; ++i) { + assertEquals(list.get(i), cloneArray[i]); + } + } + + @Test + public void toString2() { + list.addAll(Arrays.asList(1, 11, 111)); + assertEquals("[1, 11, 111]", list.toString()); + } + + @Test + public void subListToString() { + list.addAll(Arrays.asList(0, 2, 22, 222, 0)); + assertEquals("[2, 22, 222]", list.subList(1, 4).toString()); + } + + @Test + public void listIteratorSetAddThrows() { + list.addAll(getIntegerList(10)); + ListIterator listIterator = list.listIterator(3); + + list.checkInvarant(); + listIterator.add(100); + list.checkInvarant(); + assertThrows(IllegalStateException.class, + () -> listIterator.set(-100)); + + } + + @Test + public void subListIteratorSetAddThrows() { + list.addAll(getIntegerList(10)); + ListIterator listIterator = list.subList(4, 9).listIterator(3); + + listIterator.add(100); + + assertThrows(IllegalStateException.class, + () -> listIterator.set(-100)); + } + + @Test + public void listIteratorRemoveWithouttNextPreviousThrows() { + list.addAll(getIntegerList(5)); + ListIterator iter = list.listIterator(1); + assertThrows(IllegalStateException.class, + () -> iter.remove()); + } + + @Test + public void subListIteratorRemoveWithouttNextPreviousThrows() { + list.addAll(getIntegerList(8)); + List subList = list.subList(1, 6); + ListIterator iter = subList.listIterator(5); + assertThrows(IllegalStateException.class, + () -> iter.remove()); + } + + @Test + public void listIteratorSetAdd() { + list.addAll(getIntegerList(5)); + ListIterator listIterator = list.listIterator(2); + // list = <0, 1, 2, 3, 4> + // iter: | + + listIterator.add(100); + list.checkInvarant(); + // list = <0, 1, 100, 2, 3, 4> + assertEquals(Integer.valueOf(2), listIterator.next()); + listIterator.set(-100); + // list = <0, 1, 100, -100, 3, 4> + list.checkInvarant(); + + assertEquals(Arrays.asList(0, 1, 100, -100, 3, 4), list); + // list = <0, 1, 100, -100, 3, 4> + + listIterator = list.listIterator(4); + // list = <0, 1, 100, -100, 3, 4> + // iter: | + listIterator.add(1000); + list.checkInvarant(); + // list = <0, 1, 100, -100, 1000, 3, 4> + assertEquals(Arrays.asList(0, 1, 100, -100, 1000, 3, 4), list); + + assertEquals(Integer.valueOf(1000), listIterator.previous()); + listIterator.set(-1000); + list.checkInvarant(); + // list = <0, 1, 100, -1000, 1000, 3, 4> + + assertEquals( + Arrays.asList(0, 1, 100, -100, -1000, 3, 4), + list); + } + + @Test + public void subListIteratorSetAdd() { + list.addAll(getIntegerList(8)); + List subList = list.subList(1, 6); + ListIterator listIterator = subList.listIterator(2); + // subList = <1, 2, 3, 4, 5> + // iter: | + + listIterator.add(100); + list.checkInvarant(); + assertEquals(Arrays.asList(1, 2, 100, 3, 4, 5), subList); + // subList = <1, 2, 100, 3, 4, 5> + + assertEquals(Integer.valueOf(3), listIterator.next()); + listIterator.set(-100); + list.checkInvarant(); + // subList = <1, 2, 100, -100, 4, 5> + // iter: | + + assertEquals(Arrays.asList(1, 2, 100, -100, 4, 5), subList); + assertEquals(Integer.valueOf(4), listIterator.next()); + assertEquals(Integer.valueOf(5), listIterator.next()); + + list.checkInvarant(); + listIterator.add(1000); + list.checkInvarant(); + + // list = <1, 2, 100, -100, 4, 5, 1000> + assertEquals(Arrays.asList(1, 2, 100, -100, 4, 5, 1000), subList); + + assertEquals(Integer.valueOf(1000), listIterator.previous()); + list.checkInvarant(); + listIterator.set(-1000); + list.checkInvarant(); + // list = <1, 2, 100, -100, 4, 5, -1000> + + assertEquals(Arrays.asList(1, 2, 100, -100, 4, 5, -1000), subList); + } + + @Test + public void listIteratorThrowsOnSetAfterRemove() { + list.addAll(getIntegerList(8)); + ListIterator iterator = list.listIterator(2); + iterator.previous(); + list.checkInvarant(); + iterator.remove(); + list.checkInvarant(); + assertThrows(IllegalStateException.class, + () -> iterator.set(1000)); + } + + @Test + public void subListIteratorThrowsOnSetAfterRemove() { + list.addAll(getIntegerList(8)); + List subList = list.subList(1, 7); + + ListIterator iterator = subList.listIterator(2); + iterator.previous(); + iterator.remove(); + assertThrows(IllegalStateException.class, + () -> iterator.set(1000)); + } + + @Test + public void debugChainResolve() { + list.addAll(getIntegerList(9_999)); + list.checkInvarant(); + list.subList(5, list.size() - 5).clear(); + list.checkInvarant(); + } + + @Test + public void debugChainResolve2() { + list.addAll(getIntegerList(9_999)); + list.checkInvarant(); + list.subList(5, list.size()).clear(); + list.checkInvarant(); + } + + @Test + public void debugChainResolve3() { + list.addAll(getIntegerList(9_999)); + list.checkInvarant(); + list.subList(0, list.size() - 5).clear(); + list.checkInvarant(); + } + + @Test + public void addFingersAfterAppendAll() { + list.addAll(getIntegerList(9_990)); + assertEquals(100, list.getFingerListSize()); + list.checkInvarant(); + list.addAll(Arrays.asList(-1, -2)); + list.checkInvarant(); + assertEquals(100, list.getFingerListSize()); + } + + @Test + public void canUseListAsMapKey() { + List l = new ArrayList<>(Arrays.asList(1, 5, 3)); + list.addAll(l); + + Map, Integer> map = new HashMap<>(); + + map.put(l, 100); + + assertEquals(Integer.valueOf(100), map.get(list)); + + l = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5, 6)); + list.clear(); + list.addAll(l); + + List subList1 = l.subList(1, 4); + List subList2 = list.subList(1, 4); + + map.put(subList1, 200); + assertEquals(Integer.valueOf(200), map.get(subList2)); + } + + @Test + public void spliteratorOverSubList() { + list.addAll(getIntegerList(10)); + List subList = list.subList(1, 9); + + Spliterator spliterator = subList.spliterator(); + + class MyConsumer implements Consumer { + final List data = new ArrayList<>(); + + @Override + public void accept(Integer i) { + data.add(i); + } + } + + MyConsumer myConsumer = new MyConsumer(); + + assertEquals(8L, spliterator.getExactSizeIfKnown()); + assertEquals(8L, spliterator.estimateSize()); + + spliterator.tryAdvance(myConsumer); + assertEquals(1, myConsumer.data.size()); + + spliterator.tryAdvance(myConsumer); + assertEquals(2, myConsumer.data.size()); + + spliterator.tryAdvance(myConsumer); + assertEquals(3, myConsumer.data.size()); + + spliterator.tryAdvance(myConsumer); + assertEquals(4, myConsumer.data.size()); + + assertEquals(Arrays.asList(1, 2, 3, 4), myConsumer.data); + + spliterator.forEachRemaining(myConsumer); + assertEquals(8, myConsumer.data.size()); + assertEquals(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8), myConsumer.data); + } + + @Test + public void subListForEach() { + list.addAll(Arrays.asList(4, 2, 1, 3, 1, 2, 5, 8)); + + class MyConsumer implements Consumer { + + final List data = new ArrayList<>(); + + @Override + public void accept(Integer t) { + data.add(t); + } + } + + MyConsumer myConsumer = new MyConsumer(); + list.subList(1, 6).forEach(myConsumer); + + assertEquals(Integer.valueOf(2), myConsumer.data.get(0)); + assertEquals(Integer.valueOf(1), myConsumer.data.get(1)); + assertEquals(Integer.valueOf(3), myConsumer.data.get(2)); + assertEquals(Integer.valueOf(1), myConsumer.data.get(3)); + assertEquals(Integer.valueOf(2), myConsumer.data.get(4)); + } + + @Test + public void subListGet() { + list.addAll(Arrays.asList(4, 2, 8, 0, 9)); + List subList = list.subList(1, 4); + assertEquals(Integer.valueOf(2), subList.get(0)); + assertEquals(Integer.valueOf(8), subList.get(1)); + assertEquals(Integer.valueOf(0), subList.get(2)); + } + + @Test + public void subListIndexOf() { + list.addAll(Arrays.asList(5, 1, 9, 10, 2, 3, 7, 6)); + List subList = list.subList(2, 6); // <9, 10, 2, 3> + + assertEquals(-1, subList.indexOf(5)); + assertEquals(-1, subList.indexOf(1)); + assertEquals(-1, subList.indexOf(7)); + assertEquals(-1, subList.indexOf(6)); + + assertEquals(0, subList.indexOf(9)); + assertEquals(1, subList.indexOf(10)); + assertEquals(2, subList.indexOf(2)); + assertEquals(3, subList.indexOf(3)); + } + + @Test + public void subListIsEmpty() { + List subList = list.subList(0, 0); + assertTrue(subList.isEmpty()); + list.checkInvarant(); + subList.add(1); + list.checkInvarant(); + assertFalse(subList.isEmpty()); + list.checkInvarant(); + subList.remove(0); + list.checkInvarant(); + assertTrue(subList.isEmpty()); + } + + @Test + public void subListIterator() { + list.addAll(Arrays.asList(1, 2, 3, 4, 5, 6)); + List subList = list.subList(1, 5); // <2, 3, 4, 5> + Iterator iterator = subList.iterator(); + + assertTrue(iterator.hasNext()); + assertEquals(Integer.valueOf(2), iterator.next()); + + list.checkInvarant(); + iterator.remove(); + list.checkInvarant(); + + assertTrue(iterator.hasNext()); + assertEquals(Integer.valueOf(3), iterator.next()); + + assertTrue(iterator.hasNext()); + assertEquals(Integer.valueOf(4), iterator.next()); + + assertTrue(iterator.hasNext()); + assertEquals(Integer.valueOf(5), iterator.next()); + + list.checkInvarant(); + iterator.remove(); + list.checkInvarant(); + + assertFalse(iterator.hasNext()); + + assertEquals(Arrays.asList(3, 4), subList); + assertEquals(Arrays.asList(1, 3, 4, 6), list); + } + + @Test + public void subListLastIndex() { + list.addAll(Arrays.asList(1, 2, 3, 2, 3, 2, 1)); + List subList = list.subList(2, 7); // <3, 2, 3, 2, 1> + + assertEquals(4, subList.lastIndexOf(1)); + assertEquals(3, subList.lastIndexOf(2)); + assertEquals(2, subList.lastIndexOf(3)); + assertEquals(-1, subList.lastIndexOf(10)); + } + + @Test + public void subListListIterator() { + list.addAll(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9)); + List subList = list.subList(2, 8); // <3, 4, 5, 6, 7, 8> + + assertEquals(Arrays.asList(3, 4, 5, 6, 7, 8), subList); + + ListIterator iterator = subList.listIterator(2); + + assertTrue(iterator.hasNext()); + assertTrue(iterator.hasPrevious()); + + assertEquals(Integer.valueOf(5), iterator.next()); + + list.checkInvarant(); + iterator.remove(); // subList = <3, 4, 6, 7, 8> + list.checkInvarant(); + + assertEquals(Arrays.asList(1, 2, 3, 4, 6, 7, 8, 9), list); + assertEquals(Arrays.asList(3, 4, 6, 7, 8), subList); + + assertEquals(Integer.valueOf(4), iterator.previous()); + + list.checkInvarant(); + iterator.remove(); // subList = <3, 6, 7, 8> + list.checkInvarant();// [1, 2, 3, 6, 7, 8, 9] + + assertEquals(Integer.valueOf(6), iterator.next()); + assertEquals(Integer.valueOf(7), iterator.next()); + assertEquals(Integer.valueOf(8), iterator.next()); + + assertFalse(iterator.hasNext()); + assertTrue(iterator.hasPrevious()); + } + + @Test + public void debugRemoveAtFigner() { + list.addAll(Arrays.asList(1, 2, 3, 4, 6, 7, 8, 9)); + + } + + @Test + public void subListRemoveObject() { + list.addAll(Arrays.asList(3, 1, 2, 4, null, 5, null, 8, 1)); + List subList = list.subList(2, 8); + // subList = <2, 4, null, 5, null, 8> + + list.checkInvarant(); + subList.remove(null); + list.checkInvarant(); + + assertEquals(Arrays.asList(2, 4, 5, null, 8), subList); + assertEquals(Arrays.asList(3, 1, 2, 4, 5, null, 8, 1), list); + + list.checkInvarant(); + subList.remove(Integer.valueOf(5)); + list.checkInvarant(); + + assertEquals(Arrays.asList(2, 4, null, 8), subList); + assertEquals(Arrays.asList(3, 1, 2, 4, null, 8, 1), list); + + list.checkInvarant(); + subList.remove(null); + list.checkInvarant(); + + assertEquals(Arrays.asList(2, 4, 8), subList); + assertEquals(Arrays.asList(3, 1, 2, 4, 8, 1), list); + } + + @Test + public void subListRemoveInt() { + list.addAll(Arrays.asList(1, 2, 3, 4, 5, 6, 7)); + List subList = list.subList(3, 5); + + assertEquals(Arrays.asList(4, 5), subList); + + list.checkInvarant(); + subList.remove(1); + list.checkInvarant(); + + assertEquals(Arrays.asList(4), subList); + assertEquals(Arrays.asList(1, 2, 3, 4, 6, 7), list); + + list.checkInvarant(); + subList.remove(0); + list.checkInvarant(); + + assertEquals(Arrays.asList(), subList); + assertEquals(Arrays.asList(1, 2, 3, 6, 7), list); + } + + @Test + public void subListRemoveAll() { + list.addAll(Arrays.asList(4, 1, 2, 9, 8, 7, 5, 2, 8, 10, 11)); + List subList = list.subList(1, 9); + // subList = <1, 2, 9, 8, 7, 5, 2, 8> + + subList.removeAll(Arrays.asList(1)); + // subList = <2, 9, 8, 7, 5, 2, 8> + list.checkInvarant(); + + assertEquals(Arrays.asList(2, 9, 8, 7, 5, 2, 8), subList); + + subList.removeAll(Arrays.asList(2)); + // subList = <9, 8, 7, 5, 8> + list.checkInvarant(); + + assertEquals(Arrays.asList(9, 8, 7, 5, 8), subList); + + subList.removeAll(Arrays.asList(8, 7)); + // subList = <9, 5> + list.checkInvarant(); + + assertEquals(Arrays.asList(9, 5), subList); + + subList.removeAll(Arrays.asList(9, 5)); // subList = <> + list.checkInvarant(); + + assertEquals(Arrays.asList(), subList); + assertTrue(subList.isEmpty()); + assertFalse(list.isEmpty()); + } + + @Test + public void subListRemoveIf() { + list.addAll(Arrays.asList(1, 5, 2, 3, 4, 8, 9, 10, 4)); + List subList = list.subList(2, 7); + // subList = <2, 3, 4, 8, 9> + + subList.removeIf((i) -> { + return i % 2 == 1; // Remove odd integers. + }); + + list.checkInvarant(); + + // subList = <2, 4, 8> + assertEquals(Arrays.asList(2, 4, 8), subList); + assertEquals(Arrays.asList(1, 5, 2, 4, 8, 10, 4), list); + } + + @Test + public void subListReplaceAll() { + list.addAll(Arrays.asList(4, 4, 5, 1, 8, 2, 9, 0, 1, 3)); + List subList = list.subList(2, 8); + // subList = <5, 1, 8, 2, 9, 0> + subList.replaceAll((i) -> { return i * 2; }); + list.checkInvarant(); + + assertEquals(Arrays.asList(10, 2, 16, 4, 18, 0), subList); + assertEquals(Arrays.asList(4, 4, 10, 2, 16, 4, 18, 0, 1, 3), list); + } + + @Test + public void subListRetainAll() { + list.addAll(Arrays.asList(3, 10, 8, 2, 5, 4, 1, 0, 7, 4)); + List subList = list.subList(2, 8); + // subList = <8, 2, 5, 4, 1, 0> + subList.retainAll(Arrays.asList(4, 2, 5, 11)); + list.checkInvarant(); + + // subList = <2, 5, 4> + assertEquals(Arrays.asList(2, 5, 4), subList); + assertEquals(Arrays.asList(3, 10, 2, 5, 4, 7, 4), list); + + list.checkInvarant(); + list.clear(); + list.checkInvarant(); + + list.addAll(Arrays.asList(1, 3, 2, 1, 2, 3, 3, 1, 2, 0, 0)); + subList = list.subList(1, 6); + // subList = <3, 2, 1, 2, 3> + + list.checkInvarant(); + subList.retainAll(Arrays.asList(3, 4, 5)); + list.checkInvarant(); + // subList = <3, 3> + assertEquals(Arrays.asList(3, 3), subList); + assertEquals(Arrays.asList(1, 3, 3, 3, 1, 2, 0, 0), list); + } + + @Test + public void subListSet() { + list.addAll(Arrays.asList(1, 2, 3, 4, 5)); + List subList = list.subList(1, 4); + // subList = <2, 3, 4> + list.checkInvarant(); + subList.set(0, 10); + list.checkInvarant(); + subList.set(2, 11); + list.checkInvarant(); + + assertEquals(Arrays.asList(10, 3, 11), subList); + assertEquals(Arrays.asList(1, 10, 3, 11, 5), list); + } + + @Test + public void subListSort() { + Random random = new Random(1001L); + + for (int i = 0; i < 100; ++i) { + int value = random.nextInt(100) % 75; + list.add(value); + } + + Collections.shuffle(list); + List referenceList = new ArrayList<>(list); + + list.subList(10, 80).sort(Integer::compare); + list.checkInvarant(); + + referenceList.subList(10, 80).sort(Integer::compare); + + assertEquals(referenceList, list); + } + + @Test + public void subListToArray() { + list.addAll(getIntegerList(15)); + List subList = list.subList(5, 10); + + Object[] array = subList.toArray(); + + for (int i = 0; i < subList.size(); ++i) { + Integer listInteger = list.get(i + 5); + Integer arrayInteger = (Integer) array[i]; + assertEquals(listInteger, arrayInteger); + } + } + + @Test + public void subListToArrayGeneric() { + list.addAll(getIntegerList(15)); + List subList = list.subList(5, 10); + + Integer[] array = new Integer[5]; + Integer[] resultArray = subList.toArray(array); + + assertTrue(array == resultArray); + + array = new Integer[7]; + resultArray = subList.toArray(array); + + assertTrue(array == resultArray); + + assertNull(resultArray[5]); + + array = new Integer[3]; + resultArray = subList.toArray(array); + + assertFalse(array == resultArray); + assertEquals(5, resultArray.length); + } + + @Test + public void clone2() { + list.addAll(Arrays.asList(4, 1, 3, 2)); + assertEquals(Arrays.asList(4, 1, 3, 2), list.clone()); + } + + @Test + public void subListClone() { + list.addAll(Arrays.asList(4, 1, 8, 9, 5, 6, 7, 0, 1)); + List subList1 = list.subList(1, list.size() - 1); + + IndexedLinkedList.EnhancedSubList subList2 = + (IndexedLinkedList.EnhancedSubList) + subList1.subList(1, subList1.size() - 1); + + List clone = (List) subList2.clone(); + assertEquals(Arrays.asList(8, 9, 5, 6, 7), clone); + } + + @Test + public void debugContractFingerArrayIfNeeded() { + list.addAll(getIntegerList(15_877)); // 128 finger spots occupied. + assertEquals(127, list.getFingerListSize()); + list.subList(49, list.size()).clear(); // 8 finger spots occupied. + assertEquals(7, list.getFingerListSize()); + } + + @Test + public void bruteForceSublistClearOnLargeLists() { + Random random = new Random(26L); + + for (int i = 0; i < 30; ++i) { + int size = 1 + random.nextInt(5_000); + List referenceList = new ArrayList<>(getIntegerList(size)); + list.clear(); + list.addAll(referenceList); + + int f = random.nextInt(size); + int t = random.nextInt(size); + int fromIndex = Math.min(f, t); + int toIndex = Math.max(f, t); + + list.checkInvarant(); + list.subList(fromIndex, toIndex).clear(); + list.checkInvarant(); + referenceList.subList(fromIndex, toIndex).clear(); + assertEquals(referenceList, list); + } + } + + @Test + public void listEqualsThrowsOnBadIterator() { + DummyList dummyList = new DummyList(); + list.addAll(Arrays.asList(0, 0)); + + assertThrows(IllegalStateException.class, + () -> listsEqual(list, dummyList)); + } + + @Test + public void offer() { + assertTrue(list.equals(Arrays.asList())); + + list.checkInvarant(); + list.offer(1); + list.checkInvarant(); + + assertTrue(list.equals(Arrays.asList(1))); + + list.offer(2); + list.checkInvarant(); + + assertTrue(list.equals(Arrays.asList(1, 2))); + } + + @Test + public void offerFirst() { + assertTrue(list.equals(Arrays.asList())); + + list.checkInvarant(); + list.offerFirst(1); + list.checkInvarant(); + + assertTrue(list.equals(Arrays.asList(1))); + + list.offerFirst(2); + list.checkInvarant(); + + assertTrue(list.equals(Arrays.asList(2, 1))); + } + + @Test + public void offerLast() { + assertTrue(list.equals(Arrays.asList())); + + list.checkInvarant(); + list.offerLast(1); + list.checkInvarant(); + + assertTrue(list.equals(Arrays.asList(1))); + + list.offerLast(2); + list.checkInvarant(); + + assertTrue(list.equals(Arrays.asList(1, 2))); + } + + @Test + public void peek() { + assertNull(list.peek()); + + list.addLast(0); + list.checkInvarant(); + + assertEquals(Integer.valueOf(0), list.peek()); + + list.addLast(1); + list.checkInvarant(); + + assertEquals(Integer.valueOf(0), list.peek()); + + list.addFirst(-1); + list.checkInvarant(); + + assertEquals(Integer.valueOf(-1), list.peek()); + } + + @Test + public void sortEmpty() { + list.sort((c1,c2) -> { return -1; }); + } + + @Test + public void sublistRangeCheck1() { + list.add(1); + assertThrows(IndexOutOfBoundsException.class, + () -> list.subListRangeCheck(-1, 2, 5)); + } + + @Test + public void sublistRangeCheck2() { + list.add(1); + assertThrows(IndexOutOfBoundsException.class, + () -> list.subListRangeCheck(0, 3, 2)); + } + + @Test + public void sublistRangeCheck3() { + list.add(1); + assertThrows(IllegalArgumentException.class, + () -> list.subListRangeCheck(1, 0, 1)); + } + + @Test + public void emptyBatchRemove() { + list.batchRemove(new ArrayList<>(), true, 0, 1); + } + + @Test + public void checkForComodification() { + list.modCount = 123; + assertThrows(ConcurrentModificationException.class, + () -> list.checkForComodification(122)); + } + + @Test + public void distributeFingers() { + list.addAll(getIntegerList(10)); + list.distributeFingers(2, 2); + list.checkInvarant(); + } + + @Test + public void iteratorNextThrowsOnNoElements() { + Iterator iterator = + new IndexedLinkedList<>(Arrays.asList(2, 3)).iterator(); + + try { + iterator.next(); + iterator.next(); + } catch (NoSuchElementException ex) { + fail("Should not throw here."); + } + + assertThrows(NoSuchElementException.class, () -> iterator.next()); + } + + @Test + public void iteratorNextThrowsOnRemove() { + Iterator iterator = + new IndexedLinkedList<>(Arrays.asList(2, 3)).iterator(); + + try { + iterator.next(); + iterator.next(); + iterator.remove(); + } catch (IllegalStateException ex) { + fail("Should not throw here."); + } + + assertThrows(IllegalStateException.class, () -> iterator.remove()); + } + + @Test + public void checkIteratorComodification() { + BasicIterator iterator = (BasicIterator) + new IndexedLinkedList<>(Arrays.asList(2, 3)).iterator(); + + iterator.expectedModCount = 1000; + assertThrows(ConcurrentModificationException.class, + () -> iterator.checkForComodification()); + } + + @Test + public void removeEntryRange() { + list.addAll(Arrays.asList(1, 2, 3)); + list.removeRange(0, 3); + } + + @Test + public void addAllEmpty() { + list.addAll(0, new ArrayList<>()); + } + + @Test + public void listIteratorNext() { + ListIterator iterator = + new IndexedLinkedList<>(Arrays.asList(1,2,3)).listIterator(1); + + try { + iterator.next(); + iterator.next(); + } catch (NoSuchElementException ex) { + fail("Should not throw here."); + } + + assertThrows(NoSuchElementException.class, () -> iterator.next()); + } + + @Test + public void listIteratorPrev() { + ListIterator iterator = + new IndexedLinkedList<>(Arrays.asList(1,2,3)).listIterator(1); + + try { + iterator.previous(); + } catch (NoSuchElementException ex) { + fail("Should not throw here."); + } + + assertThrows(NoSuchElementException.class, () -> iterator.previous()); + } + + @Test + public void forRemainingOnEmptyList() { + new IndexedLinkedList<>().iterator().forEachRemaining((a) -> {}); + } + + @Test + public void checkFromToOnNegativeFromIndex() { + assertThrows(IndexOutOfBoundsException.class, + () -> list.checkFromTo(-1, 2)); + } + + @Test + public void checkFromToOnTooLargeToIndex() { + list.add(1); + list.add(3); + + assertThrows(IndexOutOfBoundsException.class, + () -> list.checkFromTo(0, 3)); + } + + @Test + public void checkFromToOnBackwardIndices() { + list.add(1); + list.add(3); + assertThrows(IllegalArgumentException.class, + () -> list.checkFromTo(2, 1)); + } + + @Test + public void checkIndexOnNegativeSize() { + assertThrows(IllegalArgumentException.class, + () -> checkIndex(0, -1)); + } + + @Test + public void checkIndexOnNegativeIndex() { + assertThrows(IndexOutOfBoundsException.class, + () -> checkIndex(-1, 1)); + } + + @Test + public void checkIndexOnTooLargeIndex() { + assertThrows(IndexOutOfBoundsException.class, + () -> checkIndex(2, 2)); + } + + @Test + public void equalsRangeShortArgList() { + list.addAll(Arrays.asList(1, 2, 3, 4)); + referenceList.addAll(Arrays.asList(1, 2, 3)); + assertFalse(list.equals(referenceList)); + } + + @Test + public void noHasNext() { + assertThrows(NoSuchElementException.class, + () -> list.iterator().next()); + } + + @Test + public void noHasNextDescending() { + assertThrows(NoSuchElementException.class, + () -> list.descendingIterator().next()); + } + + @Test + public void throwsOnDoubleRemove() { + list.add(10); + Iterator it = list.iterator(); + it.next(); + it.remove(); + assertThrows(IllegalStateException.class, + () -> it.remove()); + } + + @Test + public void throwsOnDoubleRemoveDescending() { + list.add(10); + Iterator it = list.descendingIterator(); + it.next(); + it.remove(); + assertThrows(IllegalStateException.class, + () -> it.remove()); + } + + @Test + public void throwOnConcurrency() { + list.addAll(Arrays.asList(1, 2, 3)); + Iterator it = list.iterator(); + + it.next(); + list.add(13); + assertThrows(ConcurrentModificationException.class, + () -> it.next()); + } + + @Test + public void throwOnConcurrencyDescending() { + list.addAll(Arrays.asList(1, 2, 3)); + Iterator it = list.descendingIterator(); + + it.next(); + list.add(13); + assertThrows(ConcurrentModificationException.class, + () -> it.next()); + } + + @Test + public void equalsHaltsOnShorterArgList() { + list.addAll(Arrays.asList(1, 2, 3)); + referenceList.addAll(Arrays.asList(1, 2)); + assertFalse(list.equals(referenceList)); + } + + @Test + public void peekFirst() { + assertNull(list.peekFirst()); + + list.checkInvarant(); + list.addLast(0); + list.checkInvarant(); + + assertEquals(Integer.valueOf(0), list.peekFirst()); + + list.checkInvarant(); + list.addFirst(1); + list.checkInvarant(); + + assertEquals(Integer.valueOf(1), list.peekFirst()); + + list.addFirst(-1); + + assertEquals(Integer.valueOf(-1), list.peekFirst()); + } + + @Test + public void peekLast() { + assertNull(list.peekLast()); + + list.addLast(0); + list.checkInvarant(); + + assertEquals(Integer.valueOf(0), list.peekLast()); + + list.checkInvarant(); + list.addLast(1); + list.checkInvarant(); + + assertEquals(Integer.valueOf(1), list.peekLast()); + + list.checkInvarant(); + list.addLast(2); + list.checkInvarant(); + + assertEquals(Integer.valueOf(2), list.peekLast()); + } + + @Test + public void poll() { + assertNull(list.poll()); + + list.addAll(Arrays.asList(1, 2, 3)); + list.checkInvarant(); + + assertEquals(Integer.valueOf(1), list.poll()); + list.checkInvarant(); + assertEquals(Integer.valueOf(2), list.poll()); + list.checkInvarant(); + assertEquals(Integer.valueOf(3), list.poll()); + list.checkInvarant(); + } + + @Test + public void pollFirst() { + assertNull(list.pollFirst()); + + list.addAll(Arrays.asList(1, 2, 3)); + + list.checkInvarant(); + assertEquals(Integer.valueOf(1), list.pollFirst()); + list.checkInvarant(); + assertEquals(Integer.valueOf(2), list.pollFirst()); + list.checkInvarant(); + assertEquals(Integer.valueOf(3), list.pollFirst()); + list.checkInvarant(); + } + + @Test + public void pollLast() { + assertNull(list.pollLast()); + + list.addAll(Arrays.asList(1, 2, 3)); + + list.checkInvarant(); + assertEquals(Integer.valueOf(3), list.pollLast()); + list.checkInvarant(); + assertEquals(Integer.valueOf(2), list.pollLast()); + list.checkInvarant(); + assertEquals(Integer.valueOf(1), list.pollLast()); + list.checkInvarant(); + } + + @Test + public void removeFirstThrowsOnEmptyList() { + assertThrows(NoSuchElementException.class, + () -> list.removeFirst()); + } + + @Test + public void pop() { + list.addAll(Arrays.asList(1, 2, 3)); + + list.checkInvarant(); + assertEquals(Integer.valueOf(1), list.pop()); + list.checkInvarant(); + assertEquals(Integer.valueOf(2), list.pop()); + list.checkInvarant(); + assertEquals(Integer.valueOf(3), list.pop()); + list.checkInvarant(); + } + + @Test + public void push() { + list.push(1); + list.checkInvarant(); + list.push(2); + list.checkInvarant(); + list.push(3); + list.checkInvarant(); + + assertTrue(list.equals(Arrays.asList(3, 2, 1))); + } + + class BadList extends IndexedLinkedList { + + class BadListIterator implements Iterator { + + @Override + public boolean hasNext() { + return true; + } + + @Override + public Integer next() { + return 3; + } + } + + @Override + public Iterator iterator() { + return new BadListIterator(); + }; + + public int size() { + return 2; + } + } + + @Test + public void badThisIterator() { + list.addAll(Arrays.asList(3, 2)); + BadList badList = new BadList(); + badList.addAll(Arrays.asList(3, 3)); + Assert.assertNotEquals(badList, list); + } + + @Test + public void removeFirstOccurrenceOfNull() { + list.addAll(Arrays.asList(1, 2, null, 4, null, 6)); + + assertTrue(list.removeFirstOccurrence(null)); + list.checkInvarant(); + // Remove the last null value: + list.set(3, 10); + + assertFalse(list.removeFirstOccurrence(null)); + list.checkInvarant(); + } + + @Test + public void removeLastOccurrenceOfNull() { + list.addAll(Arrays.asList(1, 2, null, 4, null, 6)); + + assertTrue(list.removeLastOccurrence(null)); + list.checkInvarant(); + + // Remove the last null value: + list.set(2, 10); + + assertFalse(list.removeLastOccurrence(null)); + list.checkInvarant(); + } + + @Test + public void appendAll() { + list.addAll(Arrays.asList(0, 1, 2)); + + List arrayList = new ArrayList<>(); + + for (int i = 3; i < 20_000; i++) { + arrayList.add(i); + } + + list.addAll(arrayList); + list.checkInvarant(); + + for (int i = 0; i < 20_000; i++) { + assertEquals(Integer.valueOf(i), list.get(i)); + } + } + + @Test + public void smallListRemoveFirstFinger() { + list.add(0); + list.checkInvarant(); + list.add(1); + list.checkInvarant(); + list.remove(0); + list.checkInvarant(); + } + + @Test + public void smallListRemoveSecondFinger() { + list.checkInvarant(); + list.add(0); + list.checkInvarant(); + list.add(1); + list.checkInvarant(); + list.remove(1); + list.checkInvarant(); + } + + @Test + public void prependAll() { + List l = new ArrayList<>(); + + for (int i = 0; i < 10_000; i++) { + l.add(i); + } + + list.addAll(l); + list.checkInvarant(); + + l = new ArrayList<>(); + + for (int i = 10_000; i < 20_000; i++) { + l.add(i); + } + + list.addAll(0, l); + list.checkInvarant(); + + int index = 0; + + for (int i = 10_000; i < 20_000; i++) { + assertEquals(Integer.valueOf(i), list.get(index++)); + } + + for (int i = 0; i < 10_000; i++) { + assertEquals(Integer.valueOf(i), list.get(index++)); + } + } + + @Test + public void insertAll() { + for (int i = 0; i < 20_000; i++) { + list.add(i); + } + + List arrayList = new ArrayList<>(10_000); + + for (int i = 20_000; i < 30_000; i++) { + arrayList.add(i); + } + + list.addAll(10_000, arrayList); + + int index = 0; + + for (int i = 0; i < 10_000; i++) { + assertEquals(Integer.valueOf(i), list.get(index++)); + } + + for (int i = 20_000; i < 30_000; i++) { + assertEquals(Integer.valueOf(i), list.get(index++)); + } + + for (int i = 10_000; i < 20_000; i++) { + assertEquals(Integer.valueOf(i), list.get(index++)); + } + } + + @Test + public void checkPositionIndexThrowsOnNegativeIndex() { + assertThrows(IndexOutOfBoundsException.class, + () -> list.add(-1, Integer.valueOf(0))); + } + + @Test + public void checkPositionIndxThrowsOnTooLargeIndex() { + list.add(0); + assertThrows(IndexOutOfBoundsException.class, + () -> list.add(2, 1)); + } + + @Test + public void removeLastThrowsOnEmptyList() { + assertThrows(NoSuchElementException.class, + () -> list.removeLast()); + } + + @Test + public void getFirstThrowsOnEmptyList() { + assertThrows(NoSuchElementException.class, + () -> list.getFirst()); + } + + @Test + public void getFirst() { + list.addAll(Arrays.asList(10, 20)); + assertEquals(Integer.valueOf(10), list.getFirst()); + + list.checkInvarant(); + list.removeFirst(); + list.checkInvarant(); + + assertEquals(Integer.valueOf(20), list.getFirst()); + } + + @Test + public void basicIteratorNextThrowsOnNoNext() { + list.add(1); + + Iterator iter = list.iterator(); + + try { + iter.next(); + } catch (Exception ex) { + fail("Should not get here."); + } + + assertThrows(NoSuchElementException.class, () -> iter.next()); + } + + @Test + public void basicIteratorThrowsOnDoubleRemove() { + list.add(1); + + Iterator iter = list.iterator(); + + try { + iter.next(); + iter.remove(); + } catch (Exception ex) { + fail("Should not get here."); + } + + assertThrows(IllegalStateException.class, () -> iter.remove()); + } + + @Test + public void basicIteratorRemove1() { + list.add(1); + list.add(2); + + Iterator iter = list.iterator(); + + iter.next(); + list.checkInvarant(); + iter.remove(); + list.checkInvarant(); + iter.next(); + iter.remove(); + list.checkInvarant(); + + assertEquals(0, list.size()); + assertFalse(iter.hasNext()); + } + + @Test + public void basicIteratorRemove2() { + list.add(1); + list.add(2); + list.add(3); + + Iterator iter = list.iterator(); + + iter.next(); + list.checkInvarant(); + iter.remove(); + list.checkInvarant(); + iter.next(); + iter.next(); + } + + @Test + public void enhancedIteratorThrowsOnSetAfterRemove() { + list.addAll(Arrays.asList(1, 2, 3, 4)); + + ListIterator iter = list.listIterator(1); + + iter.next(); + list.checkInvarant(); + iter.remove(); + list.checkInvarant(); + assertThrows(IllegalStateException.class, + () -> iter.set(10)); + } + + @Test + public void basicIteratorForEachRemainingThrowsOnConcurrentModification() { + list.addAll(getIntegerList(1_000_000)); + + BasicIterator iter = (BasicIterator) list.iterator(); + iter.expectedModCount = -1000; + + assertThrows(ConcurrentModificationException.class, + () -> iter.forEachRemaining((e) -> {})); + } + + @Test + public void addEmptyCollection() { + list.subList(0, 0).addAll(0, Collections.emptyList()); + } + + @Test + public void onEmptyCollectionSort() { + list.subList(0, 0).sort(null); + } + + @Test + public void sublistRemoveNullNotMatched() { + list.addAll(Arrays.asList(1, 2, 3)); + assertFalse(list.subList(0, 3).remove(null)); + } + + @Test + public void basicDescendingIteratorForEachRemainingThrowsOnConcurrentModification() { + list.addAll(getIntegerList(1_000_000)); + + DescendingIterator iter = (DescendingIterator) list.descendingIterator(); + iter.expectedModCount = -1000; + assertThrows(ConcurrentModificationException.class, + () -> iter.forEachRemaining((e) -> {})); + } + + @Test + public void + enhancedIteratorForEachRemainingThrowsOnConcurrentModification() { + + list.addAll(getIntegerList(1_000_000)); + + EnhancedIterator iter = (EnhancedIterator) list.listIterator(); + iter.expectedModCount = -1; + assertThrows(ConcurrentModificationException.class, + () -> iter.forEachRemaining((e) -> {})); + } + + @Test + public void spliteratorThrowsOnConcurrentModification() { + list.addAll(getIntegerList(50_000)); + + Spliterator spliterator = list.spliterator(); + list.add(50_000); + + assertThrows(ConcurrentModificationException.class, + () -> spliterator.tryAdvance((e) -> {})); + } + + @Test + public void spliteratorTrySplitReturnsNullOnEmptyList() { + Spliterator spliterator = list.spliterator(); + + assertNull(spliterator.trySplit()); + } + + @Test + public void spliteratorTrySplitReturnsNullOnTooSmallList() { + list.addAll( + getIntegerList( + (int) + (IndexedLinkedList + .LinkedListSpliterator + .MINIMUM_BATCH_SIZE - 1L))); + + Spliterator spliterator = list.spliterator(); + + assertNull(spliterator.trySplit()); + } + + @Test + public void spliteratorHasCharasteristics() { + Spliterator spliterator = list.spliterator(); + + assertTrue(spliterator.hasCharacteristics(Spliterator.ORDERED)); + assertTrue(spliterator.hasCharacteristics(Spliterator.SIZED)); + assertTrue(spliterator.hasCharacteristics(Spliterator.SUBSIZED)); + + assertFalse(spliterator.hasCharacteristics(Spliterator.CONCURRENT)); + assertFalse(spliterator.hasCharacteristics(Spliterator.DISTINCT)); + assertFalse(spliterator.hasCharacteristics(Spliterator.IMMUTABLE)); + assertFalse(spliterator.hasCharacteristics(Spliterator.NONNULL)); + assertFalse(spliterator.hasCharacteristics(Spliterator.SORTED)); + } + + @Test + public void enhancedListIteratorForEachRemaining() { + list.addAll(Arrays.asList(0, 1, 2, 3, 4)); + + List storageList = new ArrayList<>(3); + ListIterator iter = list.listIterator(1); + + iter.next(); + + iter.forEachRemaining((e) -> { + storageList.add(e); + }); + + storageList.equals(Arrays.asList(2, 3, 4)); + } + + @Test + public void + spliteratorTryAdvanceThrowsNullPointerExceptionOnNullConsumer() { + assertThrows(NullPointerException.class, + () -> list.spliterator().tryAdvance(null)); + } + + @Test + public void + spliteratorForEachRemainingThrowsNullPointerExceptionOnNullConsumer() { + assertThrows(NullPointerException.class, + () -> list.spliterator().forEachRemaining(null)); + } + + @Test + public void + spliteratorThrowsConcurrentModificationExceptionOnConcurrentModification() { + list.addAll(Arrays.asList(1, 2, 3)); + + Spliterator spliterator = list.spliterator(); + + list.add(4); + assertThrows(ConcurrentModificationException.class, + () -> spliterator.forEachRemaining((e) -> {})); + } + + @Test + public void enhancedIteratorNextThrowsOnNoNext() { + list.addAll(getIntegerList(20)); + + ListIterator iter = list.listIterator(19); + + try { + iter.next(); + } catch (Exception ex) { + fail("Should not get here."); + } + + assertThrows(NoSuchElementException.class, + () -> iter.next()); + } + + @Test + public void enhancedIteratorPrevioiusThrowsOnNoPrevious() { + list.addAll(getIntegerList(20)); + + ListIterator iter = list.listIterator(1); + + try { + iter.previous(); + } catch (Exception ex) { + fail("Should not get here."); + } + + assertThrows(NoSuchElementException.class, + () -> iter.previous()); + } + + @Test + public void enhancedIteratorThrowsOnDoubleRemove() { + list.add(1); + + ListIterator iter = list.listIterator(); + + try { + iter.next(); + iter.remove(); + } catch (Exception ex) { + fail("Should not get here."); + } + + assertThrows(IllegalStateException.class, + () -> iter.remove()); + } + + public void getLastThrowsOnEmptyList() { + assertThrows(NoSuchElementException.class, () -> list.getLast()); + } + + @Test + public void getLast() { + list.addAll(Arrays.asList(10, 20)); + assertEquals(Integer.valueOf(20), list.getLast()); + + list.checkInvarant(); + list.removeLast(); + list.checkInvarant(); + + assertEquals(Integer.valueOf(10), list.getLast()); + } + + @Test + public void indexOfNull() { + list.addAll(Arrays.asList(1, 2, null, 3, null, 4)); + + assertEquals(2, list.indexOf(null)); + + list.set(2, 5); + list.set(4, 10); + + assertEquals(-1, list.indexOf(null)); + } + + @Test + public void lastIndexOfNull() { + list.addAll(Arrays.asList(1, 2, null, 3, null, 4)); + + assertEquals(4, list.lastIndexOf(null)); + + list.set(2, 5); + list.set(4, 10); + + assertEquals(-1, list.lastIndexOf(null)); + } + + @Test + public void add() { + assertTrue(list.isEmpty()); + assertEquals(0, list.size()); + + list.add(1); + list.checkInvarant(); + + assertEquals(1, list.size()); + assertFalse(list.isEmpty()); + + assertEquals(Integer.valueOf(1), list.get(0)); + + list.add(2); + list.checkInvarant(); + + assertEquals(2, list.size()); + assertFalse(list.isEmpty()); + + assertEquals(Integer.valueOf(1), list.get(0)); + assertEquals(Integer.valueOf(2), list.get(1)); + } + + @Test + public void addFirst() { + assertTrue(list.isEmpty()); + assertEquals(0, list.size()); + + list.addFirst(1); + list.checkInvarant(); + + assertEquals(1, list.size()); + assertFalse(list.isEmpty()); + + assertEquals(Integer.valueOf(1), list.get(0)); + + list.addFirst(2); + list.checkInvarant(); + + assertEquals(2, list.size()); + assertFalse(list.isEmpty()); + + assertEquals(Integer.valueOf(2), list.get(0)); + assertEquals(Integer.valueOf(1), list.get(1)); + } + + @Test + public void throwsOnAccessingEmptyList() { + assertThrows(IndexOutOfBoundsException.class, () -> list.get(0)); + } + + @Test + public void throwsOnNegativeIndexInEmptyList() { + assertThrows(IndexOutOfBoundsException.class, () -> list.get(-1)); + } + + @Test + public void throwsOnNegativeIndexInNonEmptyList() { + list.addFirst(10); + assertThrows(IndexOutOfBoundsException.class, () -> list.get(-1)); + } + + @Test + public void throwsOnTooLargeIndex() { + list.addFirst(10); + list.addLast(20); + assertThrows(IndexOutOfBoundsException.class, () -> list.get(2)); + } + + @Test + public void addIndexAndElement() { + list.add(0, 1); + list.checkInvarant(); + + assertEquals(Integer.valueOf(1), list.get(0)); + + list.add(0, 2); + list.checkInvarant(); + + assertEquals(Integer.valueOf(2), list.get(0)); + assertEquals(Integer.valueOf(1), list.get(1)); + + list.add(2, 10); + list.checkInvarant(); + + assertEquals(Integer.valueOf(2), list.get(0)); + assertEquals(Integer.valueOf(1), list.get(1)); + assertEquals(Integer.valueOf(10), list.get(2)); + + list.add(2, 100); + list.checkInvarant(); + + assertEquals(Integer.valueOf(2), list.get(0)); + assertEquals(Integer.valueOf(1), list.get(1)); + assertEquals(Integer.valueOf(100), list.get(2)); + assertEquals(Integer.valueOf(10), list.get(3)); + } + + @Test + public void addCollectionOneElementToEmptyList() { + List c = new ArrayList<>(); + c.add(100); + + list.addAll(c); + list.checkInvarant(); + + assertFalse(list.isEmpty()); + assertEquals(1, list.size()); + assertEquals(Integer.valueOf(100), list.get(0)); + } + + @Test + public void addCollectionThreeElementsToEmptyList() { + assertTrue(list.isEmpty()); + assertEquals(0, list.size()); + + List c = Arrays.asList(1, 2, 3); + + list.addAll(c); + assertFalse(list.isEmpty()); + assertEquals(3, list.size()); + + for (int i = 0; i < list.size(); i++) { + assertEquals(Integer.valueOf(i + 1), list.get(i)); + } + } + + @Test + public void addCollectionAtIndex() { + list.addAll(0, Arrays.asList(2, 3)); // setAll + list.checkInvarant(); + list.addAll(0, Arrays.asList(0, 1)); // prependAll + list.checkInvarant(); + list.addAll(4, Arrays.asList(6, 7)); // appendAll + list.checkInvarant(); + list.addAll(4, Arrays.asList(4, 5)); // insertAll + list.checkInvarant(); + + for (int i = 0; i < list.size(); i++) { + assertEquals(Integer.valueOf(i), list.get(i)); + } + } + + @Test + public void removeInt() { + list.addAll(Arrays.asList(0, 1, 2, 3, 4)); + + // [0, 1, 2, 3, 4] + assertEquals(Integer.valueOf(0), list.remove(0)); + list.checkInvarant(); + // [1, 2, 3, 4] + assertEquals(Integer.valueOf(4), list.remove(3)); + list.checkInvarant(); + // [1, 2, 3] + assertEquals(Integer.valueOf(2), list.remove(1)); + list.checkInvarant(); + // [1, 3] + assertEquals(Integer.valueOf(1), list.remove(0)); + list.checkInvarant(); + // [3] + assertEquals(Integer.valueOf(3), list.remove(0)); + list.checkInvarant(); + // [] + } + + @Test + public void basicIteratorUsage() { + for (int i = 0; i < 1000; i++) { + list.add(i); + } + + Iterator iterator = list.iterator(); + + for (int i = 0; i < 1000; i++) { + assertTrue(iterator.hasNext()); + assertEquals(Integer.valueOf(i), iterator.next()); + } + + assertFalse(iterator.hasNext()); + } + + @Test + public void removeFirstLast() { + list.addAll(getIntegerList(5)); + + List referenceList = new ArrayList<>(list); + + list.removeFirst(); + list.checkInvarant(); + referenceList.remove(0); + + assertTrue(listsEqual(list, referenceList)); + + list.removeFirst(); + list.checkInvarant(); + referenceList.remove(0); + + assertTrue(listsEqual(list, referenceList)); + + list.removeLast(); + list.checkInvarant(); + referenceList.remove(referenceList.size() - 1); + + assertTrue(listsEqual(list, referenceList)); + + list.removeLast(); + list.checkInvarant(); + referenceList.remove(referenceList.size() - 1); + + assertTrue(listsEqual(list, referenceList)); + } + + @Test + public void subListThrowsOnConcurrentModification() { + List l = + new IndexedLinkedList<>( + Arrays.asList(1, 2, 3, 4)); + + List subList1 = l.subList(1, 4); // <2, 3, 4> + List subList2 = subList1.subList(0, 2); // <2, 3> + + subList1.add(1, 10); + + assertThrows(ConcurrentModificationException.class, + () -> subList2.add(1, 11)); // Must throw here. + } + + @Test + public void removeFirstLastOccurrence() { + IndexedLinkedList l = new IndexedLinkedList<>(); + + list.addAll(Arrays.asList(1, 2, 3, 1, 2, 3)); // <1, 2, 3, 1, 2, 3> + list.checkInvarant(); + l.addAll(list); + + list.removeFirstOccurrence(2); // <1, 3, 1, 2, 3> + list.checkInvarant(); + l.removeFirstOccurrence(2); + + assertTrue(listsEqual(list, l)); + + list.removeLastOccurrence(3); // <1, 3, 1, 2> + list.checkInvarant(); + l.removeLastOccurrence(3); + + assertTrue(listsEqual(list, l)); + } + + @Test + public void bruteForceAddCollectionAtIndex() { + Random random = new Random(100L); + + list.addAll(getIntegerList()); + + referenceList.clear(); + referenceList.addAll(list); + + for (int op = 0; op < 100; op++) { + int index = random.nextInt(list.size()); + Collection coll = getIntegerList(random.nextInt(40)); + referenceList.addAll(index, coll); + list.addAll(index, coll); + list.checkInvarant(); + + assertTrue(listsEqual(list, referenceList)); + } + } + + @Test + public void removeAtIndex() { + list.addAll(Arrays.asList(0, 1, 2, 3, 4)); + + // [0, 1, 2, 3, 4] + assertEquals(Integer.valueOf(2), list.remove(2)); + list.checkInvarant(); + // [0, 1, 3, 4] + assertEquals(Integer.valueOf(0), list.remove(0)); + list.checkInvarant(); + // [1, 3, 4] + assertEquals(Integer.valueOf(4), list.remove(2)); + list.checkInvarant(); + // [1, 3] + assertEquals(Integer.valueOf(3), list.remove(1)); + list.checkInvarant(); + // [1] + assertEquals(Integer.valueOf(1), list.remove(0)); + list.checkInvarant(); + // [] + } + + @Test + public void removeObject() { + list.addAll(Arrays.asList(0, 1, 2, 3, 4)); + + assertFalse(list.remove(Integer.valueOf(10))); + list.checkInvarant(); + assertFalse(list.remove(null)); + list.checkInvarant(); + + list.add(3, null); + list.checkInvarant(); + + assertTrue(list.remove(null)); + list.checkInvarant(); + + assertTrue(list.remove(Integer.valueOf(4))); + list.checkInvarant(); + assertTrue(list.remove(Integer.valueOf(0))); + list.checkInvarant(); + assertTrue(list.remove(Integer.valueOf(2))); + list.checkInvarant(); + assertFalse(list.remove(Integer.valueOf(2))); + } + + @Test + public void spreadOnSet() { + list.addAll(getIntegerList(100)); + list.checkInvarant(); + } + + @Test + public void spreadOnAppend() { + list.addAll(getIntegerList(10)); + list.addAll(getIntegerList(90)); + list.checkInvarant(); + } + + @Test + public void spreadOnPrepend() { + list.addAll(getIntegerList(10)); + list.addAll(0, getIntegerList(90)); + list.checkInvarant(); + } + + @Test + public void spreadOnInsert() { + list.addAll(getIntegerList(20)); + list.addAll(10, getIntegerList(80)); + list.checkInvarant(); + } + + @Test + public void basicIteratorTraversal() { + list.addAll(Arrays.asList(0, 1, 2, 3, 4)); + + Iterator iter = list.iterator(); + + for (int i = 0; i < list.size(); i++) { + assertTrue(iter.hasNext()); + assertEquals(Integer.valueOf(i), iter.next()); + } + + iter = list.iterator(); + + class MyConsumer implements Consumer { + + int total; + + @Override + public void accept(Integer t) { + total += t; + } + } + + MyConsumer myConsumer = new MyConsumer(); + + list.iterator().forEachRemaining(myConsumer); + assertEquals(10, myConsumer.total); + } + + @Test + public void basicIteratorRemoval() { + list.addAll(Arrays.asList(0, 1, 2, 3, 4)); + Iterator iter = list.iterator(); + + iter.next(); + iter.next(); + iter.remove(); + list.checkInvarant(); + + assertEquals(4, list.size()); + + iter = list.iterator(); + iter.next(); + iter.remove(); + list.checkInvarant(); + + assertEquals(3, list.size()); + + assertEquals(Integer.valueOf(2), list.get(0)); + assertEquals(Integer.valueOf(3), list.get(1)); + assertEquals(Integer.valueOf(4), list.get(2)); + } + + @Test + public void enhancedIteratorTraversal() { + list.addAll(Arrays.asList(0, 1, 2, 3, 4)); + ListIterator iter = list.listIterator(); + + assertFalse(iter.hasPrevious()); + + for (int i = 0; i < list.size(); i++) { + assertTrue(iter.hasNext()); + assertEquals(Integer.valueOf(i), iter.next()); + } + + assertFalse(iter.hasNext()); + + for (int i = 4; i >= 0; i--) { + assertTrue(iter.hasPrevious()); + assertEquals(Integer.valueOf(i), iter.previous()); + } + + iter = list.listIterator(2); + + assertEquals(Integer.valueOf(2), iter.next()); + assertEquals(Integer.valueOf(2), iter.previous()); + + iter = list.listIterator(3); + + assertEquals(Integer.valueOf(3), iter.next()); + assertEquals(Integer.valueOf(4), iter.next()); + + assertFalse(iter.hasNext()); + assertTrue(iter.hasPrevious()); + } + + @Test + public void removeAt() { + list.addAll(getIntegerList(10)); + referenceList.clear(); + referenceList.addAll(list); + Random random = new Random(100L); + + while (!referenceList.isEmpty()) { + int removalIndex = random.nextInt(list.size()); + Integer referenceInteger = referenceList.remove(removalIndex); + Integer listInteger = list.remove(removalIndex); + assertEquals(referenceInteger, listInteger); + assertEquals(referenceList, list); + } + } + + // Used to find a failing removal sequence: + @Test + public void removeAtFindFailing() { + long seed = 101L; + Random random = new Random(seed); + int iteration = 0; + while (true) { + iteration++; + + list.clear(); + list.addAll(getIntegerList(10)); + + List indices = new ArrayList<>(); + + if (iteration == 100) { + return; + } + + while (!list.isEmpty()) { + int index = random.nextInt(list.size()); + indices.add(index); + + try { + list.remove(index); + } catch (NullPointerException ex) { + // Should not get here. Ever. + fail("Failing indices: " + indices); + return; + }catch (AssertionError ae) { + return; + } + } + } + } + + @Test + public void bugTinyRemoveInt() { + list.addAll(getIntegerList(5)); + + list.checkInvarant(); + list.remove(4); + list.checkInvarant(); + list.remove(0); + list.checkInvarant(); + list.remove(2); + list.checkInvarant(); + list.remove(0); + list.checkInvarant(); + list.remove(0); + list.checkInvarant(); + } + + @Test + public void removeAtIndex1() { + list.addAll(getIntegerList(10)); + // TODO: remove 'getIntegerList()'! + referenceList.clear(); + referenceList.addAll(list); + int[] indices = { 9, 3, 3, 3, 1, 0 }; + + for (int i = 0; i < indices.length; i++) { + assertEquals(referenceList, list); + + int index = indices[i]; + list.remove(index); + referenceList.remove((int) index); + } + + assertEquals(referenceList, list); + } + + @Test + public void enhancedIteratorAdditionToHead() { + List referenceList = new ArrayList<>(); + list.clear(); + + ListIterator referenceListIterator = + referenceList.listIterator(); + + ListIterator myListIterator = list.listIterator(); + + referenceListIterator.add(1); + list.checkInvarant(); + myListIterator.add(1); + list.checkInvarant(); + + assertEquals(referenceList, list); + + referenceListIterator.add(3); + list.checkInvarant(); + myListIterator.add(3); + list.checkInvarant(); + + assertEquals(referenceList, list); + + assertTrue(referenceListIterator.hasPrevious()); + assertTrue(myListIterator.hasPrevious()); + + assertEquals(Integer.valueOf(3), referenceListIterator.previous()); + assertEquals(Integer.valueOf(3), myListIterator.previous()); + + referenceListIterator.add(2); + list.checkInvarant(); + myListIterator.add(2); + list.checkInvarant(); + + assertEquals(referenceList, list); + + assertEquals(Integer.valueOf(2), referenceListIterator.previous()); + assertEquals(Integer.valueOf(2), myListIterator.previous()); + + assertEquals(Integer.valueOf(1), referenceListIterator.previous()); + assertEquals(Integer.valueOf(1), myListIterator.previous()); + + list.checkInvarant(); + myListIterator.add(0); + list.checkInvarant(); + referenceListIterator.add(0); + + assertEquals(referenceList, list); + + assertEquals(Integer.valueOf(1), referenceListIterator.next()); + assertEquals(Integer.valueOf(1), myListIterator.next()); + } + + @Test + public void enhancedIteratorAddition() { + list.addAll(Arrays.asList(1, 2, 3)); + ListIterator iter = list.listIterator(); + + iter.add(0); + list.checkInvarant(); + + while (iter.hasNext()) { + iter.next(); + } + + iter.add(4); + list.checkInvarant(); + iter = list.listIterator(); + + for (int i = 0; i < list.size(); i++) { + assertEquals(Integer.valueOf(i), iter.next()); + } + + iter = list.listIterator(2); + iter.add(10); + list.checkInvarant(); + + assertEquals(Integer.valueOf(10), list.get(2)); + } + + @Test + public void debugBasicIterator1() { + list.addAll(getIntegerList(10)); + list.fingerList.setFingerIndices(0, 1, 5, 7); + Iterator iterator = list.iterator(); + + int count = 0; + + while (iterator.hasNext()) { + + iterator.next(); + list.checkInvarant(); + iterator.remove(); + list.checkInvarant(); + } + } + + // TODO: DEBUG ME! + @Test + public void debugBasicIterator2() { + // Elements 1, 2, 3, 4 will be removed. + list.addAll(Arrays.asList(0, 1, 2, 3, 4)); + Iterator iterator = list.iterator(); + + assertTrue(iterator.hasNext()); + assertEquals(Integer.valueOf(0), iterator.next()); // Omit 0. + list.checkInvarant(); + + assertTrue(iterator.hasNext()); + assertEquals(Integer.valueOf(1), iterator.next()); // Omit 1. + list.checkInvarant(); + + // Remove 1: + iterator.remove(); + list.checkInvarant(); + + assertTrue(iterator.hasNext()); + assertEquals(Integer.valueOf(2), iterator.next()); + list.checkInvarant(); + + // Remove 2: + iterator.remove(); + list.checkInvarant(); + + assertTrue(iterator.hasNext()); + assertEquals(Integer.valueOf(3), iterator.next()); + list.checkInvarant(); + + // Remove 3: + iterator.remove(); + list.checkInvarant(); + + assertTrue(iterator.hasNext()); + assertEquals(Integer.valueOf(4), iterator.next()); + list.checkInvarant(); + + // Remove 4: + iterator.remove(); + list.checkInvarant(); + + assertFalse(iterator.hasNext()); + list.checkInvarant(); + } + + @Test + public void debugBasicIterator3() { + list.addAll(getIntegerList(12)); + list.fingerList.setFingerIndices(1, 2, 4, 5); + Iterator iterator = list.iterator(); + + int count = 0; + + while (iterator.hasNext()) { +// Sysem.out.println("cnt = " + ++count); + + iterator.next(); + list.checkInvarant(); + + if (count % 2 == 0) { + iterator.remove(); + list.checkInvarant(); + } + } + } + + @Test + public void findFailingIterator() { + list.addAll(getIntegerList(3850)); + Iterator iterator = list.iterator(); + int counter = 0; + + while (iterator.hasNext()) { + Integer actualInteger = iterator.next(); + assertEquals(Integer.valueOf(counter), actualInteger); + list.checkInvarant(); + + // Remove every 10th element: + if (counter % 10 == 0) { + iterator.remove(); + list.checkInvarant(); + } + + counter++; + } + + list.checkInvarant(); + } + + @Test + public void bruteForceIteratorRemove() throws Exception { + list.addAll(getIntegerList(1000)); + + int counter = 1; + List arrayList = new ArrayList<>(list); + Iterator iter = list.iterator(); + Iterator arrayListIter = arrayList.iterator(); + int totalIterations = 0; + + while (iter.hasNext()) { + iter.next(); + arrayListIter.next(); + + if (counter % 10 == 0) { + + try { + list.checkInvarant(); + iter.remove(); + list.checkInvarant(); + } catch (IllegalStateException ex) { + throw new Exception(ex); + } + + list.checkInvarant(); + arrayListIter.remove(); + list.checkInvarant(); + counter = 1; + } else { + counter++; + } + + if (!listsEqual(list, arrayList)) { + throw new IllegalStateException( + "totalIterations = " + totalIterations); + } + + totalIterations++; + } + } + + @Test + public void findFailingRemoveObject() { + LinkedList referenceList = new LinkedList<>(); + + list.addAll(getIntegerList(10)); + list.checkInvarant(); + referenceList.addAll(list); + + Integer probe = list.get(1); + + list.remove(probe); + list.checkInvarant(); + referenceList.remove(probe); + + Iterator iterator1 = list.iterator(); + Iterator iterator2 = referenceList.iterator(); + + Random random = new Random(100L); + + while (!list.isEmpty()) { + if (!iterator1.hasNext()) { + + if (iterator2.hasNext()) { + throw new IllegalStateException(); + } + + iterator1 = list.iterator(); + iterator2 = referenceList.iterator(); + continue; + } + + iterator1.next(); + iterator2.next(); + + if (random.nextBoolean()) { + list.checkInvarant(); + iterator1.remove(); + list.checkInvarant(); + iterator2.remove(); + assertTrue(listsEqual(list, referenceList)); + } + } + + assertTrue(listsEqual(list, referenceList)); + } + + @Test + public void iteratorAdd() { + list.addAll(getIntegerList(4)); + + ListIterator iterator = list.listIterator(1); + + assertEquals(1, iterator.nextIndex()); + assertEquals(0, iterator.previousIndex()); + + iterator.next(); + + assertEquals(2, iterator.nextIndex()); + assertEquals(1, iterator.previousIndex()); + + list.checkInvarant(); + iterator.add(100); + list.checkInvarant(); + + assertEquals(Integer.valueOf(0), list.get(0)); + assertEquals(Integer.valueOf(1), list.get(1)); + assertEquals(Integer.valueOf(100), list.get(2)); + assertEquals(Integer.valueOf(2), list.get(3)); + assertEquals(Integer.valueOf(3), list.get(4)); + } + + @Test + public void bruteForceIteratorTest() { + list.addAll(getIntegerList(100)); + referenceList.clear(); + referenceList.addAll(list); + + ListIterator iterator1 = list.listIterator(2); + ListIterator iterator2 = referenceList.listIterator(2); + Random random = new Random(300L); + + while (iterator1.hasNext()) { + if (!iterator2.hasNext()) { + fail("Iterator mismatch on hasNext()."); + } + + iterator1.next(); + iterator2.next(); + + int choice = random.nextInt(10); + + if (choice < 2) { + Integer integer = random.nextInt(100); + list.checkInvarant(); + iterator1.add(integer); + list.checkInvarant(); + iterator2.add(integer); + assertTrue(listsEqual(list, referenceList)); + } else if (choice == 2) { + list.checkInvarant(); + iterator1.remove(); + list.checkInvarant(); + iterator2.remove(); + assertTrue(listsEqual(list, referenceList)); + } else if (choice < 6) { + if (iterator1.hasPrevious()) { + iterator1.previous(); + } + + if (iterator2.hasPrevious()) { + iterator2.previous(); + } + } else { + if (iterator1.hasNext()) { + iterator1.next(); + } + + if (iterator2.hasNext()) { + iterator2.next(); + } + } + } + + if (iterator2.hasNext()) { + fail("Java List iterator has more to offer."); + } + } + + @Test + public void indexOf() { + list.add(1); + list.add(2); + list.add(3); + + list.add(3); + list.add(2); + list.add(1); + + assertEquals(0, list.indexOf(1)); + assertEquals(1, list.indexOf(2)); + assertEquals(2, list.indexOf(3)); + + assertEquals(3, list.lastIndexOf(3)); + assertEquals(4, list.lastIndexOf(2)); + assertEquals(5, list.lastIndexOf(1)); + } + + class MyIntegerConsumer implements Consumer { + + List ints = new ArrayList<>(); + + @Override + public void accept(Integer t) { + ints.add(t); + } + } + + @Test + @SuppressWarnings("empty-statement") + public void basicSpliteratorUsage() { + list.addAll(getIntegerList(10_000)); + + Spliterator spliterator1 = list.spliterator(); + Spliterator spliterator2 = spliterator1.trySplit(); + + assertEquals(5000, spliterator1.getExactSizeIfKnown()); + assertEquals(5000, spliterator2.getExactSizeIfKnown()); + + + assertTrue(spliterator2.tryAdvance( + i -> assertEquals(list.get(0), Integer.valueOf(0)))); + + assertTrue(spliterator2.tryAdvance( + i -> assertEquals(list.get(1), Integer.valueOf(1)))); + + assertTrue(spliterator2.tryAdvance( + i -> assertEquals(list.get(2), Integer.valueOf(2)))); + + + + assertTrue(spliterator1.tryAdvance( + i -> assertEquals(list.get(5000), Integer.valueOf(5000)))); + + assertTrue(spliterator1.tryAdvance( + i -> assertEquals(list.get(5001), Integer.valueOf(5001)))); + + assertTrue(spliterator1.tryAdvance( + i -> assertEquals(list.get(5002), Integer.valueOf(5002)))); + + Spliterator spliterator3 = spliterator2.trySplit(); + + assertEquals(4997, spliterator1.getExactSizeIfKnown()); + + assertTrue(spliterator3.tryAdvance( + i -> assertEquals(list.get(3), Integer.valueOf(3)))); + + assertTrue(spliterator3.tryAdvance( + i -> assertEquals(list.get(4), Integer.valueOf(4)))); + + assertTrue(spliterator3.tryAdvance( + i -> assertEquals(list.get(5), Integer.valueOf(5)))); + + MyIntegerConsumer consumer = new MyIntegerConsumer(); + + while (spliterator1.tryAdvance(consumer)); + + for (int i = 0; i < consumer.ints.size(); i++) { + Integer actualInteger = consumer.ints.get(i); + Integer expectedInteger = 5003 + i; + assertEquals(expectedInteger, actualInteger); + } + } + + @Test + public void spliteratorForEachRemaining() { + list.addAll(getIntegerList(10_000)); + Spliterator split = list.spliterator(); + MyIntegerConsumer consumer = new MyIntegerConsumer(); + + split.forEachRemaining(consumer); + + for (int i = 0; i < 10_000; i++) { + assertEquals(Integer.valueOf(i), consumer.ints.get(i)); + } + } + + @Test + public void spliteratorForEachRemainingTwoSpliterators() { + list.addAll(getIntegerList(10_000)); + Spliterator splitRight = list.spliterator(); + Spliterator splitLeft = splitRight.trySplit(); + + MyIntegerConsumer consumerRight = new MyIntegerConsumer(); + MyIntegerConsumer consumerLeft = new MyIntegerConsumer(); + + splitRight.forEachRemaining(consumerRight); + splitLeft.forEachRemaining(consumerLeft); + + for (int i = 0; i < 5_000; i++) { + assertEquals(Integer.valueOf(i), consumerLeft.ints.get(i)); + } + + for (int i = 5_000; i < 10_000; i++) { + assertEquals(Integer.valueOf(i), consumerRight.ints.get(i - 5_000)); + } + } + + @Test + public void spliteratorForEachRemainingWithAdvance() { + list.addAll(getIntegerList(10_000)); + Spliterator rightSpliterator = list.spliterator(); + + assertTrue( + rightSpliterator.tryAdvance( + i -> assertEquals(Integer.valueOf(0), i))); + + Spliterator leftSpliterator = rightSpliterator.trySplit(); + + assertEquals(4_999, rightSpliterator.getExactSizeIfKnown()); + assertEquals(5_000, leftSpliterator.getExactSizeIfKnown()); + + // Check two leftmost elements of the left spliterator: + assertTrue(leftSpliterator.tryAdvance( + i -> assertEquals(Integer.valueOf(1), i))); + + assertTrue(leftSpliterator.tryAdvance( + i -> assertEquals(Integer.valueOf(2), i))); + + // Check two leftmost elements of the right splliterator: + assertTrue(rightSpliterator.tryAdvance( + i -> assertEquals(Integer.valueOf(5_000), i))); + + assertTrue(rightSpliterator.tryAdvance( + i -> assertEquals(Integer.valueOf(5_001), i))); + } + + @Test + public void spliterator() { + list.addAll(getIntegerList(6_000)); + Spliterator split = list.spliterator(); + + assertEquals(6_000L, split.getExactSizeIfKnown()); + assertEquals(6_000L, split.estimateSize()); + + assertTrue(split.tryAdvance((i) -> assertEquals(list.get((int) i), i))); + assertTrue(split.tryAdvance((i) -> assertEquals(list.get((int) i), i))); + + assertEquals(5998, split.getExactSizeIfKnown()); + + // 5998 elements left / 2 = 2999 per spliterator: + Spliterator leftSpliterator = split.trySplit(); + + assertNotNull(leftSpliterator); + assertEquals(2999, split.getExactSizeIfKnown()); + assertEquals(2999, leftSpliterator.getExactSizeIfKnown()); + + for (int i = 2; i < 3000; i++) { + Integer integer = list.get(i); + assertTrue( + leftSpliterator.tryAdvance( + (j) -> assertEquals(integer, j))); + } + + //// split = [3001, 5999] + + assertTrue(split.tryAdvance(i -> assertEquals(2999, i))); + assertTrue(split.tryAdvance(i -> assertEquals(3000, i))); + assertTrue(split.tryAdvance(i -> assertEquals(3001, i))); + + while (split.getExactSizeIfKnown() > 0) { + split.tryAdvance(i -> {}); + } + + assertFalse(split.tryAdvance(i -> {})); + } + + @Test + public void bruteforceSpliterator() { + list.addAll(getIntegerList(1_000_000)); + Collections.shuffle(list, new Random(13)); + + List newList = + list.parallelStream() + .map(i -> 2 * i) + .collect(Collectors.toList()); + + assertEquals(newList.size(), list.size()); + + for (int i = 0; i < list.size(); i++) { + Integer integer1 = 2 * list.get(i); + Integer integer2 = newList.get(i); + assertEquals(integer1, integer2); + } + } + + private static final String SERIALIZATION_FILE_NAME = "LinkedList.ser"; + + @Test + public void serialization() { + list.add(10); + list.add(13); + list.add(12); + + try { + File file = new File(SERIALIZATION_FILE_NAME); + + FileOutputStream fos = new FileOutputStream(file); + ObjectOutputStream oos = new ObjectOutputStream(fos); + + oos.writeObject(list); + oos.flush(); + oos.close(); + + FileInputStream fis = new FileInputStream(file); + ObjectInputStream ois = new ObjectInputStream(fis); + + IndexedLinkedList ll = + (IndexedLinkedList) ois.readObject(); + + ois.close(); + boolean equal = listsEqual(list, ll); + assertTrue(equal); + + if (!file.delete()) { + file.deleteOnExit(); + } + + } catch (IOException | ClassNotFoundException ex) { + fail(ex.getMessage()); + } + } + + @Test + public void bruteforceSerialization() { + for (int i = 0; i < 20; i++) { + list.addAll(getIntegerList(i)); + + try { + File file = new File(SERIALIZATION_FILE_NAME); + + FileOutputStream fos = new FileOutputStream(file); + ObjectOutputStream oos = new ObjectOutputStream(fos); + + oos.writeObject(list); + oos.flush(); + oos.close(); + + FileInputStream fis = new FileInputStream(file); + ObjectInputStream ois = new ObjectInputStream(fis); + + IndexedLinkedList ll = + (IndexedLinkedList) ois.readObject(); + + ois.close(); + boolean equal = listsEqual(list, ll); + assertTrue(equal); + + if (!file.delete()) { + file.deleteOnExit(); + } + + } catch (IOException | ClassNotFoundException ex) { + fail(ex.getMessage()); + } + + list.clear(); + } + } + + @Test + public void bugCheckInvariantAfterRemoval() { + for (int i = 0; i < 4; i++) { + list.add(i); + } + + list.remove(Integer.valueOf(3)); + list.checkInvarant(); + list.remove(1); + list.checkInvarant(); + + assertEquals(list.size(), 2); + assertEquals(Integer.valueOf(0), list.get(0)); + assertEquals(Integer.valueOf(2), list.get(1)); + } + + @Test + public void bruteForceRemoveAt1() { + Random random = new Random(400L); + + list.addAll(getIntegerList(1000)); + referenceList.clear(); + referenceList.addAll(list); + + Integer probe = 3; + + list.remove(probe); + referenceList.remove(probe); + + int iters = 0; + + while (!list.isEmpty()) { + iters++; + int index = random.nextInt(list.size()); + list.remove(index); + referenceList.remove(index); + + listsEqual(list, referenceList); + } + } + + @Test + public void contractAdaptsToMinimumCapacity() { + list.addAll(getIntegerList(1000_000)); + list.checkInvarant(); + list.subList(10, 1000_000 - 10).clear(); + list.checkInvarant(); + assertEquals(20, list.size()); + } + + @Test + public void bruteForceRemoveAt2() { + long seed = 1630487847317L; + Random random = new Random(seed); + + for (int i = 0; i < 100; i++) { + list.addAll(getIntegerList(10)); + List indices = new ArrayList<>(list.size()); + + while (!list.isEmpty()) { + int index = random.nextInt(list.size()); + indices.add(index); + + try { + list.remove(index); + } catch (AssertionError ae) { + System.out.println( + "Message: " + ae.getMessage() + ", indices: " + + indices.toString()); + return; + } + } + + indices.clear(); + } + } + + @Test + public void bugRemoveAt2() { + list.addAll(getIntegerList(10)); + referenceList.clear(); + referenceList.addAll(list); + + final int[] indices = { 7, 7, 4, 1, 2, 1, 3, 1, 1, 0 }; + + for (int i = 0; i < indices.length; i++) { + final int index = indices[i]; + list.remove(index); + list.checkInvarant(); + referenceList.remove(index); + assertEquals(referenceList, list); + } + } + + @Test + public void bugRemoveAt() { + list.addAll(getIntegerList(10)); + + assertEquals(Integer.valueOf(5), list.remove(5)); + list.checkInvarant(); + + assertEquals(Integer.valueOf(3), list.remove(3)); + list.checkInvarant(); + + assertEquals(Integer.valueOf(2), list.remove(2)); + list.checkInvarant(); + + assertEquals(Integer.valueOf(1), list.remove(1)); + list.checkInvarant(); + + // list = [0, 4, 5, 7, 8, 8] + assertEquals(Integer.valueOf(8), list.remove(4)); + list.checkInvarant(); + } + + @Test + public void bugRemoveFirst() { + list.addAll(getIntegerList(5)); + + assertEquals(5, list.size()); + + for (int i = 0; i < 2; i++) { + list.removeFirst(); + } + + Random random = new Random(500L); + List referenceList = new ArrayList<>(list); + + while (!list.isEmpty()) { + int index = random.nextInt(list.size()); + list.remove(index); + referenceList.remove(index); + assertTrue(listsEqual(list, referenceList)); + } + } + + @Test + public void bugRemoveLast() { + list.addAll(getIntegerList(10)); + + assertEquals(10, list.size()); + + for (int i = 0; i < 5; i++) { + list.removeLast(); + } + + Random random = new Random(600L); + List referenceList = new ArrayList<>(list); + + while (!list.isEmpty()) { + int index = random.nextInt(list.size()); + list.remove(index); + referenceList.remove(index); + assertTrue(listsEqual(list, referenceList)); + } + } + + @Test + public void bruteForceRemoveFirstOccurrence() { + final Random random = new Random(13L); + + for (int i = 0; i < 100; i++) { + list.add(random.nextInt(30)); + } + + final LinkedList referenceList = new LinkedList<>(list); + + while (!list.isEmpty()) { + final int target = random.nextInt(40); + + assertEquals(referenceList, list); + + list.removeFirstOccurrence(target); + list.checkInvarant(); + + referenceList.removeFirstOccurrence(target); + + assertEquals(referenceList, list); + } + } + + @Test + public void bruteForceRemoveLastOccurrence() { + final Random random = new Random(13L); + + for (int i = 0; i < 100; i++) { + list.add(random.nextInt(30)); + } + + final LinkedList referenceList = new LinkedList<>(list); + + while (!list.isEmpty()) { + final int target = random.nextInt(40); + + assertEquals(referenceList, list); + + list.removeLastOccurrence(target); + list.checkInvarant(); + + referenceList.removeLastOccurrence(target); + + assertEquals(referenceList, list); + } + } + + private static List getIntegerList(int length) { + List list = new ArrayList<>(length); + + for (int i = 0; i < length; i++) { + list.add(i); + } + + return list; + } + + private static List getIntegerList() { + return getIntegerList(100); + } + + private static boolean listsEqual(IndexedLinkedList list1, + List list2) { + + if (list1.size() != list2.size()) { + return false; + } + + Iterator iter1 = list1.iterator(); + Iterator iter2 = list2.iterator(); + + while (iter1.hasNext() && iter2.hasNext()) { + Integer int1 = iter1.next(); + Integer int2 = iter2.next(); + + if (!int1.equals(int2)) { + return false; + } + } + + if (iter1.hasNext() || iter2.hasNext()) { + throw new IllegalStateException(); + } + + return true; + } + + @Test + public void copy() { + + for (int i = 0; i < 1000; i++) { + list.add(i); + } + + Random random = new Random(10L); + list.randomizeFingers(random); + + for (int i = 0; i < 1000; i++) { + assertEquals(Integer.valueOf(i), list.get(i)); + } + } + + @Test + public void getNodeSequentially() { + list.addAll(getIntegerList(5)); + assertEquals(Integer.valueOf(3), + list.getNodeSequentially(3).item); + } + + @Test + public void stressTestGetNodeSequentially() { + list.addAll(getIntegerList(100)); + + for (int i = 0; i < list.size(); ++i) { + assertEquals(list.get(i), + list.getNodeSequentially(i).item); + } + } + + @Test + public void getPrefixNode() { + for (int i = 0; i < 1000; i++) { + list.add(i); + } + + Random random = new Random(4L); + + list.randomizeFingers(random); + + for (int index = 0; index < 11; index++) { + int datum = list.get(index); + + assertEquals(index, datum); + } + + int datum = list.get(10); + + assertEquals(10, datum); + } + + @Test + public void debugFingerGetNode() { + for (int i = 0; i < 10; i++) { + list.add(i); + } + + Integer datum = list.get(9); + + assertEquals(Integer.valueOf(9), datum); + } + + @Test + public void listIteratorRemove() { + list.addAll(getIntegerList(100)); + referenceList.addAll(list); + + final Random random = new Random(13L); + final int initialIndex = random.nextInt(list.size() + 1); + + final ListIterator listIterator = + list.listIterator(initialIndex); + + final ListIterator referenceListIterator = + referenceList.listIterator(initialIndex); + + boolean previousMove = false; + + while (!list.isEmpty()) { + + final int coin = random.nextInt(3); + + switch (coin) { + case 0: + if (listIterator.hasPrevious()) { + listIterator.previous(); + referenceListIterator.previous(); + previousMove = true; + } + + break; + + case 1: + if (listIterator.hasNext()) { + listIterator.next(); + referenceListIterator.next(); + previousMove = true; + } + + break; + + case 2: + if (previousMove) { + previousMove = false; + list.checkInvarant(); + listIterator.remove(); + list.checkInvarant(); + referenceListIterator.remove(); + assertEquals(referenceList, list); + } + + break; + } + } + } + /* + void loadFingerCoverageCounters(int fromFingerIndex, + int toFingerIndex, + int fromIndex, + int toIndex, + int fingersToRemove)*/ + @Test + public void loadFingerCoverageCounters1() { + list.size = 10; + list.fingerList.size = 4; + list.loadFingerCoverageCounters(2, 4, 3, 6, 1); + assertEquals(0, list.numberOfCoveringFingersToPrefix); + assertEquals(1, list.numberOfCoveringFingersToSuffix); + } + + @Test + public void loadFingerCoverageCounters2() { + list.size = 100; + list.fingerList.size = 10; + list.loadFingerCoverageCounters(0, 9, 1, 99, 8); + assertEquals(1, list.numberOfCoveringFingersToPrefix); + assertEquals(0, list.numberOfCoveringFingersToSuffix); + } + + @Test + public void loadFingerCoverageCounters3() { + list.size = 22; + list.fingerList.size = 5; + list.loadFingerCoverageCounters(0, 5, 13, 18, 0); + assertEquals(3, list.numberOfCoveringFingersToPrefix); + assertEquals(2, list.numberOfCoveringFingersToSuffix); + } + + @Test + public void makeRoomAtPrefix1() { + list.clear(); + + for (int i = 0; i < 10; i++) { + list.add(i); + } + + list.fingerList.setFingerIndices(2, 4, 6, 7); + + list.fingerList.makeRoomAtPrefix(4, 1, 2); + } + + @Test + public void bruteForceDeleteRangeAllIndexCombinations() { + IndexListGenerator ilg = new IndexListGenerator(11, 4); + List sourceList = getIntegerList(11); + List referenceList = new ArrayList<>(); + + int iteration = 0; + int combinations = 0; + + do { + int[] indices = ilg.getIndices(); + + combinations++; + + for (int fromIndex = 0; fromIndex <= 10; fromIndex++) { + for (int toIndex = fromIndex; toIndex <= 10; toIndex++) { + + iteration++; + + list.clear(); + list.addAll(sourceList); + list.fingerList.setFingerIndices(indices); + + referenceList.clear(); + referenceList.addAll(sourceList); + + list.subList(fromIndex, toIndex).clear(); + referenceList.subList(fromIndex, toIndex).clear(); + + assertEquals(referenceList, list); + + list.checkInvarant(); + } + } + + } while (ilg.inc()); + } + + @Test + public void removeByIndex1() { + list.addAll(getIntegerList(10)); + referenceList.addAll(list); + + list.remove(4); + referenceList.remove(4); + + assertEquals(referenceList, list); + list.checkInvarant(); + } + + @Test + public void removeByIndex2() { + list.addAll(getIntegerList(9)); + referenceList.addAll(list); + list.fingerList.setFingerIndices(2, 5, 8); + list.remove(5); + referenceList.remove(5); + + assertEquals(referenceList, list); + list.checkInvarant(); + } + + @Test + public void removeByIndex3() { + list.addAll(getIntegerList(9)); + referenceList.addAll(list); + list.fingerList.setFingerIndices(2, 5, 8); + list.remove(6); + referenceList.remove(6); + + assertEquals(referenceList, list); + list.checkInvarant(); + } +} + +class IndexListGenerator { + + private final int listSize; + private final int[] indices; + + IndexListGenerator(final int listSize, + final int numberOfIndices) { + this.listSize = listSize; + this.indices = new int[numberOfIndices]; + + preload(); + } + + boolean inc() { + if (indices[indices.length - 1] < listSize - 1) { + indices[indices.length - 1]++; + return true; + } + + for (int i = indices.length - 2; i >= 0; i--) { + int a = indices[i]; + int b = indices[i + 1]; + + if (a < b - 1) { + indices[i]++; + + for (int j = i + 1; j < indices.length; j++) { + indices[j] = indices[j - 1] + 1; + } + + return true; + } + } + + return false; + } + + int[] getIndices() { + return indices; + } + + private void preload() { + for (int i = 0; i < indices.length; i++) { + indices[i] = i; + } + } +}