diff --git a/primitiveCollections/src/main/java/org/lucares/collections/LongList.java b/primitiveCollections/src/main/java/org/lucares/collections/LongList.java new file mode 100644 index 0000000..22fe225 --- /dev/null +++ b/primitiveCollections/src/main/java/org/lucares/collections/LongList.java @@ -0,0 +1,1052 @@ +package org.lucares.collections; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.List; +import java.util.Random; +import java.util.Spliterator.OfLong; +import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.LongStream; +import java.util.stream.StreamSupport; + +/** + * A list for primitive longs. + *
+ * This class does not (yet) implements all methods a java.util {@link List} + * would have. + */ +public final class LongList implements Serializable, Cloneable { + + private static final long serialVersionUID = 2622570032686034909L; + + private static final int DEFAULT_CAPACITY = 10; + + /** + * The maximum size of an array. + */ + private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; + + private static final long[] EMPTY_ARRAY = {}; + + /** + * The array containing the values. It is transient, so that we can implement + * our own serialization. + */ + transient private long[] data; + + private int size = 0; + + /** + * Keeps track of whether or not the list is sorted. This allows us to use + * binary search for {@link #indexOf(int)}. An empty list is sorted. + */ + private boolean sorted = true; + + /** + * Create a new {@link LongList} with initial capacity 10. + */ + public LongList() { + this(DEFAULT_CAPACITY); + } + + /** + * Create a new {@link LongList}. + * + * @param initialCapacity + * initial capacity + * @throws IllegalArgumentException + * if initial capacity is negative + */ + public LongList(final int initialCapacity) { + if (initialCapacity < 0 || initialCapacity > MAX_ARRAY_SIZE) { + throw new IllegalArgumentException( + "initial capacity must not be negative and not larger than " + MAX_ARRAY_SIZE); + } + + data = initialCapacity > 0 ? new long[initialCapacity] : EMPTY_ARRAY; + } + + /** + * Create a new {@link LongList} with a copy of the elements of + * {@code longList}. + * + * @param longList + * the list to copy + * @throws NullPointerException + * if the specified {@link LongList} is null + */ + public LongList(final LongList longList) { + data = EMPTY_ARRAY; + addAll(longList); + } + + /** + * Create a new {@link LongList} with a copy of the given elements. + * + * @param values + * the values + * @return the list + * @throws NullPointerException + * if the specified array is null + */ + public static LongList of(final long... values) { + final LongList result = new LongList(values.length); + result.addAll(values); + return result; + } + + /** + * Returns {@code true} if this list contains no elements. + * + * @return {@code true} if this list contains no elements + */ + public boolean isEmpty() { + return size == 0; + } + + /** + * Returns the number of elements in this list. + * + * @return the number of elements in this list + */ + public int size() { + return size; + } + + /** + * Returns the number of elements this list can hold before a resize is + * necessary, this is the size of the internal backing array. + * + * @return the number of elements this list can hold before a resize is + * necessary + */ + public int getCapacity() { + return data.length; + } + + /** + * Returns whether or not the list is sorted. + *
+ * An empty list is sorted. + * + * @return true iff the list is sorted. + */ + public boolean isSorted() { + return sorted; + } + + /** + * Adds {@code value} to the list. + * + * @param value + * the value to add + */ + public void add(final long value) { + ensureCapacity(1); + + data[size] = value; + + if (sorted) { + sorted = size == 0 ? sorted : data[size - 1] <= data[size]; + } + size++; + } + + /** + * Inserts {@code values} at position {@code pos} into the list. + * + * @param pos + * the position to insert the elements + * @param values + * the elements to insert + * @throws IndexOutOfBoundsException + * if pos is out of bounds {@code pos < 0 || pos > size()} + * @throws NullPointerException + * if the given array is null + */ + public void insert(final int pos, final long... values) { + + if (pos < 0) { + throw new IndexOutOfBoundsException("pos must not be negative, but was: " + pos); + } + if (pos > size) { + throw new IndexOutOfBoundsException("pos must be smaller than size(), but was: " + pos); + } + if (values == null) { + throw new NullPointerException("values parameter must not be null"); + } + if (values.length == 0) { + return; + } + + ensureCapacity(values.length); + + // move everything after the insert position to make room for the new values + System.arraycopy(data, pos, data, pos + values.length, size - pos); + // insert the new values + System.arraycopy(values, 0, data, pos, values.length); + size += values.length; + + if (sorted) { + // compare with element before the insertion + sorted = pos == 0 ? sorted : data[pos - 1] <= data[pos]; + + // check if the inserted values are sorted + for (int i = 1; i < values.length && sorted; i++) { + sorted = data[pos + i - 1] <= data[pos + i]; + } + if (sorted) { + // compare last inserted element with next element in the list + sorted = pos + values.length < size()// + ? data[pos + values.length - 1] <= data[pos + values.length]// + : sorted; + } + } + + } + + /** + * Set the value {@code value} at position {@code pos}. + * + * @param pos + * the position to overwrite + * @param value + * the new value + * @throws IndexOutOfBoundsException + * if pos is out of bounds {@code pos < 0 || pos >= size()} + */ + public void set(final int pos, final long value) { + + if (pos < 0) { + throw new IndexOutOfBoundsException("pos must not be negative, but was: " + pos); + } + + if (pos >= size) { + throw new IndexOutOfBoundsException("pos must not smaller than size(), but was: " + pos); + } + + data[pos] = value; + + if (sorted) { + sorted = pos <= 0 ? sorted : data[pos - 1] <= data[pos]; + sorted = pos + 1 >= size() ? sorted : data[pos] <= data[pos + 1]; + } + } + + /** + * Add {@code values} to the list. + * + * @param values + * the values to add + * @throws NullPointerException + * if the given array is null + */ + public void addAll(final long... values) { + ensureCapacity(values.length); + + System.arraycopy(values, 0, data, size, values.length); + + if (sorted) { + for (int i = 0; i < values.length && sorted; i++) { + sorted = size + i - 1 < 0 ? sorted : data[size + i - 1] <= data[size + i]; + } + } + + size += values.length; + } + + /** + * Add all value of the given list. + * + * @param list + * the list + * @throws NullPointerException + * if the given list is null + */ + public void addAll(final LongList list) { + ensureCapacity(list.size()); + + System.arraycopy(list.data, 0, data, size, list.size()); + + if (sorted) { + sorted = list.isSorted(); + if (list.isSorted() // new list is sorted + && !isEmpty() // if old list is empty, then the new list's sorted value is equal to that of + // the new list + && !list.isEmpty()) // if new list is empty, then old list stays sorted + { + // check that the first new value is bigger than the highest old value + final long highestOldValue = data[size - 1]; // size has still the old value + final long lowestNewValue = list.get(0); + sorted = highestOldValue <= lowestNewValue; + } + } + + size += list.size(); + } + + /** + * Removes elements from the list. + *
+ * This method does not release any memory. Call {@link #trim()} to free unused + * memory. + * + * @param fromIndex + * index of the first element to remove + * @param toIndex + * the index of the last element to remove (exclusive) + * @throws IndexOutOfBoundsException + * if {@code fromIndex} or {@code toIndex} is negative, or if the + * range defined by {@code fromIndex} and {@code toIndex} is out of + * bounds + * @see #trim() + */ + public void remove(final int fromIndex, final int toIndex) { + + if (fromIndex < 0) { + throw new IndexOutOfBoundsException("from must not be negative, but was: " + fromIndex); + } + if (toIndex < fromIndex) { + throw new IndexOutOfBoundsException("toIndex must not be smaller than fromIndex, but was: " + toIndex); + } + if (toIndex > size) { + throw new IndexOutOfBoundsException( + "toIndex must not be larger than the size of this list, but was: " + toIndex); + } + + final int numRemoved = size - toIndex; + System.arraycopy(data, toIndex, data, fromIndex, numRemoved); + + size = size - (toIndex - fromIndex); + + if (!sorted) { + sorted = true; + for (int i = 1; i < size && sorted; i++) { + sorted = data[i - 1] <= data[i]; + } + } + } + + /** + * Remove all elements that contained in the specified list. + *
+ * This method does not release any memory. Call {@link #trim()} to free unused + * memory. + *
+ * If {@code retain} is sorted, then the algorithm has a complexity of + * O(n*log(m)), where n is the length of {@code this} and m the length of + * {@code retain}. If {@code retain} is not sorted, then the complexity is + * O(n*m). + * + * @param remove + * the elements to remove + * @throws NullPointerException + * if the specified {@link LongList} is null + * @see #trim() + */ + public void removeAll(final LongList remove) { + removeIf((val, index) -> remove.indexOf(val) >= 0); + } + + /** + * Remove all elements that match the given predicate. + *
+ * This method does not release any memory. Call {@link #trim()} to free unused + * memory. + * + * + * @param predicate + * the predicate + * @throws NullPointerException + * if the specified predicate is null + * @see #trim() + */ + public void removeIf(final LongPredicate predicate) { + + int insertPosition = 0; + for (int i = 0; i < size; i++) { + final long current = data[i]; + if (!predicate.test(current, i)) { + // keep current element + data[insertPosition] = current; + insertPosition++; + } + } + size = insertPosition; + + if (!sorted) { + sorted = true; + for (int i = 1; i < size && sorted; i++) { + sorted = data[i - 1] <= data[i]; + } + } + } + + /** + * Retains all elements contained in the specified list. + *
+ * This method does not release any memory. Call {@link #trim()} to free unused + * memory. + *
+ * For a method that computes the intersection of two lists and also removes + * duplicate values, see {@link #intersection(LongList, LongList)}. + *
+ * If {@code retain} is sorted, then the algorithm has a complexity of + * O(n*log(m)), where n is the length of {@code this} and m the length of + * {@code retain}. If {@code retain} is not sorted, then the complexity is + * O(n*m). + * + * @param retain + * the elements to retain + * @throws NullPointerException + * if the specified {@link LongList} is null + * @see #trim() + * @see #intersection(LongList, LongList) + */ + public void retainAll(final LongList retain) { + removeIf((val, index) -> retain.indexOf(val) < 0); + } + + /** + * Replaces all values in the list by applying {@code operator}. + * + * @param operator + * the operator + * @throws NullPointerException + * if the specified {@link UnaryLongOperator} is null + */ + public void replaceAll(final UnaryLongOperator operator) { + + for (int i = 0; i < size; i++) { + final long newValue = operator.apply(data[i]); + set(i, newValue); + } + } + + /** + * Returns the element at position {@code pos}. + * + * @param pos + * position of the element to return + * @return the element at position {@code pos} + * @throws IndexOutOfBoundsException + * if {@code pos} is out of bounds + * {@code index < 0 || index >= size()} + */ + public long get(final int pos) { + if (pos < 0 || pos >= size) { + throw new IndexOutOfBoundsException(); + } + return data[pos]; + } + + /** + * Returns the {@code length} elements starting at {@code from}. + * + * @param from + * position of the first element + * @param length + * number of elements + * @return the {@code length} elements starting at {@code from} + * @throws IndexOutOfBoundsException + * if {@code from} or {@code length} is negative, or if the range + * defined by {@code from} and {@code length} is out of bounds + */ + public long[] get(final int from, final int length) { + if (from < 0) { + throw new IndexOutOfBoundsException("from must not be negative, but was: " + from); + } + if (length < 0) { + throw new IndexOutOfBoundsException("length must not be negative, but was: " + length); + } + + if (from + length > size) { + throw new IndexOutOfBoundsException("from: " + from + " length: " + length); + } + return Arrays.copyOfRange(data, from, from + length); + } + + /** + * Returns an array containing all elements of this list. + * + * @return an array containing all elements of this list + */ + public long[] toArray() { + return get(0, size); + } + + /** + * Fills the given array with the elements of this list if the array can hold + * all elements. A new array is returned otherwise. + * + * @param input + * @throws NullPointerException + * if the specified array is null + * @return an array containing all elements of this list + */ + public long[] toArray(final long[] input) { + + if (input.length < size) { + return toArray(); + } + System.arraycopy(data, 0, input, 0, size); + return input; + } + + /** + * Sorts the list into ascending order. + */ + public void sort() { + Arrays.sort(data, 0, size); + sorted = true; + } + + /** + * Sorts the list into ascending order using an algorithm than can use + * parallelism. + */ + public void parallelSort() { + Arrays.parallelSort(data, 0, size); + sorted = true; + } + + /** + * Shuffles the list. The permutation is uniformly distributed. + */ + public void shuffle() { + shuffle(ThreadLocalRandom.current()); + } + + /** + * Shuffles the list. The permutation is uniformly distributed. + * + * @param random + * the random number generator used + */ + public void shuffle(final Random random) { + + /* + * See Knuth, Donald E. Seminumerical algorithms. (1998). The Art of Computer + * Programming. 2. Addison–Wesley pp. 145–146. Algorithm P and exercise 18. + */ + for (int i = size() - 1; i > 0; i--) { + final int other = random.nextInt(i); + + final long tmp = data[other]; + data[other] = data[i]; + data[i] = tmp; + } + + // update sorted + sorted = true; + for (int i = 1; i < size && sorted; i++) { + sorted = data[i - 1] <= data[i]; + } + } + + private void ensureCapacity(final int newElements) { + + final int requiredCapacity = size + newElements; + if (requiredCapacity < 0 || requiredCapacity > MAX_ARRAY_SIZE) { + // overflow, or too big + throw new OutOfMemoryError(); + } + if (requiredCapacity > data.length) { + + int newCapacity = data.length + data.length / 2; + newCapacity = Math.max(requiredCapacity, newCapacity); + newCapacity = Math.min(MAX_ARRAY_SIZE, newCapacity); + + data = Arrays.copyOf(data, newCapacity); + } + } + + /** + * Reduces the capacity to the size of the list. + *
+ * Call this method to reduce the memory consumption of this list. + */ + public void trim() { + if (size == 0) { + data = EMPTY_ARRAY; + } else if (size != data.length) { + data = Arrays.copyOf(data, size); + } + } + + /** + * Removes all elements from this list. + *
+ * The implementation is equivalent to calling {@code remove(0, size())} and + * {@code trim()}. + */ + public void clear() { + data = EMPTY_ARRAY; + size = 0; + sorted = true; + } + + /** + * Returns a sequential {@link LongStream} with this collection as its source. + * + * @return a sequential {@link LongStream} + */ + public LongStream stream() { + + return Arrays.stream(data, 0, size); + } + + /** + * Returns a parallel {@link LongStream} with this collection as its source. + * + * @return a parallel {@link LongStream} + */ + public LongStream parallelStream() { + final OfLong spliterator = Arrays.spliterator(data, 0, size); + return StreamSupport.longStream(spliterator, true); + } + + /** + * Returns the index of the first occurrence of {@code value}, or -1 if it does + * not exist. + *
+ * This method uses a binary search algorithm if the list is sorted. + * + * @param value + * the value + * @return the index, or -1 + * @see #isSorted() + */ + public int indexOf(final long value) { + return indexOf(value, 0); + } + + /** + * Returns the index of the first occurrence of {@code value}, or -1 if it does + * not exist. + *
+ * This method uses a binary search algorithm if the list is sorted. + * + * @param value + * the value + * @param offset + * the offset (inclusive). There is no invalid value. If the offset + * is negative, then it behaves as if it was 0. If it is >= + * {@link #size()}, then -1 is returned. + * @return the index, or -1 + * @throws ArrayIndexOutOfBoundsException + * if offset is negative, or larger than the size of the list + * @throws IllegalArgumentException + * if offset is negative + * @see #isSorted() + */ + public int indexOf(final long value, final int offset) { + + int result = -1; + if (sorted && offset >= 0 && offset < size()) { + int insertionPoint = Arrays.binarySearch(data, offset, size(), value); + while (insertionPoint > offset && data[insertionPoint - 1] == value) { + insertionPoint--; + } + result = insertionPoint < 0 ? -1 : insertionPoint; + } else { + final int start = Math.max(offset, 0); + for (int i = start; i < size; i++) { + if (data[i] == value) { + result = i; + break; + } + } + } + return result; + } + + /** + * Returns the index of the last occurrence of {@code value} searching + * backwards, or -1 if it does not exist. + *
+ * This method uses a binary search algorithm if the list is sorted. + * + * @param value + * the value + * @return the index, or -1 + * @see #isSorted() + */ + public int lastIndexOf(final long value) { + return lastIndexOf(value, size() - 1); + } + + /** + * Returns the index of the last occurrence of {@code value} searching backwards + * from {@code fromIndex}, or -1 if it does not exist. + *
+ * This method uses a binary search algorithm if the list is sorted. + * + * @param value + * the value + * @param fromIndex + * the index the start the search from (inclusive). There is no + * invalid input. If {@code fromIndex} is < 0, then -1 is + * returned. If it is larger than the length of the list, then it + * behaves as if it were {@link #size()}-1. + * @return the index, or -1 + * @see #isSorted() + */ + public int lastIndexOf(final long value, final int fromIndex) { + int result = -1; + + if (sorted) { + final int toIndex = Math.min(size - 1, fromIndex + 1); // toIndex is exclusive in binarySearch, but + // fromIndex is inclusive + if (toIndex > 0) { + int insertionPoint = Arrays.binarySearch(data, 0, toIndex, value); + + // if value exists more than once, then binarySearch can find any one of them, + // but we are looking for the last occurrence + while (insertionPoint >= 0 && insertionPoint < fromIndex && data[insertionPoint + 1] == value) { + insertionPoint++; + } + result = insertionPoint < 0 ? -1 : insertionPoint; + } + } else { + final int startIndex = Math.min(size - 1, fromIndex); + for (int i = startIndex; i >= 0; i--) { + if (data[i] == value) { + result = i; + break; + } + } + } + return result; + } + + @Override + public String toString() { + + assert data != null : "data cannot be null"; + final int iMax = size - 1; + if (iMax == -1) + return "[]"; + + final StringBuilder result = new StringBuilder(); + result.append('['); + for (int i = 0; i < size; i++) { + if (i > 0) { + result.append(", "); + } + result.append(data[i]); + } + return result.append(']').toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + size; + + /* + * only consider values in the range of 0 to size + */ + for (int i = 0; i < size; i++) { + result = 31 * result + (int) data[i]; // low bits + result = 31 * result + (int) (data[i] >> 32); // high bits + } + + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + final LongList other = (LongList) obj; + if (size != other.size) + return false; + if (sorted != other.sorted) + return false; + + /* + * only consider values in the range of 0 to size + */ + for (int i = 0; i < size; i++) { + if (data[i] != other.data[i]) { + return false; + } + } + + return true; + } + + /** + * Serialize the {@link LongList}. + *
+ * This only serializes the used part of the data array. + * + * @serialData The length of the array containing the values, followed by the + * values. + */ + private void writeObject(final java.io.ObjectOutputStream s) throws java.io.IOException { + s.defaultWriteObject(); + + // write out the array length. According to the implementation in ArrayList this + // is needed to be compatible with clone. + s.writeInt(size); + + for (int i = 0; i < size; i++) { + s.writeLong(data[i]); + } + } + + /** + * Deserialize the {@link LongList} + */ + private void readObject(final java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { + data = EMPTY_ARRAY; + + s.defaultReadObject(); + + // Read in capacity + s.readInt(); // ignored + + if (size > 0) { + final long[] local_data = new long[size]; + + for (int i = 0; i < size; i++) { + local_data[i] = s.readLong(); + } + data = local_data; + } + } + + @Override + public LongList clone() { + try { + final LongList result = (LongList) super.clone(); + result.data = size == 0 ? EMPTY_ARRAY : Arrays.copyOf(data, size); + return result; + } catch (final CloneNotSupportedException e) { + throw new IllegalStateException(e); + } + } + + /** + * Returns a list with all elements that are in {@code a} and {@code b}. + *
+ * The cardinality of each element will be equal to one (like in a set). This + * method returns a new list. The list can have unused capacity. Consider using + * {@link #trim()}. + *
+ * If both lists were sorted, then the output list will also be sorted. + *
+ * See {@link #retainAll(LongList)} for a method that modifies the list and + * keeps duplicate values. + *
+ * If both lists are sorted, then the time complexity is O(n+m), where n is the + * length of the first list and m the length of the second list. If at least one + * list is not sorted, then the time complexity is O(n*m), where n is the length + * of the shorter list and m the length of the longer list. + * + * @param a + * a sorted {@link LongList} + * @param b + * a sorted {@link LongList} + * @return {@link LongList} containing all elements that are in {@code a} and + * {@code b} + * @throws NullPointerException + * if {@code a} or {@code b} is null + * @see #retainAll(LongList) + * @see #trim() + */ + public static LongList intersection(final LongList a, final LongList b) { + final LongList result; + + if (a.isSorted() && b.isSorted()) { + result = intersectionSorted(a, b); + } else { + result = intersectionUnsorted(a, b); + } + return result; + } + + /** + * Implements an intersection algorithm with O(n+m), where n is the length of + * the first list and m the length of the second list. + * + * @param a + * first list + * @param b + * second list + * @return the intersection + */ + private static LongList intersectionSorted(final LongList a, final LongList b) { + final int aSize = a.size(); + final int bSize = b.size(); + final LongList result = new LongList(Math.min(aSize, bSize)); + + assert a.isSorted() : "first list must be sorted"; + assert b.isSorted() : "second list must be sorted"; + + int l = 0; + int r = 0; + + while (l < aSize && r < bSize) { + + final long lv = a.get(l); + final long rv = b.get(r); + + if (lv < rv) { + l++; + } else if (lv > rv) { + r++; + } else { + result.add(lv); + do { + l++; + } while (l < aSize && lv == a.get(l)); + do { + r++; + } while (r < bSize && rv == b.get(r)); + } + } + return result; + } + + /** + * Implements an algorithm with O(n*m), where n is the length of the shorter + * list and m the length of the longer list. + * + * @param a + * first list + * @param b + * second list + * @return the intersection + */ + private static LongList intersectionUnsorted(final LongList a, final LongList b) { + final int aSize = a.size(); + final int bSize = b.size(); + final LongList result; + + if (aSize < bSize) { + result = new LongList(Math.min(aSize, bSize)); + + for (int l = 0; l < aSize; l++) { + final long lv = a.get(l); + + if (b.indexOf(lv) >= 0) { + result.add(lv); + } + + while (l + 1 < aSize && lv == a.get(l + 1)) { + l++; + } + } + } else { + result = intersectionUnsorted(b, a); + } + + return result; + } + + /** + * Returns a list with all elements that are in list {@code a} or {@code b} + * (logical or). + *
+ * The result does not contain duplicate values. + *
+ * If both lists were sorted, then the output list will also be sorted. If at + * least one list is unsorted, then the order is undefined. + *
+ * If both lists are sorted, then the time complexity is O(n+m), where n is the
+ * length of the first list and m the length of the second list. If at least one
+ * list is not sorted, then the time complexity is O(m*log(m)), where m is the
+ * length of the longer list.
+ *
+ * @param a
+ * the first list
+ * @param b
+ * the second list
+ * @return the union of both lists
+ */
+ public static LongList union(final LongList a, final LongList b) {
+ final LongList result;
+
+ if (a.isSorted() && b.isSorted()) {
+ result = unionSorted(a, b);
+ } else {
+ result = unionUnsorted(a, b);
+ }
+ return result;
+ }
+
+ private static LongList unionSorted(final LongList a, final LongList b) {
+
+ final int aSize = a.size();
+ final int bSize = b.size();
+
+ final LongList result = new LongList(aSize + bSize);
+
+ int l = 0;
+ int r = 0;
+
+ while (l < aSize && r < bSize) {
+
+ final long lv = a.get(l);
+ final long rv = b.get(r);
+
+ if (lv < rv) {
+ result.add(lv);
+ l++;
+ while (l < aSize && lv == a.get(l)) {
+ l++;
+ }
+ } else if (lv > rv) {
+ result.add(rv);
+ r++;
+ while (r < bSize && rv == b.get(r)) {
+ r++;
+ }
+ } else {
+ result.add(lv);
+ l++;
+ r++;
+
+ while (l < aSize && lv == a.get(l)) {
+ l++;
+ }
+ while (r < bSize && rv == b.get(r)) {
+ r++;
+ }
+ }
+ }
+
+ // add remaining values from list a, if any exist
+ for (; l < aSize; l++) {
+ if (l == 0 || a.get(l) != a.get(l - 1)) {
+ result.add(a.get(l));
+ }
+ }
+
+ // add remaining values from list b, if any exist
+ for (; r < bSize; r++) {
+ if (r == 0 || b.get(r) != b.get(r - 1)) {
+ result.add(b.get(r));
+ }
+ }
+
+ return result;
+ }
+
+ private static LongList unionUnsorted(final LongList a, final LongList b) {
+ final LongList aSorted = new LongList(a);
+ aSorted.parallelSort();
+ final LongList bSorted = new LongList(b);
+ bSorted.parallelSort();
+
+ return unionSorted(aSorted, bSorted);
+ }
+}
diff --git a/primitiveCollections/src/main/java/org/lucares/collections/LongPredicate.java b/primitiveCollections/src/main/java/org/lucares/collections/LongPredicate.java
new file mode 100644
index 0000000..7b47687
--- /dev/null
+++ b/primitiveCollections/src/main/java/org/lucares/collections/LongPredicate.java
@@ -0,0 +1,58 @@
+package org.lucares.collections;
+
+import java.util.function.Predicate;
+
+/**
+ * A {@link Predicate} for primitive longs.
+ */
+@FunctionalInterface
+public interface LongPredicate {
+
+ /**
+ * Evaluates the predicate.
+ *
+ * @param value
+ * the value
+ * @param index
+ * the index in the list
+ * @return {@code true} iff the input argument matches the predicate
+ */
+ boolean test(long value, int index);
+
+ /**
+ * Returns a predicate that represents the logical AND of {@code this} and
+ * another predicate. The and operation is short-circuiting.
+ *
+ * @param other
+ * the other predicate
+ * @return the result of combining both predicates with a logical AND operation
+ * @throws NullPointerException
+ * if {@code other} is null
+ */
+ default LongPredicate and(final LongPredicate other) {
+ return (value, index) -> test(value, index) && other.test(value, index);
+ }
+
+ /**
+ * Returns a predicate that represents the logical OR of {@code this} and
+ * another predicate. The and operation is short-circuiting.
+ *
+ * @param other
+ * the other predicate
+ * @return the result of combining both predicates with a logical OR operation
+ * @throws NullPointerException
+ * if {@code other} is null
+ */
+ default LongPredicate or(final LongPredicate other) {
+ return (value, index) -> test(value, index) || other.test(value, index);
+ }
+
+ /**
+ * Returns a predicate that negates the result of this predicate.
+ *
+ * @return the negation of {@code this}
+ */
+ default LongPredicate negate() {
+ return (value, index) -> !test(value, index);
+ }
+}
diff --git a/primitiveCollections/src/main/java/org/lucares/collections/UnaryLongOperator.java b/primitiveCollections/src/main/java/org/lucares/collections/UnaryLongOperator.java
new file mode 100644
index 0000000..f4f7f5f
--- /dev/null
+++ b/primitiveCollections/src/main/java/org/lucares/collections/UnaryLongOperator.java
@@ -0,0 +1,18 @@
+package org.lucares.collections;
+
+import java.util.function.Function;
+
+/**
+ * Represents an operation that maps a long to a long. This is a specialization
+ * of {@link Function} for a primitive long.
+ */
+public interface UnaryLongOperator {
+ /**
+ * Applies the operation to the integer
+ *
+ * @param value
+ * the input value
+ * @return the result of the operation
+ */
+ long apply(long value);
+}
diff --git a/primitiveCollections/src/test/java/org/lucares/collections/LongListTest.java b/primitiveCollections/src/test/java/org/lucares/collections/LongListTest.java
new file mode 100644
index 0000000..937c148
--- /dev/null
+++ b/primitiveCollections/src/test/java/org/lucares/collections/LongListTest.java
@@ -0,0 +1,1454 @@
+package org.lucares.collections;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.stream.Collectors;
+import java.util.stream.LongStream;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class LongListTest {
+
+ @Test
+ public void testEmptyList() {
+ final LongList list = new LongList();
+
+ Assert.assertEquals(true, list.isEmpty());
+ }
+
+ @Test
+ public void testCopyConstructor() {
+ final LongList list = new LongList();
+ list.addAll(1, 2, 3, 4, 5);
+
+ final LongList copy = new LongList(list);
+
+ Assert.assertArrayEquals(copy.toArray(), list.toArray());
+ }
+
+ @Test
+ public void testInvalidInitialCapacity() {
+ try {
+ new LongList(-1);
+ Assert.fail();
+ } catch (final IllegalArgumentException e) {
+ // expected
+ }
+ }
+
+ @Test
+ public void testAdd() {
+ // setting initial size to one, so that the first add does not need to resize,
+ // but the second add must
+ final LongList list = new LongList(1);
+
+ list.add(1);
+
+ Assert.assertFalse(list.isEmpty());
+ Assert.assertEquals(1, list.size());
+
+ list.add(2);
+ Assert.assertEquals(2, list.size());
+
+ Assert.assertEquals(1, list.get(0));
+ Assert.assertEquals(2, list.get(1));
+ }
+
+ @Test
+ public void testInsert() {
+ final LongList list = new LongList();
+
+ list.insert(0);
+ Assert.assertArrayEquals(new long[] {}, list.toArray());
+
+ list.insert(0, 1);
+ Assert.assertArrayEquals(new long[] { 1 }, list.toArray());
+
+ list.insert(1, 2, 2, 2);
+ Assert.assertArrayEquals(new long[] { 1, 2, 2, 2 }, list.toArray());
+
+ list.insert(1, 3);
+ Assert.assertArrayEquals(new long[] { 1, 3, 2, 2, 2 }, list.toArray());
+
+ list.insert(2, 4, 4, 4);
+ Assert.assertArrayEquals(new long[] { 1, 3, 4, 4, 4, 2, 2, 2 }, list.toArray());
+
+ list.insert(2);
+ Assert.assertArrayEquals(new long[] { 1, 3, 4, 4, 4, 2, 2, 2 }, list.toArray());
+
+ list.insert(8, 5, 5);
+ Assert.assertArrayEquals(new long[] { 1, 3, 4, 4, 4, 2, 2, 2, 5, 5 }, list.toArray());
+ }
+
+ @Test
+ public void testInsertOutOfBounds() {
+ final LongList list = new LongList();
+ try {
+ list.insert(-1, 1);
+ Assert.fail();
+ } catch (final IndexOutOfBoundsException e) {
+ // expected
+ }
+
+ try {
+ list.insert(1, 1);
+ Assert.fail();
+ } catch (final IndexOutOfBoundsException e) {
+ // expected
+ }
+
+ try {
+ list.add(1);
+ list.insert(2, 2);
+ Assert.fail();
+ } catch (final IndexOutOfBoundsException e) {
+ // expected
+ }
+ }
+
+ @Test
+ public void testInsertNull() {
+ final LongList list = new LongList();
+ try {
+ list.insert(0, null);
+ Assert.fail();
+ } catch (final NullPointerException e) {
+ // expected
+ }
+ }
+
+ @Test
+ public void testSet() {
+ final LongList list = new LongList();
+ list.addAll(0, 1, 2, 3, 4, 5);
+
+ list.set(0, 10);
+ Assert.assertArrayEquals(new long[] { 10, 1, 2, 3, 4, 5 }, list.toArray());
+
+ list.set(1, 11);
+ Assert.assertArrayEquals(new long[] { 10, 11, 2, 3, 4, 5 }, list.toArray());
+
+ list.set(5, 55);
+ Assert.assertArrayEquals(new long[] { 10, 11, 2, 3, 4, 55 }, list.toArray());
+ }
+
+ @Test
+ public void testSetOutOfBounds() {
+ final LongList list = new LongList();
+ try {
+ list.set(-1, 1);
+ Assert.fail();
+ } catch (final IndexOutOfBoundsException e) {
+ // expected
+ }
+
+ try {
+ list.set(1, 1);
+ Assert.fail();
+ } catch (final IndexOutOfBoundsException e) {
+ // expected
+ }
+
+ list.addAll(0, 1, 2, 3, 4, 5);
+
+ try {
+ list.set(6, 1);
+ Assert.fail();
+ } catch (final IndexOutOfBoundsException e) {
+ // expected
+ }
+ }
+
+ @Test
+ public void testGrow() {
+ final LongList list = new LongList(1);
+
+ final int size = 100;
+ for (int i = 0; i < size; i++) {
+ list.add(i);
+ }
+ Assert.assertEquals(size, list.size());
+
+ for (int i = 0; i < size; i++) {
+ Assert.assertEquals(i, list.get(i));
+ }
+ }
+
+ @Test
+ public void testGrowLongListOfCapacityNull() {
+ final LongList list = new LongList(0);
+
+ final int size = 100;
+ for (int i = 0; i < size; i++) {
+ list.add(i);
+ }
+ Assert.assertEquals(size, list.size());
+
+ for (int i = 0; i < size; i++) {
+ Assert.assertEquals(i, list.get(i));
+ }
+ }
+
+ @Test
+ public void testAddArray() {
+ final LongList list = new LongList();
+
+ list.addAll();
+ Assert.assertTrue(list.isEmpty());
+ Assert.assertEquals(0, list.size());
+
+ final int size = 100;
+ final long[] longs = ThreadLocalRandom.current().longs(size).toArray();
+
+ list.addAll(longs);
+ Assert.assertEquals(size, list.size());
+
+ for (int i = 0; i < size; i++) {
+ Assert.assertEquals(longs[i], list.get(i));
+ }
+
+ final long[] anotherLongs = ThreadLocalRandom.current().longs(size).toArray();
+ list.addAll(anotherLongs);
+ Assert.assertEquals(size * 2, list.size());
+ for (int i = 0; i < size; i++) {
+ Assert.assertEquals(anotherLongs[i], list.get(size + i));
+ }
+ }
+
+ @Test
+ public void testAddList() {
+ final LongList list = new LongList();
+
+ // adding empty list with no capacity
+ list.addAll(LongList.of());
+ Assert.assertEquals(new LongList(), list);
+ Assert.assertTrue(list.isSorted());
+
+ // adding empty list with capacity 2
+ list.addAll(new LongList(2));
+ Assert.assertEquals(new LongList(), list);
+ Assert.assertTrue(list.isSorted());
+
+ // adding sorted list to an empty list
+ list.addAll(LongList.of(1, 2, 3));
+ Assert.assertEquals(LongList.of(1, 2, 3), list);
+ Assert.assertTrue(list.isSorted());
+
+ // add empty list to a sorted list
+ list.addAll(LongList.of());
+ Assert.assertEquals(LongList.of(1, 2, 3), list);
+ Assert.assertTrue(list.isSorted());
+
+ // adding sorted list to a sorted list so that the list stays sorted
+ list.addAll(LongList.of(3, 4, 5));
+ Assert.assertEquals(LongList.of(1, 2, 3, 3, 4, 5), list);
+ Assert.assertTrue(list.isSorted());
+
+ // adding sorted list to a sorted list, but the new list is not sorted
+ list.clear();
+ list.addAll(LongList.of(1, 2, 3));
+ list.addAll(LongList.of(0));
+ Assert.assertEquals(LongList.of(1, 2, 3, 0), list);
+ Assert.assertFalse(list.isSorted());
+
+ // adding unsorted list to a sorted list
+ list.clear();
+ list.addAll(LongList.of(1, 2, 3));
+ list.addAll(LongList.of(6, 5, 4));
+ Assert.assertEquals(LongList.of(1, 2, 3, 6, 5, 4), list);
+ Assert.assertFalse(list.isSorted());
+
+ // adding unsorted list to an empty list
+ list.clear();
+ list.addAll(LongList.of(3, 2, 1));
+ Assert.assertEquals(LongList.of(3, 2, 1), list);
+ Assert.assertFalse(list.isSorted());
+
+ // adding sorted list to an unsorted list
+ list.clear();
+ list.addAll(LongList.of(3, 2, 1));
+ list.addAll(LongList.of(1, 2, 3));
+ Assert.assertEquals(LongList.of(3, 2, 1, 1, 2, 3), list);
+ Assert.assertFalse(list.isSorted());
+
+ }
+
+ @Test
+ public void testGetArray() {
+ final LongList list = new LongList();
+
+ final int size = 100;
+ final long[] longs = ThreadLocalRandom.current().longs(size).toArray();
+
+ list.addAll(longs);
+
+ final int length = 20;
+ final int from = 10;
+ final long[] actualLongs = list.get(from, length);
+
+ for (int i = 0; i < length; i++) {
+ Assert.assertEquals(actualLongs[i], list.get(from + i));
+ Assert.assertEquals(actualLongs[i], longs[from + i]);
+ }
+ }
+
+ @Test
+ public void testGetOutOfBounds() {
+ final LongList list = new LongList();
+
+ list.addAll(0, 1, 2, 3);
+
+ try {
+ list.get(-1);
+ Assert.fail();
+ } catch (final IndexOutOfBoundsException e) {
+ // expected
+ }
+ try {
+ list.get(4);
+ Assert.fail();
+ } catch (final IndexOutOfBoundsException e) {
+ // expected
+ }
+ }
+
+ @Test
+ public void testGetArrayOutOfBounds() {
+ final LongList list = new LongList();
+
+ list.addAll(0, 1, 2, 3);
+
+ try {
+ list.get(-1, 1);
+ Assert.fail();
+ } catch (final IndexOutOfBoundsException e) {
+ // expected
+ }
+ try {
+ list.get(1, -1);
+ Assert.fail();
+ } catch (final IndexOutOfBoundsException e) {
+ // expected
+ }
+ try {
+ list.get(3, 2);
+ Assert.fail();
+ } catch (final IndexOutOfBoundsException e) {
+ // expected
+ }
+ try {
+ list.get(4, 1);
+ Assert.fail();
+ } catch (final IndexOutOfBoundsException e) {
+ // expected
+ }
+ }
+
+ @Test
+ public void testToArrayEmptyLIst() {
+ final LongList list = new LongList();
+
+ final long[] actual = list.toArray();
+ Assert.assertArrayEquals(actual, new long[0]);
+ }
+
+ @Test
+ public void testToArray() {
+ final LongList list = new LongList();
+ list.addAll(1, 2, 3, 4, 5, 6);
+
+ {
+ final long[] input = new long[1];
+ final long[] actual = list.toArray(input);
+ // input is too short -> new array returned
+ Assert.assertNotSame(input, actual);
+ Assert.assertArrayEquals(list.toArray(), actual);
+ }
+
+ {
+ final long[] input = new long[list.size()];
+ final long[] actual = list.toArray(input);
+ // input fits exactly -> input returned
+ Assert.assertSame(input, actual);
+ Assert.assertArrayEquals(list.toArray(), actual);
+ }
+
+ {
+ final long[] input = new long[list.size() + 1];
+ final long[] expected = { 1, 2, 3, 4, 5, 6, 0 };
+ final long[] actual = list.toArray(input);
+ // input too big -> input returned
+ Assert.assertSame(input, actual);
+ Assert.assertArrayEquals(expected, actual);
+ }
+ }
+
+ @Test
+ public void testToArrayWithEmptyList() {
+ final LongList list = new LongList();
+
+ {
+ final long[] input = new long[0];
+ final long[] actual = list.toArray(input);
+ // input fits exactly -> input returned
+ Assert.assertSame(input, actual);
+ Assert.assertArrayEquals(list.toArray(), actual);
+ }
+
+ {
+ final long[] input = new long[list.size() + 1];
+ final long[] expected = { 0 };
+ final long[] actual = list.toArray(input);
+ // input too big -> input returned
+ Assert.assertSame(input, actual);
+ Assert.assertArrayEquals(expected, actual);
+ }
+ }
+
+ @Test
+ public void testRemove() {
+ final LongList list = new LongList();
+
+ final int size = 10;
+ final long[] ints = new long[size];
+ for (int i = 0; i < size; i++) {
+ ints[i] = i;
+ }
+
+ list.addAll(ints);
+
+ // remove nothing
+ list.remove(2, 2);
+ Assert.assertArrayEquals(ints, list.toArray());
+
+ // remove the last element
+ list.remove(9, 10);
+ final long[] expectedA = removeElements(ints, 9);
+ Assert.assertArrayEquals(expectedA, list.toArray());
+
+ // remove the first element
+ list.remove(0, 1);
+ final long[] expectedB = removeElements(expectedA, 0);
+ Assert.assertArrayEquals(expectedB, list.toArray());
+
+ // remove single element in the middle
+ list.remove(3, 4);
+ final long[] expectedC = removeElements(expectedB, 3);
+ Assert.assertArrayEquals(expectedC, list.toArray());
+
+ // remove several elements in the middle
+ list.remove(2, 5);
+ final long[] expectedD = removeElements(expectedC, 2, 3, 4);
+ Assert.assertArrayEquals(expectedD, list.toArray());
+
+ // remove all elements
+ list.remove(0, 4);
+ final long[] expectedE = removeElements(expectedD, 0, 1, 2, 3);
+ Assert.assertArrayEquals(expectedE, list.toArray());
+ }
+
+ @Test
+ public void testRemoveOutOfRange() {
+ final LongList list = new LongList();
+
+ list.addAll(0, 1, 2, 3);
+
+ try {
+ list.remove(-1, 1);
+ Assert.fail();
+ } catch (final IndexOutOfBoundsException e) {
+ // expected
+ }
+ try {
+ list.remove(1, 0);
+ Assert.fail();
+ } catch (final IndexOutOfBoundsException e) {
+ // expected
+ }
+ try {
+ list.remove(3, 5);
+ Assert.fail();
+ } catch (final IndexOutOfBoundsException e) {
+ // expected
+ }
+ try {
+ list.remove(4, 5);
+ Assert.fail();
+ } catch (final IndexOutOfBoundsException e) {
+ // expected
+ }
+ }
+
+ @Test
+ public void testTrim() {
+ final LongList list = new LongList();
+
+ list.addAll(0, 1, 2, 3);
+
+ list.trim();
+ Assert.assertEquals(4, list.getCapacity());
+ }
+
+ @Test
+ public void testTrimOnEmptyList() {
+ final LongList list = new LongList();
+
+ list.trim();
+ Assert.assertEquals(0, list.getCapacity());
+ }
+
+ @Test
+ public void testClear() {
+ final LongList list = LongList.of(2, 0, 1); // unsorted list
+ list.clear();
+
+ Assert.assertEquals(0, list.size());
+ Assert.assertEquals(0, list.getCapacity());
+ Assert.assertTrue(list.isSorted());
+ }
+
+ @SuppressWarnings("unlikely-arg-type")
+ @Test
+ public void testHashCodeEquals() {
+ final LongList a = new LongList();
+ final LongList b = new LongList();
+
+ // on empty lists
+ Assert.assertTrue(a.equals(b));
+ Assert.assertEquals(a.hashCode(), b.hashCode());
+
+ // on equal lists
+ a.addAll(1, 2, 3, 4);
+ b.addAll(1, 2, 3, 4);
+ Assert.assertTrue(a.equals(b));
+ Assert.assertEquals(a.hashCode(), b.hashCode());
+
+ // trim only one of them
+ // we want to compare the actual content
+ a.trim();
+ Assert.assertTrue(a.equals(b));
+ Assert.assertEquals(a.hashCode(), b.hashCode());
+
+ // change one value
+ a.remove(2, 3);
+ a.insert(2, 99);
+ Assert.assertFalse(a.equals(b));
+ Assert.assertNotEquals(a.hashCode(), b.hashCode());
+
+ // have different length
+ a.add(66);
+ Assert.assertFalse(a.equals(b));
+ Assert.assertNotEquals(a.hashCode(), b.hashCode());
+
+ // same object
+ Assert.assertTrue(a.equals(a));
+
+ // other is null
+ Assert.assertFalse(a.equals(null));
+
+ // equals with a different class
+ Assert.assertFalse(a.equals("not an LongList"));
+
+ // sorted and unsorted lists with same length
+ final LongList sorted = LongList.of(1, 2, 3, 4);
+ final LongList unsorted = LongList.of(4, 3, 2, 1);
+ Assert.assertFalse(sorted.equals(unsorted));
+ Assert.assertNotEquals(sorted.hashCode(), unsorted.hashCode());
+ }
+
+ @Test
+ public void testSort() {
+ final LongList list = new LongList();
+
+ list.addAll(4, 2, 3, 1);
+ list.sort();
+
+ final long[] expected = new long[] { 1, 2, 3, 4 };
+ Assert.assertArrayEquals(expected, list.toArray());
+ }
+
+ @Test
+ public void testSortParallel() {
+ final LongList list = new LongList();
+
+ list.addAll(4, 2, 3, 1);
+ list.parallelSort();
+
+ final long[] expected = new long[] { 1, 2, 3, 4 };
+ Assert.assertArrayEquals(expected, list.toArray());
+ }
+
+ @Test
+ public void testShuffle() {
+ final LongList list = LongList.of(0, 1, 2, 3, 4, 5, 6, 7, 8, 9);
+ final LongList original = list.clone();
+ final LongList original2 = list.clone();
+
+ final Random random = new Random(1);
+ list.shuffle(random);
+
+ Assert.assertEquals(original.size(), list.size());
+
+ original.removeAll(list);
+ Assert.assertTrue("original minus shuffled list: " + original, original.isEmpty());
+
+ list.removeAll(original2);
+ Assert.assertTrue("shuffled list minus original: " + list, list.isEmpty());
+ }
+
+ @Test
+ public void testShuffle_sortOfEmptyElementList() {
+ final LongList list = LongList.of();
+ list.shuffle();
+
+ Assert.assertTrue("an empty list is sorted", list.isSorted());
+ }
+
+ @Test
+ public void testShuffle_sortOfOneElementList() {
+ final LongList list = LongList.of(1);
+ list.shuffle();
+
+ Assert.assertTrue("a shuffled list of size 1 is sorted", list.isSorted());
+ }
+
+ @Test
+ public void testShuffle_sortOfListWithIdenticalElements() {
+ final LongList list = LongList.of(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1);
+ list.shuffle();
+
+ Assert.assertTrue("a shuffled list with identical elements is sorted", list.isSorted());
+ }
+
+ @Test
+ public void testShuffle_sortOfThreeElementList() {
+ final LongList list = LongList.of(2, 3, 1);
+
+ int sorted = 0;
+ int unsorted = 0;
+ for (int i = 0; i < 20; i++) {
+ final Random random = new Random(i);
+ list.shuffle(random);
+
+ sorted += LongList.of(1, 2, 3).equals(list) ? 1 : 0;
+ unsorted += LongList.of(1, 2, 3).equals(list) ? 0 : 1;
+
+ Assert.assertEquals(LongList.of(1, 2, 3).equals(list), list.isSorted());
+ }
+
+ Assert.assertTrue(sorted > 0);
+ Assert.assertTrue(unsorted > 0);
+ }
+
+ private long[] removeElements(final long[] data, final int... removedIndices) {
+ final long[] result = new long[data.length - removedIndices.length];
+ final List