org.conqat.lib.commons.collections.PairList Maven / Gradle / Ivy
Show all versions of teamscale-lib-commons Show documentation
/*
* Copyright (c) CQSE GmbH
*
* Licensed 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
*
* http://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.conqat.lib.commons.collections;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.BinaryOperator;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import org.conqat.lib.commons.assertion.CCSMAssert;
import org.conqat.lib.commons.function.BiFunctionWithException;
import org.conqat.lib.commons.function.FunctionWithException;
import org.jetbrains.annotations.Contract;
import com.fasterxml.jackson.annotation.JsonGetter;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting;
/**
* A list for storing pairs in a specific order.
*/
public class PairList implements Serializable, Iterable> {
private static final long serialVersionUID = 1;
/** The JSON property name of {@link #firstElements}. */
private static final String FIRST_ELEMENTS_PROPERTY_NAME = "firstElements";
/** The JSON property name of {@link #secondElements}. */
private static final String SECOND_ELEMENTS_PROPERTY_NAME = "secondElements";
/** The current size. */
@JsonProperty("size")
private int size = 0;
/** The array used for storing the S. */
// Please note that the actual JSON serialization
// uses the result of #getFirstList
@JsonProperty(FIRST_ELEMENTS_PROPERTY_NAME)
// Same as e.g. ArrayList this instance is only Serializable if the actual
// elements are all Serializable
@SuppressWarnings("NonSerializableFieldInSerializableClass")
private Object[] firstElements;
/** The array used for storing the T. */
// Please note that the actual JSON serialization
// uses the result of #getSecondList
@JsonProperty(SECOND_ELEMENTS_PROPERTY_NAME)
// Same as e.g. ArrayList this instance is only Serializable if the actual
// elements are all Serializable
@SuppressWarnings("NonSerializableFieldInSerializableClass")
private Object[] secondElements;
public PairList() {
this(16);
}
/** Constructor. */
public PairList(int initialCapacity) {
if (initialCapacity < 1) {
initialCapacity = 1;
}
firstElements = new Object[initialCapacity];
secondElements = new Object[initialCapacity];
}
/** Copy constructor. */
public PairList(PairList other) {
this(other.size);
addAll(other);
}
/**
* Constructor to convert a map into a pair list.
*/
public PairList(Map map) {
this(map.size());
for (Entry entry : map.entrySet()) {
add(entry.getKey(), entry.getValue());
}
}
/**
* Creates a new pair list initialized with a single key/value pair. This is especially helpful for
* construction of small pair lists, as type inference reduces writing overhead.
*/
public static PairList from(S key, T value) {
PairList result = new PairList<>(1);
result.add(key, value);
return result;
}
/**
* Creates a new {@link PairList} from given {@link Pair}s.
*/
@SafeVarargs
public static PairList fromPairs(Pair pair, Pair... additionalPairs) {
PairList result = new PairList<>(1 + additionalPairs.length);
result.add(pair.getFirst(), pair.getSecond());
for (Pair additionalPair : additionalPairs) {
result.add(additionalPair.getFirst(), additionalPair.getSecond());
}
return result;
}
/** Creates a new {@link PairList} from the given {@link Map}. */
public static PairList fromMap(Map map) {
PairList pairList = new PairList<>(map.size());
map.forEach(pairList::add);
return pairList;
}
/** Returns whether the list is empty. */
public boolean isEmpty() {
return size == 0;
}
/** Returns the size of the list. */
public int size() {
return size;
}
/** Add the given pair to the list. */
public void add(S first, T second) {
ensureSpace(size + 1);
firstElements[size] = first;
secondElements[size] = second;
++size;
}
/** Add the given pair to the list. */
public void add(Pair pair) {
add(pair.getFirst(), pair.getSecond());
}
/**
* Inserts the given element at the given index, shifting all other elements. Note that this
* operation is liner in the length of this list.
*/
public void insert(int index, S first, T second) {
ensureSpace(size + 1);
for (int i = size; i > index; --i) {
firstElements[i] = firstElements[i - 1];
secondElements[i] = secondElements[i - 1];
}
++size;
firstElements[index] = first;
secondElements[index] = second;
}
/** Adds all pairs from another list. */
public void addAll(PairList other) {
// we have to store this in a local var, as other.size may change if
// other == this
int otherSize = other.size;
ensureSpace(size + otherSize);
for (int i = 0; i < otherSize; ++i) {
firstElements[size] = other.firstElements[i];
secondElements[size] = other.secondElements[i];
++size;
}
}
/**
* Add the product of {@code firstElement} and with all the elements from {@code secondElements}.
*/
public void addAll(S firstElement, Iterable secondElements) {
for (T secondElement : secondElements) {
add(firstElement, secondElement);
}
}
/** Make sure there is space for at least the given amount of elements. */
private void ensureSpace(int space) {
if (space <= firstElements.length) {
return;
}
Object[] oldFirst = firstElements;
Object[] oldSecond = secondElements;
int newSize = firstElements.length * 2;
while (newSize < space) {
newSize *= 2;
}
firstElements = new Object[newSize];
secondElements = new Object[newSize];
System.arraycopy(oldFirst, 0, firstElements, 0, size);
System.arraycopy(oldSecond, 0, secondElements, 0, size);
}
/** Returns the first element at given index. */
@SuppressWarnings("unchecked")
public S getFirst(int i) {
checkWithinBounds(i);
return (S) firstElements[i];
}
/**
* Checks whether the given i
is within the bounds. Throws an exception otherwise.
*/
private void checkWithinBounds(int i) {
if (i < 0 || i >= size) {
throw new IndexOutOfBoundsException("Out of bounds: " + i);
}
}
/** Sets the first element at given index. */
public void setFirst(int i, S value) {
checkWithinBounds(i);
firstElements[i] = value;
}
/** Returns the second element at given index. */
@SuppressWarnings("unchecked")
public T getSecond(int i) {
checkWithinBounds(i);
return (T) secondElements[i];
}
/** Sets the first element at given index. */
public void setSecond(int i, T value) {
checkWithinBounds(i);
secondElements[i] = value;
}
/** Creates a new list containing all first elements. */
@SuppressWarnings("unchecked")
public List extractFirstList() {
List result = new ArrayList<>(size + 1);
for (int i = 0; i < size; ++i) {
result.add((S) firstElements[i]);
}
return result;
}
/** Returns a list view of the first elements backed directly by the array. */
// Use this getter for actual JSON serialization, so that no trailing null
// elements of the array are serialized (when firstElements.length > size)
@JsonGetter(FIRST_ELEMENTS_PROPERTY_NAME)
@SuppressWarnings("unchecked")
public UnmodifiableList getFirstList() {
return (UnmodifiableList) CollectionUtils.asUnmodifiable(Arrays.asList(firstElements).subList(0, size));
}
/** Creates a new list containing all second elements. */
public List extractSecondList() {
return extractSecondElementsFilteredByFirst(x -> true);
}
/** Returns a list view of the second elements backed directly by the array. */
// Use this getter for actual JSON serialization, so that no trailing null
// elements of the array are serialized (when secondElements.length > size)
@JsonGetter(SECOND_ELEMENTS_PROPERTY_NAME)
@SuppressWarnings("unchecked")
public UnmodifiableList getSecondList() {
return (UnmodifiableList) CollectionUtils.asUnmodifiable(Arrays.asList(secondElements).subList(0, size));
}
/**
* Creates a new list containing all second elements where the corresponding first element is
* matched by the predicate.
*/
@SuppressWarnings("unchecked")
public List extractSecondElementsFilteredByFirst(Predicate predicate) {
List result = new ArrayList<>();
for (int i = 0; i < size; ++i) {
if (predicate.test((S) firstElements[i])) {
result.add((T) secondElements[i]);
}
}
return result;
}
/** Swaps the entries located at indexes i and j. */
public void swapEntries(int i, int j) {
S tmp1 = getFirst(i);
T tmp2 = getSecond(i);
setFirst(i, getFirst(j));
setSecond(i, getSecond(j));
setFirst(j, tmp1);
setSecond(j, tmp2);
}
/** Clears this list. */
public void clear() {
size = 0;
}
/** Removes the last element of the list. */
public void removeLast() {
CCSMAssert.isTrue(size > 0, "Size must be positive!");
size -= 1;
// clear elements to allow GC
firstElements[size] = null;
secondElements[size] = null;
}
/**
* Removes the element at given index. Note that this operation is liner in the length of this list.
*/
public void remove(int index) {
for (int i = index + 1; i < size; ++i) {
firstElements[i - 1] = firstElements[i];
secondElements[i - 1] = secondElements[i];
}
removeLast();
}
@Override
public String toString() {
StringBuilder result = new StringBuilder();
result.append('[');
for (int i = 0; i < size; i++) {
if (i != 0) {
result.append(',');
}
result.append('(');
result.append(firstElements[i]);
result.append(',');
result.append(secondElements[i]);
result.append(')');
}
result.append(']');
return result.toString();
}
/** {@inheritDoc} */
@Override
public int hashCode() {
int prime = 31;
int hash = size;
hash = prime * hash + hashArrayUntil(firstElements, size);
return prime * hash + hashArrayUntil(secondElements, size);
}
/**
* Generates a hash code for part of an array. The hash code is calculated from the hash codes of
* the given objects. Only objects that lie within index 0 and end index in the array are considered
* (so we ignore the {@link #capacity()}).
*
* This method offers a better performance than creating a copy of the array that only contains the
* desired elements and then calling {@link Objects#hashCode(Object)}.
*/
private static int hashArrayUntil(Object[] objects, int endExclusive) {
int hashCode = 1;
for (int i = 0; i < endExclusive; i++) {
hashCode = hashCode * 31 + Objects.hashCode(objects[i]);
}
return hashCode;
}
/**
* Checks if the two given arrays match until the given {@code endExclusive} (which can be used to
* ignore the {@link #capacity()}).
*/
private static boolean arrayEqualsUntil(Object[] elements, Object[] otherElements, int endExclusive) {
for (int i = 0; i < endExclusive; i++) {
if (!Objects.equals(elements[i], otherElements[i])) {
return false;
}
}
return true;
}
/** {@inheritDoc} */
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof PairList)) {
return false;
}
PairList other = (PairList) obj;
if (size != other.size) {
return false;
}
return arrayEqualsUntil(firstElements, other.firstElements, size)
&& arrayEqualsUntil(secondElements, other.secondElements, size);
}
/** {@inheritDoc} */
@Override
public Iterator> iterator() {
return new Iterator>() {
private int index = 0;
/** {@inheritDoc} */
@Override
public boolean hasNext() {
return index < size;
}
/** {@inheritDoc} */
@Override
public Pair next() {
checkWithinBounds(index);
int oldIndex = index;
index++;
return createPairForIndex(oldIndex);
}
};
}
/**
* Returns a non-parallel stream of {@link Pair}s from this list.
*
* NOTE: the underlying {@link Spliterator} implementation does currently not support splitting.
* Thus, trying to use the returned stream in parallel execution will fail.
*
* @see PairListSpliterator#trySplit()
*/
public Stream> stream() {
return StreamSupport.stream(new PairListSpliterator(), false);
}
/**
* Converts this {@link PairList} into a {@link Map}.
*
* If the first elements contain duplicates (according to {@link Object#equals(Object)}), an
* {@link IllegalStateException} is thrown. If the first elements may have duplicates, use
* {@link #toMap(BinaryOperator)} instead.
*/
public Map toMap() {
Map result = new HashMap<>();
// must use explicit put, as stream collector does not support null
// values
forEach(result::put);
return result;
}
/**
* Converts this {@link PairList} into a {@link Map}.
*
* If the first elements contain duplicates (according to {@link Object#equals(Object)}), the
* respective second elements are merged using the provided merging function.
*/
public Map toMap(BinaryOperator mergeFunction) {
return stream().collect(Collectors.toMap(Pair::getFirst, Pair::getSecond, mergeFunction));
}
/**
* Converts this {@link PairList} into a {@link Map} with the {@link #getFirst(int) first} elements
* as keys and the {@link #getSecond(int) second} elements combined into a {@link List}.
*/
public Map> groupedByFirst() {
return groupedByFirst(Collectors.toList());
}
/**
* Converts this {@link PairList} into a {@link Map} with the {@link #getFirst(int) first} elements
* as keys and the {@link #getSecond(int) second} combined by the provided {@code downstream}.
*/
public Map groupedByFirst(Collector downstream) {
return stream().collect(Collectors.groupingBy(Pair::getFirst, Collectors.mapping(Pair::getSecond, downstream)));
}
/**
* Creates a pair from the values at the given index in this list.
*
* We suppress unchecked cast warnings since the PairList stores all elements as plain Objects.
*/
@SuppressWarnings("unchecked")
private Pair createPairForIndex(int index) {
return new Pair<>((S) firstElements[index], (T) secondElements[index]);
}
/**
* Spliterator for the {@link PairList}.
*/
private final class PairListSpliterator implements Spliterator> {
/**
* The next element to process when {@link #tryAdvance(Consumer)} is called.
*/
@JsonProperty("nextElementToProcess")
private int nextElementToProcess;
@Override
public int characteristics() {
// the Spliterator is not sub-sized since we don't support splitting
// at the moment
return Spliterator.IMMUTABLE | Spliterator.ORDERED | Spliterator.SIZED;
}
@Override
public long estimateSize() {
return size - nextElementToProcess;
}
@Override
public boolean tryAdvance(Consumer> action) {
if (nextElementToProcess == size) {
return false;
}
action.accept(createPairForIndex(nextElementToProcess));
nextElementToProcess++;
return true;
}
/**
* {@inheritDoc}
*
* We return null
to indicate that this {@link Spliterator} cannot be split. If
* parallel execution is required, this method must be implemented and the sub-sized characteristic
* should be adhered to.
*/
@Override
public Spliterator> trySplit() {
return null;
}
}
/**
* Collects a stream of elements into a {@link PairList}.
*
* @param
* stream element type
* @param
* {@link PairList#getFirst(int) first} {@link PairList} element type
* @param
* {@link PairList#getSecond(int) second} {@link PairList} element type
*/
private static class PairListCollector implements Collector, PairList> {
/**
* Maps the element to the {@link PairList#getFirst(int) first} entry.
*/
private final Function firstMapper;
/**
* Maps the element to the {@link PairList#getSecond(int) second} entry.
*/
private final Function secondMapper;
private PairListCollector(Function firstMapper, Function secondMapper) {
CCSMAssert.isNotNull(firstMapper, () -> String.format("Expected \"%s\" to be not null", "firstMapper"));
CCSMAssert.isNotNull(secondMapper, () -> String.format("Expected \"%s\" to be not null", "secondMapper"));
this.firstMapper = firstMapper;
this.secondMapper = secondMapper;
}
@Override
public Supplier> supplier() {
return PairList::new;
}
@Override
public BiConsumer, I> accumulator() {
return (list, value) -> list.add(firstMapper.apply(value), secondMapper.apply(value));
}
@Override
public BinaryOperator> combiner() {
return (list1, list2) -> {
list1.addAll(list2);
return list1;
};
}
@Override
public Function, PairList> finisher() {
return list -> list;
}
@Override
public Set characteristics() {
return EnumSet.of(Characteristics.IDENTITY_FINISH);
}
}
/**
* Returns a collector for collecting a stream of {@link Pair}s into a {@link PairList}.
*/
public static Collector, ?, PairList> toPairList() {
return new PairListCollector<>(Pair::getFirst, Pair::getSecond);
}
/**
* Returns a collector for collecting a stream of {@link PairList}s into a single {@link PairList}.
*/
public static Collector, ?, PairList> combine() {
return Collector.of(PairList::new, PairList::addAll, (p1, p2) -> {
p1.addAll(p2);
return p1;
});
}
/**
* Returns a collector for collecting a stream of elements into a {@link PairList}.
*
* @param firstMapper
* Maps an element of the stream to the {@link PairList }s
* {@link PairList#setFirst(int, Object) first} element type
* @param secondMapper
* Maps an element of the stream to the {@link PairList }s
* {@link PairList#setSecond(int, Object) second} element type
*/
public static Collector> toPairList(Function firstMapper,
Function secondMapper) {
return new PairListCollector<>(firstMapper, secondMapper);
}
/**
* Creates a {@link PairList} that consists of pairs of values taken from the two {@link List}s.
* I.e. entry i in the resulting {@link PairList} will consists of the pair (a, b) where a is the
* entry at index i in the first collection and b is the entry at index i of the second collection.
*
* Both collections must be of the same size. The order of insertion into the new {@link PairList}
* is determined by the order imposed by the collections' iterators.
*/
public static PairList zip(List firstValues, List secondValues) {
CCSMAssert.isTrue(firstValues.size() == secondValues.size(),
"Can only zip together collections of the same size.");
PairList result = new PairList<>(firstValues.size());
Iterator firstIterator = firstValues.iterator();
Iterator secondIterator = secondValues.iterator();
while (firstIterator.hasNext()) {
result.add(firstIterator.next(), secondIterator.next());
}
return result;
}
/**
* For each element pair in this list, calls the given consumer with the first and second value from
* the pair as the only arguments.
*/
public void forEach(BiConsumer consumer) {
for (int i = 0; i < size; i++) {
consumer.accept(getFirst(i), getSecond(i));
}
}
/**
* Returns a new {@link PairList}, where both mappers are applied to the elements of the current
* {@link PairList} (input for each mapper is the pair of elements at each position).
*/
public PairList map(BiFunction firstMapper, BiFunction secondMapper) {
PairList result = new PairList<>(size);
forEach((key, value) -> result.add(firstMapper.apply(key, value), secondMapper.apply(key, value)));
return result;
}
/**
* Produce a {@link List} of the given {@link PairList} using the given mapper function.
*/
public List map(BiFunctionWithException mapper) throws E {
List result = new ArrayList<>(size());
for (int i = 0; i < size; i++) {
result.add(mapper.apply(getFirst(i), getSecond(i)));
}
return result;
}
/**
* Returns a new {@link PairList}, where the first elements are obtained by applying the given
* mapper function to the first elements of this list.
*/
public PairList mapFirst(FunctionWithException mapper) throws E {
PairList mappedList = new PairList<>(size);
for (int i = 0; i < size; i++) {
mappedList.add(mapper.apply(getFirst(i)), getSecond(i));
}
return mappedList;
}
/**
* Returns a new {@link PairList}, where the second elements are obtained by applying the given
* mapper function to the second elements of this list.
*/
public PairList mapSecond(FunctionWithException mapper) throws E {
PairList mappedList = new PairList<>(size);
for (int i = 0; i < size; i++) {
mappedList.add(getFirst(i), mapper.apply(getSecond(i)));
}
return mappedList;
}
/**
* Filters the pair list by testing all items against the predicate and returning a pair list of
* those for which it returns true. This method does not modify the original pair list.
*/
public PairList filter(BiFunction filterPredicate) {
PairList filteredList = new PairList<>(size);
for (int i = 0; i < size; i++) {
if (filterPredicate.apply(getFirst(i), getSecond(i))) {
filteredList.add(getFirst(i), getSecond(i));
}
}
return filteredList;
}
/**
* Tests all the items against the predicate and returns true, if any of the items matched the
* predicate. Otherwise, returns false.
*/
public boolean anyMatch(BiFunction predicate) {
for (int i = 0; i < size; i++) {
if (predicate.apply(getFirst(i), getSecond(i))) {
return true;
}
}
return false;
}
/** Returns a reversed copy of this list. */
@Contract(value = "-> new", pure = true)
public PairList reversed() {
PairList reversed = new PairList<>(size());
for (int i = size() - 1; i >= 0; i--) {
reversed.add(getFirst(i), getSecond(i));
}
return reversed;
}
/**
* Returns an inversed copy of this list, i.e. a list where keys and values are switched.
*/
@Contract(value = "-> new", pure = true)
public PairList inversed() {
PairList inverse = new PairList<>(size());
for (int i = size() - 1; i >= 0; i--) {
inverse.add(getSecond(i), getFirst(i));
}
return inverse;
}
/**
* Returns a newly allocated array containing all the elements in this PairList as an array of
* Pairs.
*/
@SuppressWarnings("unchecked")
public Pair[] toArray() {
Pair[] result = new Pair[size];
for (int i = 0; i < size; i++) {
result[i] = new Pair<>((S) firstElements[i], (T) secondElements[i]);
}
return result;
}
/**
* Returns a newly allocated list containing all the elements in this PairList as a list of
* Pairs.
*/
public List> toList() {
return toList(Pair::new);
}
/**
* Returns a newly allocated list containing all the elements in this PairList as a list of entries
* that are created by the given entryFactory.
*/
public List toList(BiFunction entryFactory) {
List result = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
result.add(entryFactory.apply((S) firstElements[i], (T) secondElements[i]));
}
return result;
}
/**
* Sorts the PairList according to the given comparator. Returns a reference to itself, for
* convenience.
*/
public void sort(Comparator> comparator) {
Pair[] sorted = toArray();
Arrays.sort(sorted, comparator);
for (int i = 0; i < sorted.length; i++) {
setFirst(i, sorted[i].getFirst());
setSecond(i, sorted[i].getSecond());
}
}
/**
* Returns a new {@link PairList} with all elements from both lists in order.
*/
public static PairList concatenate(PairList list1, PairList list2) {
PairList result = new PairList<>(list1.size() + list2.size());
result.addAll(list1);
result.addAll(list2);
return result;
}
/**
* The empty list (immutable). This list is serializable.
*/
@SuppressWarnings("rawtypes")
private static final PairList EMPTY_PAIR_LIST = new PairList