diff --git a/hutool-core/src/main/java/cn/hutool/core/stream/CollectorUtil.java b/hutool-core/src/main/java/cn/hutool/core/stream/CollectorUtil.java index 50911a02d..5e1fcefb3 100644 --- a/hutool-core/src/main/java/cn/hutool/core/stream/CollectorUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/stream/CollectorUtil.java @@ -243,6 +243,23 @@ public class CollectorUtil { ); } + /** + * 将流转为{@link EntryStream} + * + * @param keyMapper 键的映射方法 + * @param valueMapper 值的映射方法 + * @param 输入元素类型 + * @param 元素的键类型 + * @param 元素的值类型 + * @return 收集器 + */ + public static Collector, EntryStream> toEntryStream( + Function keyMapper, Function valueMapper) { + Objects.requireNonNull(keyMapper); + Objects.requireNonNull(valueMapper); + return transform(ArrayList::new, list -> EntryStream.of(list, keyMapper, valueMapper)); + } + /** * 将流转为{@link EasyStream} * diff --git a/hutool-core/src/main/java/cn/hutool/core/stream/EntryStream.java b/hutool-core/src/main/java/cn/hutool/core/stream/EntryStream.java new file mode 100644 index 000000000..d97e74a3f --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/stream/EntryStream.java @@ -0,0 +1,774 @@ +package cn.hutool.core.stream; + +import cn.hutool.core.collection.ConcurrentHashSet; +import cn.hutool.core.map.multi.RowKeyTable; +import cn.hutool.core.map.multi.Table; +import cn.hutool.core.util.ObjUtil; + +import java.util.*; +import java.util.function.*; +import java.util.stream.Collector; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +/** + *

针对键值对对象{@link Map.Entry}特化的增强流, + * 本身可视为一个元素类型为{@link Map.Entry}的{@link Stream}。
+ * 用于支持流式处理{@link Map}集合中的、或具有潜在可能转为{@link Map}集合的数据。 + * + * @param 键类型 + * @param 值类型 + * @author huangchengxing + */ +public class EntryStream extends StreamWrapper, EntryStream> { + + /** + * 根据键与值的集合创建键值对流,若两集合在相同下标的位置找不到对应的键或值,则使用{@code null}填充。
+ * 比如: {@code [1, 2, 3]}与{@code [1, 2]}合并,则得到{@code [{1=1}, {2=2}, {3=null}]}。 + * + * @param keys 键集合 + * @param values 值集合 + * @return {@link EntryStream}实例 + */ + public static EntryStream merge(Iterable keys, Iterable values) { + final boolean hasKeys = ObjUtil.isNotNull(keys); + final boolean hasValues = ObjUtil.isNotNull(values); + // 皆为空 + if (!hasKeys && !hasValues) { + return empty(); + } + // 值为空 + if (hasKeys && !hasValues) { + return of(keys, Function.identity(), k -> null); + } + // 键为空 + if (!hasKeys) { + return of(values, v -> null, Function.identity()); + } + // 皆不为空 + final List> entries = new ArrayList<>(); + final Iterator keyItr = keys.iterator(); + final Iterator valueItr = values.iterator(); + while (keyItr.hasNext() || valueItr.hasNext()) { + entries.add(new Entry<>( + keyItr.hasNext() ? keyItr.next() : null, + valueItr.hasNext() ? valueItr.next() : null + )); + } + return of(entries); + } + + /** + * 根据一个{@link Map}集合中的键值对创建一个串行流, + * 对流的操作不会影响到入参的{@code map}实例本身 + * + * @param map 集合 + * @param 键类型 + * @param 值类型 + * @return {@link EntryStream}实例 + */ + public static EntryStream of(Map map) { + return ObjUtil.isNull(map) ? + empty() : of(map.entrySet()); + } + + /** + * 根据一个{@link Map.Entry}类型的{@link Iterable}创建一个串行流, + * 对流的操作不会影响到入参的{@code entries}实例本身。
+ * 若输入流中存在元素为{@code null},则会映射为一个键值皆为{@code null}的键值对。 + * + * @param entries {@link Iterable}实例 + * @param
键类型 + * @param 值类型 + * @return {@link EntryStream}实例 + */ + public static EntryStream of(Iterable> entries) { + return ObjUtil.isNull(entries) ? + empty() : of(StreamSupport.stream(entries.spliterator(), false)); + } + + /** + * 根据一个{@link Collection}集合中创建一个串行流 + * + * @param source 原始集合 + * @param keyMapper 键的映射方法 + * @param valueMapper 值的映射方法 + * @param 键类型 + * @param 值类型 + * @return {@link EntryStream}实例 + */ + public static EntryStream of( + Iterable source, Function keyMapper, Function valueMapper) { + Objects.requireNonNull(keyMapper); + Objects.requireNonNull(valueMapper); + if (ObjUtil.isNull(source)) { + return empty(); + } + final Stream> stream = StreamSupport.stream(source.spliterator(), false) + .map(t -> new Entry<>(keyMapper.apply(t), valueMapper.apply(t))); + return new EntryStream<>(stream); + } + + /** + * 包装一个已有的流,若入参为空则返回一个空的串行流。
+ * 若输入流中存在元素为{@code null},则会映射为一个键值皆为{@code null}的键值对。 + * + * @param stream 流 + * @param
键类型 + * @param 值类型 + * @return {@link EntryStream}实例 + */ + public static EntryStream of(Stream> stream) { + return ObjUtil.isNull(stream) ? + empty() : new EntryStream<>(stream.map(Entry::new)); + } + + /** + * 创建一个空的串行流 + * + * @param 键类型 + * @param 值类型 + * @return {@link EntryStream}实例 + */ + public static EntryStream empty() { + return new EntryStream<>(Stream.empty()); + } + + /** + * 构造 + */ + EntryStream(Stream> stream) { + super(stream); + } + + // ================================ override ================================ + + /** + * 根据一个原始的流,返回一个新包装类实例 + * + * @param stream 流 + * @return 实现类 + */ + @Override + protected EntryStream convertToStreamImpl(Stream> stream) { + return new EntryStream<>(stream); + } + + // ================================ 中间操作 ================================ + + /** + * 根据键去重,默认丢弃靠后的 + * + * @return {@link EntryStream}实例 + */ + public EntryStream distinctByKey() { + Set accessed = new ConcurrentHashSet<>(16); + return new EntryStream<>(stream.filter(e -> { + K key = e.getKey(); + if (accessed.contains(key)) { + return false; + } + accessed.add(key); + return true; + })); + } + + /** + * 根据值去重,默认丢弃靠后的 + * + * @return {@link EntryStream}实例 + */ + public EntryStream distinctByValue() { + Set accessed = new ConcurrentHashSet<>(16); + return new EntryStream<>(stream.filter(e -> { + V val = e.getValue(); + if (accessed.contains(val)) { + return false; + } + accessed.add(val); + return true; + })); + } + + /** + * 根据键和值过滤键值对 + * + * @param filter 判断条件 + * @return {@link EntryStream}实例 + */ + public EntryStream filter(BiPredicate filter) { + Objects.requireNonNull(filter); + return super.filter(e -> filter.test(e.getKey(), e.getValue())); + } + + /** + * 根据键过滤键值对 + * + * @param filter 判断条件 + * @return {@link EntryStream}实例 + */ + public EntryStream filterByKey(Predicate filter) { + Objects.requireNonNull(filter); + return super.filter(e -> filter.test(e.getKey())); + } + + /** + * 根据值过滤键值对 + * + * @param filter 判断条件 + * @return {@link EntryStream}实例 + */ + public EntryStream filterByValue(Predicate filter) { + Objects.requireNonNull(filter); + return super.filter(e -> filter.test(e.getValue())); + } + + /** + * 过滤流中键值对本身、键值对中的值或键为{@code null}的元素 + * + * @return {@link EntryStream}实例 + */ + public EntryStream nonNull() { + return super.filter(e -> ObjUtil.isNotNull(e) && ObjUtil.isNotNull(e.getKey()) && ObjUtil.isNotNull(e.getValue())); + } + + /** + * 过滤流中键值对本身,或键值对的键为{@code null}的元素 + * + * @return {@link EntryStream}实例 + */ + public EntryStream keyNonNull() { + return super.filter(e -> ObjUtil.isNotNull(e) && ObjUtil.isNotNull(e.getKey())); + } + + /** + * 过滤流中键值对本身,或键值对的值为{@code null}的元素 + * + * @return {@link EntryStream}实例 + */ + public EntryStream valueNonNull() { + return super.filter(e -> ObjUtil.isNotNull(e) && ObjUtil.isNotNull(e.getValue())); + } + + /** + * 检查键 + * + * @param consumer 操作 + * @return {@link EntryStream}实例 + */ + public EntryStream peekKey(Consumer consumer) { + Objects.requireNonNull(consumer); + return super.peek(e -> consumer.accept(e.getKey())); + } + + /** + * 检查值 + * + * @param consumer 操作 + * @return {@link EntryStream}实例 + */ + public EntryStream peekValue(Consumer consumer) { + Objects.requireNonNull(consumer); + return super.peek(e -> consumer.accept(e.getValue())); + } + + /** + * 根据键排序 + * + * @param comparator 排序器 + * @return {@link EntryStream}实例 + */ + public EntryStream sortByKey(Comparator comparator) { + Objects.requireNonNull(comparator); + return sorted(Map.Entry.comparingByKey(comparator)); + } + + /** + * 根据值排序 + * + * @param comparator 排序器 + * @return {@link EntryStream}实例 + */ + public EntryStream sortByValue(Comparator comparator) { + Objects.requireNonNull(comparator); + return sorted(Map.Entry.comparingByValue(comparator)); + } + + // ================================ 转换操作 ================================ + + /** + * 向当前流末尾追加元素 + * + * @param key 键 + * @param value 值 + * @return {@link EntryStream}实例 + */ + public EntryStream push(K key, V value) { + return new EntryStream<>(Stream.concat(stream, Stream.of(new Entry<>(key, value)))); + } + + /** + * 转为值的流 + * + * @return 值的流 + */ + public EasyStream toValueStream() { + return EasyStream.of(stream.map(Map.Entry::getValue)); + } + + /** + * 转为键的流 + * + * @return 值的流 + */ + public EasyStream toKeyStream() { + return EasyStream.of(stream.map(Map.Entry::getKey)); + } + + /** + * 将键映射为另一类型 + * + * @param mapper 映射方法 + * @param 新的键类型 + * @return {@link EntryStream}实例 + */ + public EntryStream mapKeys(Function mapper) { + Objects.requireNonNull(mapper); + return new EntryStream<>( + stream.map(e -> new Entry<>(mapper.apply(e.getKey()), e.getValue())) + ); + } + + /** + * 将值映射为另一类型 + * + * @param mapper 映射方法 + * @param 新的值类型 + * @return {@link EntryStream}实例 + */ + public EntryStream mapValues(Function mapper) { + Objects.requireNonNull(mapper); + return new EntryStream<>( + stream.map(e -> new Entry<>(e.getKey(), mapper.apply(e.getValue()))) + ); + } + + /** + * 返回与指定函数将元素作为参数执行的结果组成的流 + * 这是一个无状态中间操作 + * + * @param mapper 指定的函数 + * @param 函数执行后返回流中元素的类型 + * @return 返回叠加操作后的流 + */ + @Override + public EasyStream map(Function, ? extends R> mapper) { + Objects.requireNonNull(mapper); + return EasyStream.of(stream.map(mapper)); + } + + /** + * 将实例转为根据键值对生成的单对象{@link Stream}实例 + * + * @param mapper 映射方法 + * @param 函数执行后返回流中元素的类型 + * @return 映射后的单对象组成的流 + */ + public EasyStream map(BiFunction mapper) { + Objects.requireNonNull(mapper); + return EasyStream.of(stream.map(e -> mapper.apply(e.getKey(), e.getValue()))); + } + + /** + * 扩散流操作,可能影响流元素个数,将原有流元素执行mapper操作,返回多个流所有元素组成的流
+ * 这是一个无状态中间操作
+ * 例如,将users里所有user的id和parentId组合在一起,形成一个新的流: + *

{@code
+	 *     FastStream ids = FastStream.of(users).flatMap(user -> FastStream.of(user.getId(), user.getParentId()));
+	 * }
+ * + * @param mapper 操作,返回流 + * @param 拆分后流的元素类型 + * @return 返回叠加拆分操作后的流 + */ + @Override + public EasyStream flatMap(Function, ? extends Stream> mapper) { + Objects.requireNonNull(mapper); + return EasyStream.of(stream.flatMap(mapper)); + } + + /** + *

将原有流的键执行mapper操作映射为流,流中的所有所有元素仍然对应原本的值, + * 然后再返回由这些流中所有元素组成的流新{@link EntryStream}串行流。
+ * 效果类似: + *

{@code
+	 * // stream = [{a = 1}, {b = 2}, {c = 3}]
+	 * stream.flatMapKey(key -> Stream.of(key + "1", key + "2"));
+	 * // stream = [{a1 = 1}, {a2 = 1}, {b1 = 2}, {b2 = 2}, {c1 = 3}, {c2 = 3}]
+	 * }
+ * + * @param keyMapper 值转映射方法 + * @param 新的键类型 + * @return 返回叠加拆分操作后的流 + */ + public EntryStream flatMapKey(Function> keyMapper) { + Objects.requireNonNull(keyMapper); + return new EntryStream<>( + stream.flatMap(e -> keyMapper + .apply(e.getKey()) + .map(newKey -> new Entry<>(newKey, e.getValue())) + ) + ); + } + + /** + *

将原有流的值执行mapper操作映射为流,流中的所有所有元素仍然对应原本的键, + * 然后再返回由这些流中所有元素组成的流新{@link EntryStream}串行流。
+ * 效果类似: + *

{@code
+	 * // stream = [{a = 1}, {b = 2}, {c = 3}]
+	 * stream.flatMapValue(num -> Stream.of(num, num+1));
+	 * // stream = [{a = 1}, {a = 2}, {b = 2}, {b = 3}, {c = 3}, {c = 4}]
+	 * }
+ * + * @param valueMapper 值转映射方法 + * @param 新的值类型 + * @return 返回叠加拆分操作后的流 + */ + public EntryStream flatMapValue(Function> valueMapper) { + Objects.requireNonNull(valueMapper); + return new EntryStream<>( + stream.flatMap(e -> valueMapper + .apply(e.getValue()) + .map(newVal -> new Entry<>(e.getKey(), newVal)) + ) + ); + } + + // ================================ 结束操作 ================================ + + /** + * 转为{@link Map}集合 + * + * @param mapFactory 获取集合的工厂方法 + * @param operator 当存在重复键时的处理 + * @return 集合 + * @see Collectors#toMap(Function, Function, BinaryOperator, Supplier) + */ + public Map toMap(Supplier> mapFactory, BinaryOperator operator) { + Objects.requireNonNull(mapFactory); + Objects.requireNonNull(operator); + return super.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, operator, mapFactory)); + } + + /** + * 转为{@link Map}集合 + * + * @param mapFactory 获取集合的工厂方法 + * @return 集合 + * @see Collectors#toMap(Function, Function, BinaryOperator) + */ + public Map toMap(Supplier> mapFactory) { + Objects.requireNonNull(mapFactory); + return super.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (t1, t2) -> t2, mapFactory)); + } + + /** + * 转为{@link HashMap}集合 + * + * @return 集合 + * @see Collectors#toMap(Function, Function) + * @throws IllegalArgumentException 当键重复时抛出 + */ + public Map toMap() { + return super.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + /** + * 将键值对分组后再转为二维{@link Map}集合,最终返回一个{@link Table}集合 + * + * @param rowKeyMapper 将键映射为父集合中键的方法 + * @param colMapFactory 创建子集合的工厂方法 + * @param operator 当存在重复键时的处理 + * @param 父集合的键类型 + * @return 集合 + * @see Collectors#groupingBy(Function, Supplier, Collector) + */ + public Table toTable( + BiFunction rowKeyMapper, Supplier> colMapFactory, BinaryOperator operator) { + Objects.requireNonNull(rowKeyMapper); + Objects.requireNonNull(colMapFactory); + Objects.requireNonNull(operator); + final Map> rawMap = collect(Collectors.groupingBy( + e -> rowKeyMapper.apply(e.getKey(), e.getValue()), + HashMap::new, + Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, operator, colMapFactory) + )); + return new RowKeyTable<>(rawMap, colMapFactory::get); + } + + /** + * 将键值对分组后再转为二维{@link HashMap}集合,最终返回一个{@link Table}集合 + * + * @param rowKeyMapper 创建父集合的工厂方法 + * @param 父集合的键类型 + * @return 集合 + * @throws IllegalArgumentException 当父集合或子集合中的键重复时抛出 + */ + public Table toTable(BiFunction rowKeyMapper) { + return toTable(rowKeyMapper, HashMap::new, throwingMerger()); + } + + /** + * 将键值对按值分组后再转为二维{@link Map}集合,最终返回一个{@link Table}集合 + * + * @param rowKeyMapper 将键映射为父集合中键的方法 + * @param colMapFactory 创建子集合的工厂方法 + * @param operator 当存在重复键时的处理 + * @param 父集合的键类型 + * @return 集合 + */ + public Table toTableByKey( + Function rowKeyMapper, Supplier> colMapFactory, BinaryOperator operator) { + return toTable((k, v) -> rowKeyMapper.apply(k), colMapFactory, operator); + } + + /** + * 将键值对按键分组后再转为二维{@link HashMap}集合,最终返回一个{@link Table}集合 + * + * @param rowKeyMapper 创建父集合的工厂方法 + * @param 父集合的键类型 + * @return 集合 + * @throws IllegalArgumentException 当父集合或子集合中的键重复时抛出 + */ + public Table toTableByKey(Function rowKeyMapper) { + return toTable((k, v) -> rowKeyMapper.apply(k)); + } + + /** + * 将键值对按值分组后再转为二维{@link Map}集合,最终返回一个{@link Table}集合 + * + * @param rowKeyMapper 将键映射为父集合中键的方法 + * @param colMapFactory 创建子集合的工厂方法 + * @param operator 当存在重复键时的处理 + * @param 父集合的键类型 + * @return 集合 + */ + public Table toTableByValue( + Function rowKeyMapper, Supplier> colMapFactory, BinaryOperator operator) { + return toTable((k, v) -> rowKeyMapper.apply(v), colMapFactory, operator); + } + + /** + * 将键值对按键分组后再转为二维{@link HashMap}集合,最终返回一个{@link Table}集合 + * + * @param rowKeyMapper 创建父集合的工厂方法 + * @param 父集合的键类型 + * @return 集合 + * @throws IllegalArgumentException 当父集合或子集合中的键重复时抛出 + */ + public Table toTableByValue(Function rowKeyMapper) { + return toTable((k, v) -> rowKeyMapper.apply(v)); + } + + /** + * 将键值对按键分组 + * + * @return 集合 + */ + public Map> groupByKey() { + return groupByKey(Collectors.toList()); + } + + /** + * 将键值对按键分组 + * + * @param collector 对具有相同键的值的收集器 + * @param 值集合的类型 + * @return 集合 + */ + public > Map groupByKey(Collector collector) { + return groupByKey((Supplier>)HashMap::new, collector); + } + + /** + * 将键值对按键分组 + * + * @param mapFactory 创建map集合的工厂方法 + * @param collector 对具有相同键的值的收集器 + * @param 值集合的类型 + * @param 返回的map集合类型 + * @return 集合 + */ + public , M extends Map> M groupByKey(Supplier mapFactory, Collector collector) { + return super.collect(Collectors.groupingBy( + Map.Entry::getKey, mapFactory, + CollectorUtil.transform(ArrayList::new, s -> s.stream().map(Map.Entry::getValue).collect(collector)) + )); + } + + /** + * 遍历键值对 + * + * @param consumer 操作 + */ + public void forEach(BiConsumer consumer) { + Objects.requireNonNull(consumer); + super.forEach(e -> consumer.accept(e.getKey(), e.getValue())); + } + + /** + * 将键值对翻转 + * + * @return {@link EntryStream}实例 + */ + public EntryStream inverse() { + return new EntryStream<>( + stream.map(e -> new Entry<>(e.getValue(), e.getKey())) + ); + } + + /** + * 收集键 + * + * @param collector 收集器 + * @param 返回值类型 + * @return 收集容器 + */ + public R collectKeys(Collector collector) { + return toKeyStream().collect(collector); + } + + /** + * 收集值 + * + * @param collector 收集器 + * @param 返回值类型 + * @return 收集容器 + */ + public R collectValues(Collector collector) { + return toValueStream().collect(collector); + } + + /** + * 是否存在任意符合条件的键值对 + * + * @param predicate 判断条件 + * @return 是否 + */ + public boolean anyMatch(BiPredicate predicate) { + return super.anyMatch(e -> predicate.test(e.getKey(), e.getValue())); + } + + /** + * 所有键值对是否都符合条件 + * + * @param predicate 判断条件 + * @return 是否 + */ + public boolean allMatch(BiPredicate predicate) { + Objects.requireNonNull(predicate); + return super.allMatch(e -> predicate.test(e.getKey(), e.getValue())); + } + + /** + * 所有键值对是否都不符合条件 + * + * @param predicate 判断条件 + * @return 是否 + */ + public boolean noneMatch(BiPredicate predicate) { + Objects.requireNonNull(predicate); + return super.noneMatch(e -> predicate.test(e.getKey(), e.getValue())); + } + + /** + * {@link Map.Entry}的基本实现 + */ + static class Entry implements Map.Entry { + + /** + * 键 + */ + private final K key; + + /** + * 值 + */ + private V val; + + /** + * 创建一个简单键值对对象 + * + * @param key 键 + * @param val 值 + */ + public Entry(K key, V val) { + this.key = key; + this.val = val; + } + + /** + * 创建一个简单键值对对象 + * + * @param entry 键值对 + */ + public Entry(Map.Entry entry) { + if (ObjUtil.isNull(entry)) { + this.key = null; + this.val = null; + } else { + this.key = entry.getKey(); + this.val = entry.getValue(); + } + } + + /** + * 获取键 + * + * @return 键 + */ + @Override + public K getKey() { + return key; + } + + /** + * 获取值 + * + * @return 值 + */ + @Override + public V getValue() { + return val; + } + + /** + * 设置值 + * + * @param value 值 + * @return 旧值 + */ + @Override + public V setValue(V value) { + V old = val; + val = value; + return old; + } + + @Override + public String toString() { + return "{" + key + "=" + val + '}'; + } + + } + + /** + * key重复时直接抛出异常 + */ + private static BinaryOperator throwingMerger() { + return (u,v) -> { throw new IllegalStateException(String.format("Duplicate key %s", u)); }; + } + +} diff --git a/hutool-core/src/test/java/cn/hutool/core/stream/CollectorUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/stream/CollectorUtilTest.java index 068357369..00670f8d2 100644 --- a/hutool-core/src/test/java/cn/hutool/core/stream/CollectorUtilTest.java +++ b/hutool-core/src/test/java/cn/hutool/core/stream/CollectorUtilTest.java @@ -5,6 +5,7 @@ import org.junit.Assert; import org.junit.Test; import java.util.*; +import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -51,4 +52,18 @@ public class CollectorUtilTest { Assert.assertEquals(EasyStream.class, stream.getClass()); } + @Test + public void testToEntryStream() { + Map map = Stream.of(1, 2, 3, 4, 5) + // 转为EntryStream + .collect(CollectorUtil.toEntryStream(Function.identity(), String::valueOf)) + // 过滤偶数 + .filterByKey(k -> (k & 1) == 1) + .inverse() + .toMap(); + Assert.assertEquals((Integer)1, map.get("1")); + Assert.assertEquals((Integer)3, map.get("3")); + Assert.assertEquals((Integer)5, map.get("5")); + } + } diff --git a/hutool-core/src/test/java/cn/hutool/core/stream/EntryStreamTest.java b/hutool-core/src/test/java/cn/hutool/core/stream/EntryStreamTest.java new file mode 100644 index 000000000..3bbfe072c --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/stream/EntryStreamTest.java @@ -0,0 +1,505 @@ +package cn.hutool.core.stream; + +import cn.hutool.core.map.multi.Table; +import org.junit.Assert; +import org.junit.Test; + +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class EntryStreamTest { + + @Test + public void testMerge() { + Assert.assertEquals(0, EntryStream.merge(null, null).count()); + Assert.assertEquals( + Arrays.asList(2, 4, 6), + EntryStream.merge(Arrays.asList(1, 2, 3), Arrays.asList(1, 2, 3)) + .map(Integer::sum) + .toList() + ); + Assert.assertEquals( + Arrays.asList(1, 2, null), + EntryStream.merge(Arrays.asList(1, 2, 3), Arrays.asList(1, 2)) + .collectValues(Collectors.toList()) + ); + Assert.assertEquals( + Arrays.asList(1, 2, null), + EntryStream.merge(Arrays.asList(1, 2), Arrays.asList(1, 2, 3)) + .collectKeys(Collectors.toList()) + ); + } + + @Test + public void testOf() { + Map map = new HashMap<>(); + map.put("1", "1"); + Assert.assertEquals(1, EntryStream.of(map).count()); + + Set> entries = new HashSet<>(); + entries.add(new Entry<>(1, 1)); + entries.add(null); + Assert.assertEquals(2, EntryStream.of(entries).count()); + Assert.assertEquals(2, EntryStream.of(entries.stream()).count()); + + Iterable iterable = Arrays.asList(1, 2, null); + Assert.assertEquals(3, EntryStream.of(iterable, Function.identity(), Function.identity()).count()); + } + + @Test + public void testEmpty() { + Assert.assertEquals(0, EntryStream.empty().count()); + } + + @Test + public void testDistinctByKey() { + long count = EntryStream.of(Arrays.asList(new Entry<>(1, 1), new Entry<>(1, 2), new Entry<>(2, 1), new Entry<>(2, 2))) + .distinctByKey() + .count(); + Assert.assertEquals(2, count); + } + + @Test + public void testDistinctByValue() { + long count = EntryStream.of(Arrays.asList(new Entry<>(1, 1), new Entry<>(1, 2), new Entry<>(2, 1), new Entry<>(2, 2))) + .distinctByValue() + .count(); + Assert.assertEquals(2, count); + } + + @Test + public void testFilter() { + long count = EntryStream.of(Arrays.asList(new Entry<>(1, 1), new Entry<>(1, 2), new Entry<>(2, 1), new Entry<>(2, 2))) + .filter((k, v) -> k == 1 && v == 1) + .count(); + Assert.assertEquals(1, count); + } + + @Test + public void testFilterByKey() { + long count = EntryStream.of(Arrays.asList(new Entry<>(1, 1), new Entry<>(1, 2), new Entry<>(2, 1), new Entry<>(2, 2))) + .filterByKey(k -> k == 1) + .count(); + Assert.assertEquals(2, count); + } + + @Test + public void testFilterByValue() { + long count = EntryStream.of(Arrays.asList(new Entry<>(1, 1), new Entry<>(1, 2), new Entry<>(2, 1), new Entry<>(2, 2))) + .filterByValue(v -> v == 1) + .count(); + Assert.assertEquals(2, count); + } + + @Test + public void testPeekKey() { + List keys = new ArrayList<>(); + EntryStream.of(Arrays.asList(new Entry<>(1, 1), new Entry<>(1, 2), new Entry<>(2, 1), new Entry<>(2, 2))) + .peekKey(keys::add) + .count(); + Assert.assertEquals(Arrays.asList(1, 1, 2, 2), keys); + } + + @Test + public void testPeekValue() { + List values = new ArrayList<>(); + EntryStream.of(Arrays.asList(new Entry<>(1, 1), new Entry<>(1, 2), new Entry<>(2, 1), new Entry<>(2, 2))) + .peekValue(values::add) + .count(); + Assert.assertEquals(Arrays.asList(1, 2, 1, 2), values); + } + + @Test + public void testPush() { + Assert.assertEquals( + 5, + EntryStream.of(Arrays.asList(1, 2, 3), Function.identity(), Function.identity()) + .push(4, 4) + .push(5, 5) + .count() + ); + + } + + @Test + public void testSortByKey() { + List> entries = EntryStream.of(Arrays.asList(new Entry<>(3, 1), new Entry<>(2, 1), new Entry<>(4, 1), new Entry<>(1, 1))) + .sortByKey(Comparator.comparingInt(Integer::intValue)) + .collect(Collectors.toList()); + Assert.assertEquals( + Arrays.asList(1, 2, 3, 4), + entries.stream().map(Map.Entry::getKey).collect(Collectors.toList()) + ); + } + + @Test + public void testSortByValue() { + List> entries = EntryStream.of(Arrays.asList(new Entry<>(4, 4), new Entry<>(2, 2), new Entry<>(1, 1), new Entry<>(3, 3))) + .sortByValue(Comparator.comparingInt(Integer::intValue)) + .collect(Collectors.toList()); + Assert.assertEquals( + Arrays.asList(1, 2, 3, 4), + entries.stream().map(Map.Entry::getValue).collect(Collectors.toList()) + ); + } + + @Test + public void testToValueStream() { + Map map = new HashMap<>(); + map.put(1, 1); + map.put(2, 2); + map.put(3, 3); + Assert.assertEquals( + new ArrayList<>(map.values()), EntryStream.of(map).toValueStream().collect(Collectors.toList()) + ); + } + + @Test + public void testToKeyStream() { + Map map = new HashMap<>(); + map.put(1, 1); + map.put(2, 2); + map.put(3, 3); + Assert.assertEquals( + new ArrayList<>(map.keySet()), EntryStream.of(map).toKeyStream().collect(Collectors.toList()) + ); + } + + @Test + public void testCollectKey() { + Map map = new HashMap<>(); + map.put(1, 1); + map.put(2, 2); + map.put(3, 3); + List keys = EntryStream.of(map).collectKeys(Collectors.toList()); + Assert.assertEquals(new ArrayList<>(map.keySet()), keys); + } + + @Test + public void testCollectValue() { + Map map = new HashMap<>(); + map.put(1, 1); + map.put(2, 2); + map.put(3, 3); + List keys = EntryStream.of(map).collectValues(Collectors.toList()); + Assert.assertEquals(new ArrayList<>(map.keySet()), keys); + } + + @Test + public void testMapKeys() { + Map map = new HashMap<>(); + map.put(1, 1); + map.put(2, 2); + map.put(3, 3); + Assert.assertEquals( + Arrays.asList("1", "2", "3"), + EntryStream.of(map) + .mapKeys(String::valueOf) + .toKeyStream() + .collect(Collectors.toList()) + ); + } + + @Test + public void testMapValues() { + Map map = new HashMap<>(); + map.put(1, 1); + map.put(2, 2); + map.put(3, 3); + Assert.assertEquals( + Arrays.asList("1", "2", "3"), + EntryStream.of(map) + .mapValues(String::valueOf) + .toValueStream() + .collect(Collectors.toList()) + ); + } + + @Test + public void testMap() { + Map map = new HashMap<>(); + map.put(1, 1); + map.put(2, 2); + map.put(3, 3); + Assert.assertEquals( + Arrays.asList("11", "22", "33"), + EntryStream.of(map) + .map((k, v) -> k.toString() + v.toString()) + .collect(Collectors.toList()) + ); + } + + @Test + public void testFlatMap() { + Map map = new HashMap<>(); + map.put(1, 1); + map.put(2, 2); + map.put(3, 3); + List list = EntryStream.of(map) + .flatMap(e -> Stream.of(e.getKey(), e.getKey() + 1)) + .collect(Collectors.toList()); + Assert.assertEquals(Arrays.asList(1, 2, 2, 3, 3, 4), list); + } + + @Test + public void testFlatMapValue() { + Map map = new HashMap<>(); + map.put("class1", 1); + map.put("class2", 2); + map.put("class3", 3); + List values = EntryStream.of(map) + .flatMapKey(k -> Stream.of(k + "'s student1", k + "'s student2")) + .map((k, v) -> v + "=" + k) + .sorted() + .collect(Collectors.toList()); + Assert.assertEquals( + Arrays.asList( + "1=class1's student1", "1=class1's student2", + "2=class2's student1", "2=class2's student2", + "3=class3's student1", "3=class3's student2" + ), + values + ); + } + + @Test + public void testInverse() { + Map map = new HashMap<>(); + map.put("key1", "value1"); + map.put("key2", "value2"); + map.put("key3", "value3"); + List results = EntryStream.of(map) + .inverse() + .map((k, v) -> k + "=" + v) + .collect(Collectors.toList()); + Assert.assertEquals( + Arrays.asList("value1=key1", "value2=key2", "value3=key3"), + results + ); + } + + @Test + public void testFlatMapKey() { + Map map = new HashMap<>(); + map.put(1, "class1"); + map.put(2, "class2"); + map.put(3, "class3"); + List values = EntryStream.of(map) + .flatMapValue(v -> Stream.of(v + "'s student1", v + "'s student2")) + .map((k, v) -> k + "=" + v) + .collect(Collectors.toList()); + Assert.assertEquals( + Arrays.asList( + "1=class1's student1", "1=class1's student2", + "2=class2's student1", "2=class2's student2", + "3=class3's student1", "3=class3's student2" + ), + values + ); + } + + @Test + public void testForEach() { + Map map = new HashMap<>(); + map.put(1, 1); + map.put(2, 2); + map.put(3, 3); + + List keys = new ArrayList<>(); + List values = new ArrayList<>(); + EntryStream.of(map).forEach((k ,v) -> { + keys.add(k); + values.add(v); + }); + Assert.assertEquals(Arrays.asList(1, 2, 3), keys); + Assert.assertEquals(Arrays.asList(1, 2, 3), values); + } + + @Test + public void testToMap() { + Map map = new HashMap<>(); + map.put(1, 1); + map.put(2, 2); + map.put(3, 3); + + Map result = EntryStream.of(map).toMap(); + Assert.assertEquals(map, result); + + result = EntryStream.of(map).toMap(LinkedHashMap::new); + Assert.assertEquals(new LinkedHashMap<>(map), result); + + result = EntryStream.of(map).toMap(LinkedHashMap::new, (t1, t2) -> t1); + Assert.assertEquals(new LinkedHashMap<>(map), result); + } + + @Test + public void testToTable() { + Map map = new HashMap<>(); + map.put(1, 1); + map.put(2, 2); + map.put(3, 3); + map.put(4, 4); + + // 按是否偶数分组 + Table table = EntryStream.of(map).toTable( + (k ,v) -> (k & 1) == 0, HashMap::new, (t1, t2) -> t1 + ); + Assert.assertEquals((Integer)1, table.get(false, 1)); + Assert.assertEquals((Integer)3, table.get(false, 3)); + Assert.assertEquals((Integer)2, table.get(true, 2)); + Assert.assertEquals((Integer)4, table.get(true, 4)); + + table = EntryStream.of(map).toTable((k ,v) -> (k & 1) == 0); + Assert.assertEquals((Integer)1, table.get(false, 1)); + Assert.assertEquals((Integer)3, table.get(false, 3)); + Assert.assertEquals((Integer)2, table.get(true, 2)); + Assert.assertEquals((Integer)4, table.get(true, 4)); + } + + @Test + public void testToTableByKey() { + Map map = new HashMap<>(); + map.put(1, 1); + map.put(2, 2); + map.put(3, 3); + map.put(4, 4); + + // 按是否偶数分组 + Table table = EntryStream.of(map).toTableByKey( + k -> (k & 1) == 0, HashMap::new, (t1, t2) -> t1 + ); + Assert.assertEquals((Integer)1, table.get(false, 1)); + Assert.assertEquals((Integer)3, table.get(false, 3)); + Assert.assertEquals((Integer)2, table.get(true, 2)); + Assert.assertEquals((Integer)4, table.get(true, 4)); + + table = EntryStream.of(map).toTableByKey(k -> (k & 1) == 0); + Assert.assertEquals((Integer)1, table.get(false, 1)); + Assert.assertEquals((Integer)3, table.get(false, 3)); + Assert.assertEquals((Integer)2, table.get(true, 2)); + Assert.assertEquals((Integer)4, table.get(true, 4)); + } + + @Test + public void testToTableByValue() { + Map map = new HashMap<>(); + map.put(1, 1); + map.put(2, 2); + map.put(3, 3); + map.put(4, 4); + + // 按是否偶数分组 + Table table = EntryStream.of(map).toTableByValue( + v -> (v & 1) == 0, HashMap::new, (t1, t2) -> t1 + ); + Assert.assertEquals((Integer)1, table.get(false, 1)); + Assert.assertEquals((Integer)3, table.get(false, 3)); + Assert.assertEquals((Integer)2, table.get(true, 2)); + Assert.assertEquals((Integer)4, table.get(true, 4)); + + table = EntryStream.of(map).toTableByValue(v -> (v & 1) == 0); + Assert.assertEquals((Integer)1, table.get(false, 1)); + Assert.assertEquals((Integer)3, table.get(false, 3)); + Assert.assertEquals((Integer)2, table.get(true, 2)); + Assert.assertEquals((Integer)4, table.get(true, 4)); + } + + @Test + public void testGroupByKey() { + Map> map1 = EntryStream.of(Arrays.asList(1, 1, 2, 2), Function.identity(), Function.identity()) + .groupByKey(); + Assert.assertEquals(2, map1.size()); + Assert.assertEquals(Arrays.asList(1, 1), map1.get(1)); + Assert.assertEquals(Arrays.asList(2, 2), map1.get(2)); + + Map> map2 = EntryStream.of(Arrays.asList(1, 1, 2, 2), Function.identity(), Function.identity()) + .groupByKey(Collectors.toSet()); + Assert.assertEquals(2, map2.size()); + Assert.assertEquals(Collections.singleton(1), map2.get(1)); + Assert.assertEquals(Collections.singleton(2), map2.get(2)); + + Map> map3 = EntryStream.of(Arrays.asList(1, 1, 2, 2), Function.identity(), Function.identity()) + .groupByKey(LinkedHashMap::new, Collectors.toSet()); + Assert.assertEquals(2, map3.size()); + Assert.assertEquals(Collections.singleton(1), map3.get(1)); + Assert.assertEquals(Collections.singleton(2), map3.get(2)); + } + + @Test + public void testAnyMatch() { + Assert.assertTrue(EntryStream.of(Arrays.asList(1, 1, 2, 2), Function.identity(), Function.identity()) + .anyMatch((k, v) -> (k & 1) == 1)); + Assert.assertFalse(EntryStream.of(Arrays.asList(2, 2, 2, 2), Function.identity(), Function.identity()) + .anyMatch((k, v) -> (k & 1) == 1)); + } + + @Test + public void testAllMatch() { + Assert.assertFalse(EntryStream.of(Arrays.asList(1, 1, 2, 2), Function.identity(), Function.identity()) + .allMatch((k, v) -> (k & 1) == 1)); + Assert.assertTrue(EntryStream.of(Arrays.asList(2, 2, 2, 2), Function.identity(), Function.identity()) + .allMatch((k, v) -> (k & 1) == 0)); + } + + @Test + public void testNoneMatch() { + Assert.assertFalse(EntryStream.of(Arrays.asList(1, 1, 2, 2), Function.identity(), Function.identity()) + .noneMatch((k, v) -> (k & 1) == 1)); + Assert.assertTrue(EntryStream.of(Arrays.asList(2, 2, 2, 2), Function.identity(), Function.identity()) + .noneMatch((k, v) -> (k & 1) == 1)); + } + + @Test + public void testNonNull() { + Map map = new HashMap<>(); + map.put(1, null); + map.put(null, 1); + Assert.assertEquals(0, EntryStream.of(map).nonNull().count()); + } + + @Test + public void testKeyNonNull() { + Map map = new HashMap<>(); + map.put(1, null); + map.put(null, 1); + Assert.assertEquals(1, EntryStream.of(map).keyNonNull().count()); + } + + @Test + public void testValueNonNull() { + Map map = new HashMap<>(); + map.put(1, null); + map.put(null, 1); + Assert.assertEquals(1, EntryStream.of(map).valueNonNull().count()); + } + + private static class Entry implements Map.Entry { + + private final K key; + private final V value; + + public Entry(K key, V value) { + this.key = key; + this.value = value; + } + + @Override + public K getKey() { + return key; + } + + @Override + public V getValue() { + return value; + } + + @Override + public V setValue(V value) { + return null; + } + } + +} +