All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.conqat.lib.commons.collections.CounterSet Maven / Gradle / Ivy

There is a newer version: 2024.7.2
Show newest version
/*
 * 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); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy