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

net.pincette.util.Collections Maven / Gradle / Ivy

There is a newer version: 2.5.0
Show newest version
package net.pincette.util;

import static java.lang.Integer.min;
import static java.util.Arrays.copyOf;
import static java.util.Collections.nCopies;
import static java.util.Optional.ofNullable;
import static java.util.regex.Pattern.quote;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import static java.util.stream.Collectors.toSet;
import static net.pincette.util.Pair.pair;
import static net.pincette.util.StreamUtil.stream;
import static net.pincette.util.Util.countingIterator;

import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Stream;
import net.pincette.function.SideEffect;

/**
 * Collection utilities.
 *
 * @author Werner Donn\u00e9
 */
public class Collections {
  private Collections() {}

  /**
   * Works like Map.computeIfAbsent, but avoids a concurrent access exception when the
   * given function also modifies the map.
   *
   * @param map the given map.
   * @param key the key to look up.
   * @param fn the function that generates a new value if it doesn't exist for the given key.
   * @param  the key type.
   * @param  the value type.
   * @return The existing or generated value.
   * @since 1.8.1
   */
  public static  V computeIfAbsent(
      final Map map, final K key, final Function fn) {
    return ofNullable(map.get(key))
        .orElseGet(
            () ->
                Optional.of(fn.apply(key))
                    .map(v -> SideEffect.run(() -> map.put(key, v)).andThenGet(() -> v))
                    .orElse(null));
  }

  /**
   * Works like Map.computeIfPresent, but avoids a concurrent access exception when the
   * given function also modifies the map.
   *
   * @param map the given map.
   * @param key the key to look up.
   * @param fn the function that generates a new value if one already exists for the given key.
   * @param  the key type.
   * @param  the value type.
   * @return The new value or null.
   * @since 1.8.1
   */
  public static  V computeIfPresent(
      final Map map, final K key, final BiFunction fn) {
    return ofNullable(map.get(key))
        .map(
            v ->
                ofNullable(fn.apply(key, v))
                    .map(val -> SideEffect.run(() -> map.put(key, v)).andThenGet(() -> val))
                    .orElseGet(
                        () -> SideEffect.run(() -> map.remove(key)).andThenGet(() -> null)))
        .orElse(null);
  }

  /**
   * Concatenates collections into one list containing the elements of the collections.
   *
   * @param collections the given collections.
   * @param  the element type.
   * @return The new list.
   * @since 1.7
   */
  @SafeVarargs
  public static  List concat(final Collection... collections) {
    return concat(Arrays.stream(collections));
  }

  /**
   * Concatenates collections into one list containing the elements of the collections.
   *
   * @param collections the given collections.
   * @param  the element type.
   * @return The new list.
   * @since 1.7
   */
  public static  List concat(final Stream> collections) {
    return collections.flatMap(Collection::stream).collect(toList());
  }

  /**
   * Returns a new set with the elements of c1 but without those that also occur in
   * c2.
   *
   * @param c1 the first collection.
   * @param c2 the second collection.
   * @param  the type of the elements in the collections and the result.
   * @return The new set.
   */
  public static  Set difference(final Collection c1, final Collection c2) {
    final Set result = new HashSet<>(c1);

    result.removeAll(c2);

    return result;
  }

  /**
   * Returns a map where all keys that are paths with segments separated by delimiter
   * are replaced with the first segment as the key and an expanded submap with the remainder of the
   * segments as the value.
   *
   * @param map the given map.
   * @param delimiter the delimiter for the keys in the given map.
   * @return The new expanded map.
   * @since 1.8
   */
  public static Map expand(final Map map, final String delimiter) {
    final Map result = new HashMap<>();

    map.forEach(
        (k, v) -> {
          final String[] segments = k.split(quote(delimiter));

          Arrays.stream(copyOf(segments, segments.length - 1))
              .reduce(
                  result,
                  (m, segment) ->
                      (Map) m.computeIfAbsent(segment, s -> new HashMap<>()),
                  (m1, m2) -> m1)
              .put(segments[segments.length - 1], v);
        });

    return result;
  }

