org.conqat.lib.commons.collections.CounterSet 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.PrintWriter;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.function.ToIntFunction;
import java.util.stream.Collector;
import org.conqat.lib.commons.assertion.CCSMAssert;
import org.conqat.lib.commons.test.IndexValueClass;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* This class manages a set of counters (i.e. is a mapping from some key objects to integers). As
* the implementation is based on hash maps, key objects must provide suitable hash keys.
*/
@IndexValueClass(containedInBackup = true)
public class CounterSet implements Serializable, Iterable> {
/**
* The empty counter set (immutable).
*
* @see #empty()
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
private static final CounterSet EMPTY = new CounterSet(Collections.emptyMap());
/**
* Returns an empty counter set (immutable).
*
* @see #EMPTY
*/
@SuppressWarnings("unchecked")
public static CounterSet empty() {
return EMPTY;
}
/** Version used for serialization. */
private static final long serialVersionUID = 1;
/**
* The property name of the map field.
*
* Used to identify CounterSets in the {@literal MetricArrayDeserializer}.
*/
public static final String MAP_PROPERTY = "map";
/**
* The property name of the total field.
*
* Used to identify CounterSets in the {@literal MetricArrayDeserializer}.
*/
public static final String TOTAL_PROPERTY = "total";
/** The underlying map. */
@JsonProperty(MAP_PROPERTY)
protected final Map map;
/** Stores total value. */
@JsonProperty(TOTAL_PROPERTY)
protected int total = 0;
/**
* Constructs an empty {@link CounterSet} with the provided {@code map} as backing map.
*/
private CounterSet(Map map) {
this.map = map;
}
/** Constructs an empty {@link CounterSet}. */
public CounterSet() {
this(new LinkedHashMap<>());
}
/**
* Constructs a new {@link CounterSet} from the given keys. Initializes all keys with 1.
*/
public CounterSet(Collection keys) {
this();
incAll(keys);
}
/** Constructs a CounterSet with one value. */
public CounterSet(E key, int value) {
this();
inc(key, value);
}
/**
* Constructs a new {@link CounterSet} for the provided {@code enumClass}.
*
* @param enumClass
* the class object of the key type for the constructed {@link CounterSet}
* @param
* enum type
* @implNote Uses an {@link EnumMap} as backing map, which provides better characteristics in terms
* of performance and required memory.
*/
public static > CounterSet forEnum(Class enumClass) {
return new CounterSet<>(new EnumMap<>(enumClass));
}
/**
* Add the given increment to an element. If the element was not present before, it is interpreted
* as if it was present with value 0. Returns the new value.
*
* @param key
* the key of the counter to increment.
* @param increment
* the increment.
*/
public int inc(E key, int increment) {
Integer value = map.get(key);
int newValue;
if (value == null) {
newValue = increment;
} else {
newValue = value + increment;
}
map.put(key, newValue);
// update total sum
total += increment;
return getValue(key);
}
/**
* Same as inc(key, 1)
.
*
* @see #inc(Object, int)
*/
public int inc(E key) {
return inc(key, 1);
}
/**
* Add the given increment to the given keys. If a key was not present before, it is interpreted as
* if it was present with value 0.
*
* @param keys
* the keys of the counter to increment.
* @param increment
* the increment.
*/
public void incAll(Collection keys, int increment) {
for (E key : keys) {
inc(key, increment);
}
}
/** Increments the given elements by 1 */
public void incAll(Collection keys) {
for (E key : keys) {
inc(key);
}
}
/**
* Adds the given {@link CounterSet} to this {@link CounterSet} by incrementing all keys contained
* from other.
*/
public void add(CounterSet other) {
for (E key : other.getKeys()) {
inc(key, other.getValue(key));
}
}
/**
* Removes the second CounterSet from the first one and returns a new CounterSet.
*/
public static CounterSet removeSecondFromFirst(CounterSet first, CounterSet second) {
CounterSet merged = new CounterSet<>();
for (E key : CollectionUtils.unionSet(first.getKeys(), second.getKeys())) {
merged.inc(key, first.getValue(key));
merged.inc(key, -second.getValue(key));
if (merged.getValue(key) == 0) {
merged.remove(key);
}
}
return merged;
}
/**
* Removes all keys that the given filter matches.
*/
public void removeIf(BiFunction filter) {
map.entrySet().removeIf(next -> filter.apply(next.getKey(), next.getValue()));
}
/**
* Remove the entry with the given key, i.e. sets its value to 0. In case the entry does not exist,
* nothing happens.
*/
public void remove(E key) {
total -= getValue(key);
map.remove(key);
}
/** Removes all entries with the given keys. */
public void removeAll(Collection keys) {
for (E key : keys) {
remove(key);
}
}
/** Clears the counter set. */
public void clear() {
map.clear();
total = 0;
}
/**
* Checks if an element is stored in the array.
*/
public boolean contains(E key) {
return map.containsKey(key);
}
/**
* Get the value for an element. If the element is not stored in the counter 0
is
* returned.
*/
public int getValue(E key) {
Integer value = map.get(key);
if (value == null) {
return 0;
}
return value;
}
/**
* Returns the set of all elements used a keys for counters.
*/
public UnmodifiableSet getKeys() {
return CollectionUtils.asUnmodifiable(map.keySet());
}
/** Returns a list of all keys ordered by their value ascending */
public List getKeysByValueAscending() {
return CollectionUtils.sort(getKeys(), new Comparator() {
@Override
public int compare(E key1, E key2) {
return map.get(key1).compareTo(map.get(key2));
}
});
}
/** Returns a list of all keys ordered by their value descending */
public List getKeysByValueDescending() {
return CollectionUtils.reverse(getKeysByValueAscending());
}
/** Convert to {@link TreeMap} to have keys sorted. */
public TreeMap toSortedMap() {
return new TreeMap<>(toMap());
}
/** Get total sum of all elements. */
public int getTotal() {
return total;
}
/** Returns whether this counter set is empty */
public boolean isEmpty() {
// we don't check for the map being empty here as it may contain 0 entries due
// to usage of negative increments with inc()
return total == 0;
}
/** Returns a collection of all values */
public Collection values() {
return map.values();
}
/** {@inheritDoc} */
@Override
public String toString() {
return map.toString();
}
/**
* Transform the given counter set to a list. The ordering is determined by the given key sequence.
*/
int[] transformToList(E[] keySequence) {
int[] result = new int[keySequence.length];
for (int i = 0; i < keySequence.length; i++) {
E key = keySequence[i];
result[i] = map.getOrDefault(key, 0);
}
return result;
}
/**
* Prints the distribution of values (ascending or descending) to System.out, where each value is
* printed on a separate line in the form <key> : <value>.
*
*
* Example:
*
* foo : 4
* bar : 2
*
*/
public void printValueDistribution(boolean ascending) {
printValueDistribution(new PrintWriter(System.out), ascending);
}
/**
* Prints the distribution of values (ascending or descending) to the given stream, where each value
* is printed on a separate line in the form <key> : <value>.
*
*
* Example:
*
* foo : 4
* bar : 2
*
*/
public void printValueDistribution(PrintWriter writer, boolean ascending) {
List keys = null;
if (ascending) {
keys = getKeysByValueAscending();
} else {
keys = getKeysByValueDescending();
}
for (E key : keys) {
writer.print(String.valueOf(key));
writer.print(" : ");
writer.print(getValue(key));
writer.println();
}
writer.flush();
}
/** {@inheritDoc} */
@Override
public boolean equals(Object obj) {
if (obj instanceof CounterSet) {
@SuppressWarnings("rawtypes")
CounterSet other = (CounterSet) obj;
return map.equals(other.map);
}
return false;
}
/** {@inheritDoc} */
@Override
public int hashCode() {
return map.hashCode();
}
/** Returns the values as {@link Map}. */
public Map toMap() {
return new LinkedHashMap<>(map);
}
/** Returns the values as a {@link Map}, but omits keys where the value is 0. */
public Map toMapWithoutZeroEntries() {
Map map = toMap();
map.keySet().removeIf(key -> map.get(key) == 0);
return map;
}
/** {@inheritDoc} */
@Override
public Iterator> iterator() {
return new Iterator>() {
private Iterator> delegate = map.entrySet().iterator();
/** {@inheritDoc} */
@Override
public boolean hasNext() {
return delegate.hasNext();
}
/** {@inheritDoc} */
@Override
public Pair next() {
Entry next = delegate.next();
if (next == null) {
return null;
}
return new Pair<>(next.getKey(), next.getValue());
}
};
}
/**
* Returns a collector for collecting a stream of elements into a {@link CounterSet}.
*/
public static Collector> toCounterSet() {
return toCounterSet(Function.identity(), ignored -> 1);
}
/**
* Returns a collector for collecting a stream of elements into a {@link CounterSet}.
*
* @param elementExtractor
* Extracts the actual element to store in the {@link CounterSet}
* @param counter
* Provides the amount to increase for a specific element.
*/
public static Collector> toCounterSet(Function elementExtractor,
ToIntFunction counter) {
return new CounterSetCollector<>(elementExtractor, counter);
}
private static class CounterSetCollector implements Collector, CounterSet> {
private final Function elementExtractor;
private final ToIntFunction counter;
public CounterSetCollector(Function elementExtractor, ToIntFunction counter) {
CCSMAssert.isNotNull(elementExtractor,
() -> String.format("Expected \"%s\" to be not null", "elementExtractor"));
CCSMAssert.isNotNull(counter, () -> String.format("Expected \"%s\" to be not null", "counter"));
this.elementExtractor = elementExtractor;
this.counter = counter;
}
@Override
public Supplier> supplier() {
return CounterSet::new;
}
@Override
public BiConsumer, T> accumulator() {
return (set, element) -> set.inc(elementExtractor.apply(element), counter.applyAsInt(element));
}
@Override
public BinaryOperator> combiner() {
return (c1, c2) -> {
c1.add(c2);
return c1;
};
}
@Override
public Function, CounterSet> finisher() {
return Function.identity();
}
@Override
public Set characteristics() {
return EnumSet.of(Characteristics.UNORDERED, Characteristics.IDENTITY_FINISH);
}
}
}