  /**
   * Returns a map where the keys are paths created by the keys of the maps and the submaps,
   * separated by delimiter. The keys of all the submaps must be strings too. The
   * result is a map without submaps.
   *
   * @param map the given map.
   * @param delimiter the delimiter for the keys in the new map.
   * @return The new flattened map.
   * @since 1.8
   */
  public static Map flatten(final Map map, final String delimiter) {
    return map.entrySet().stream()
        .flatMap(
            e ->
                e.getValue() instanceof Map
                    ? flatten((Map) e.getValue(), delimiter).entrySet().stream()
                        .map(sub -> pair(e.getKey() + delimiter + sub.getKey(), sub.getValue()))
                    : Stream.of(pair(e.getKey(), e.getValue())))
        .collect(toMap(p -> p.first, p -> p.second));
  }

  /**
   * Returns an optional value for a key. The value will be empty is the key doesn't exist.
   *
   * @param map the map that is queried.
   * @param key the query key.
   * @param  the key type.
   * @param  the value type.
   * @return The optional value.
   */
  public static  Optional get(final Map map, final K key) {
    return ofNullable(map.get(key));
  }

  /**
   * Returns a stream of pairs for a list, where the second element of the pair is the zero-based
   * position of the element in the list.
   *
   * @param list the list of which the stream is made.
   * @param  the type of the elements in the list.
   * @return The stream of pairs.
   */
  public static  Stream> indexedStream(final List list) {
    return stream(countingIterator(list.iterator()));
  }

  /**
   * Returns a new set containing all elements that are common in the given collections.
   *
   * @param collections the given collections.
   * @param  the element type.
   * @return The intersection.
   */
  @SafeVarargs
  public static  Set intersection(final Collection... collections) {
    return intersection(Arrays.stream(collections));
  }

  /**
   * Returns a new set containing all elements that are common in the given collections.
   *
   * @param collections the given collections.
   * @param  the element type.
   * @return The intersection.
   * @since 1.7
   */
  public static  Set intersection(final Stream> collections) {
    return collections
        .map(HashSet::new)
        .reduce((s1, s2) -> SideEffect.>run(() -> s1.retainAll(s2)).andThenGet(() -> s1))
        .map(Set.class::cast)
        .orElseGet(java.util.Collections::emptySet);
  }

  /**
   * Creates a list with the given elements.
   *
   * @param elements the given elements.
   * @param  the element type.
   * @return The new list.
   */
  @SafeVarargs
  public static  List list(final T... elements) {
    return Arrays.stream(elements).collect(toList());
  }

  /**
   * Creates a map with the given element pairs, where each first element is a key and each second
   * element a value.
   *
   * @param pairs the given pairs.
   * @param  the key type.
   * @param  the value type.
   * @return The new map.
   */
  @SafeVarargs
  public static  Map map(final Pair... pairs) {
    return map(Arrays.stream(pairs));
  }

  /**
   * Creates a map with the given element pairs, where each first element is a key and each second
   * element a value.
   *
   * @param pairs the given pairs.
   * @param  the key type.
   * @param  the value type.
   * @return The new map.
   * @since 1.7
   */
  public static  Map map(final Stream> pairs) {
    return pairs.collect(toMap(pair -> pair.first, pair -> pair.second));
  }

  /**
   * Returns a new map with all the mappings of the given maps combined. When there is more than one
   * mapping for a key only the last one will be retained.
   *
   * @param maps the given maps.
   * @param  the key type.
   * @param  the value type.
   * @return The new map.
   */
  @SafeVarargs
  public static  Map merge(final Map... maps) {
    return merge(Arrays.stream(maps));
  }

  /**
   * Returns a new map with all the mappings of the given maps combined. When there is more than one
   * mapping for a key only the last one will be retained.
   *
   * @param maps the given maps.
   * @param  the key type.
   * @param  the value type.
   * @return The new map.
   * @since 1.7
   */
  public static  Map merge(final Stream> maps) {
    return maps.flatMap(m -> m.entrySet().stream())
        .collect(toMap(Entry::getKey, Entry::getValue, (v1, v2) -> v2));
  }

  /**
   * Returns a set of pairs where each element in s1 is combined with each element in
   * s2.
   *
   * @param s1 the elements for the first elements of the pairs.
   * @param s2 the elements for the second elements of the pairs.
   * @param  the element type of the first set.
   * @param  the element type of the second set.
   * @return The new set.
   */
  public static  Set> multiply(final Set s1, final Set s2) {
    return s1.stream().flatMap(el1 -> s2.stream().map(el2 -> pair(el1, el2))).collect(toSet());
  }

  /**
   * Returns a new map with the added mapping.
   *
   * @param map the original map.
   * @param key the new key.
   * @param value the new value.
   * @param  the key type.
   * @param  the value type.
   * @return The new map.
   */
  public static  Map put(final Map map, final K key, final V value) {
    final Map result = new HashMap<>(map);

    result.put(key, value);

    return result;
  }

  /**
   * Returns a new map, which hasn't mappings for the given keys.
   *
   * @param map the original map.
   * @param keys the keys that are to be removed.
   * @param  the key type.
   * @param  the value type.
   * @return The new map.
   */
  @SafeVarargs
  public static  Map remove(final Map map, final K... keys) {
    final Map result = new HashMap<>(map);

    Arrays.stream(keys).forEach(result::remove);

    return result;
  }

  /**
   * Returns an iterator that iterates over the list in reverse order.
   *
   * @param list the given list.
   * @param  the element type.
   * @return The iterator.
   */
  public static  Iterator reverse(final List list) {
    return list.isEmpty() ? list.iterator() : new ReverseIterator<>(list);
  }

  /**
   * Creates a set with the given elements.
   *
   * @param elements the given elements.
   * @param  the element type.
   * @return The new set.
   */
  @SafeVarargs
  public static  Set set(final T... elements) {
    return Arrays.stream(elements).collect(toSet());
  }

  /**
   * Removes the first positions elements from the list and adds the last element
   * positions times at the end of the list.
   *
   * @param list the given list.
   * @param positions the number of positions over which to shift. If it is larger than the list
   *     size it will be reduced to the list size.
   * @param  the element type.
   * @return The new list.
   * @since 1.7
   */
  public static  List shiftDown(final List list, final int positions) {
    return !list.isEmpty() ? shiftDown(list, positions, list.get(list.size() - 1)) : list;
  }

  /**
   * Removes the first positions elements from the list and adds newElement
   *  positions times at the end of the list.
   *
   * @param list the given list.
   * @param positions the number of positions over which to shift. If it is larger than the list
   *     size it will be reduced to the list size.
   * @param newElement the element that is shifted in.
   * @param  the element type.
   * @return The new list.
   * @since 1.7
   */
  public static  List shiftDown(final List list, final int positions, final T newElement) {
    final int pos = min(positions, list.size());

    return !list.isEmpty()
        ? concat(list.subList(pos, list.size()), nCopies(pos, newElement))
        : list;
  }

  /**
   * Removes the last positions elements from the list and adds the first element
   * positions times at the start of the list.
   *
   * @param list the given list.
   * @param positions the number of positions over which to shift. If it is larger than the list
   *     size it will be reduced to the list size.
   * @param  the element type.
   * @return The new list.
   * @since 1.7
   */
  public static  List shiftUp(final List list, final int positions) {
    return !list.isEmpty() ? shiftUp(list, positions, list.get(0)) : list;
  }

  /**
   * Removes the last positions elements from the list and adds newElement
   *  positions times at the start of the list.
   *
   * @param list the given list.
   * @param positions the number of positions over which to shift. If it is larger than the list
   *     size it will be reduced to the list size.
   * @param newElement the element that is shifted in.
   * @param  the element type.
   * @return The new list.
   * @since 1.7
   */
  public static  List shiftUp(final List list, final int positions, final T newElement) {
    final int pos = min(positions, list.size());

    return !list.isEmpty()
        ? concat(nCopies(pos, newElement), list.subList(0, list.size() - pos))
        : list;
  }

  /**
   * Returns a new set containing all of the elements from the given collections.
   *
   * @param collections the given collections.
   * @param  the element type.
   * @return The new set.
   */
  @SafeVarargs
  public static  Set union(final Collection... collections) {
    return union(Arrays.stream(collections));
  }

  /**
   * Returns a new set containing all of the elements from the given collections.
   *
   * @param collections the given collections.
   * @param  the element type.
   * @return The new set.
   * @since 1.7
   */
  public static  Set union(final Stream> collections) {
    return collections.flatMap(Collection::stream).collect(toSet());
  }

  private static class ReverseIterator implements Iterator {
    private final ListIterator iterator;

    private ReverseIterator(final List list) {
      iterator = list.listIterator(list.size());
    }

    public boolean hasNext() {
      return iterator.hasPrevious();
    }

    public T next() {
      if (!hasNext()) {
        throw new NoSuchElementException();
      }

      return iterator.previous();
    }
  }
}