diff --git a/src/main/java/it/aboutbits/springboot/toolbox/util/CollectUtil.java b/src/main/java/it/aboutbits/springboot/toolbox/util/CollectUtil.java new file mode 100644 index 0000000..42f7ed0 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/toolbox/util/CollectUtil.java @@ -0,0 +1,114 @@ +package it.aboutbits.springboot.toolbox.util; + +import lombok.NonNull; +import org.springframework.data.util.Streamable; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * This is a collection of tools to simply collect a property to a new collection or stream. + * The common usage would, for example, be to collect the ids of all items in a list to a set. + *

+ * The returned collections are unmodifiable. + */ +public final class CollectUtil { + private CollectUtil() { + } + + public static Set collectToSet(@NonNull Collection items, @NonNull Function mapper) { + return items.stream() + .map(mapper) + .filter(Objects::nonNull) + .collect(Collectors.toUnmodifiableSet()); + } + + public static Set collectToSet(@NonNull Streamable items, @NonNull Function mapper) { + return items.stream() + .map(mapper) + .filter(Objects::nonNull) + .collect(Collectors.toUnmodifiableSet()); + } + + public static Set collectToSet(@NonNull Stream items, @NonNull Function mapper) { + return items + .map(mapper) + .filter(Objects::nonNull) + .collect(Collectors.toUnmodifiableSet()); + } + + public static List collectToList(@NonNull Collection items, @NonNull Function mapper) { + return items.stream() + .map(mapper) + .filter(Objects::nonNull) + .toList(); + } + + public static List collectToList(@NonNull Streamable items, @NonNull Function mapper) { + return items.stream() + .map(mapper) + .filter(Objects::nonNull) + .toList(); + } + + public static List collectToList(@NonNull Stream items, @NonNull Function mapper) { + return items + .map(mapper) + .filter(Objects::nonNull) + .toList(); + } + + public static Stream collectToStream(@NonNull Collection items, @NonNull Function mapper) { + return items.stream() + .map(mapper) + .filter(Objects::nonNull); + } + + public static Stream collectToStream(@NonNull Streamable items, @NonNull Function mapper) { + return items.stream() + .map(mapper) + .filter(Objects::nonNull); + } + + public static Stream collectToStream(@NonNull Stream items, @NonNull Function mapper) { + return items + .map(mapper) + .filter(Objects::nonNull); + } + + public static Map collectToMap( + @NonNull Collection items, + @NonNull Function keyMapper, + @NonNull Function valueMapper + ) { + return items.stream() + .filter(Objects::nonNull) + .collect(Collectors.toMap(keyMapper, valueMapper)); + } + + public static Map collectToMap( + @NonNull Streamable items, + @NonNull Function keyMapper, + @NonNull Function valueMapper + ) { + return items.stream() + .filter(Objects::nonNull) + .collect(Collectors.toMap(keyMapper, valueMapper)); + } + + public static Map collectToMap( + @NonNull Stream items, + @NonNull Function keyMapper, + @NonNull Function valueMapper + ) { + return items + .filter(Objects::nonNull) + .collect(Collectors.toMap(keyMapper, valueMapper)); + } +} diff --git a/src/test/java/it/aboutbits/springboot/toolbox/util/CollectUtilTest.java b/src/test/java/it/aboutbits/springboot/toolbox/util/CollectUtilTest.java new file mode 100644 index 0000000..dfb3a17 --- /dev/null +++ b/src/test/java/it/aboutbits/springboot/toolbox/util/CollectUtilTest.java @@ -0,0 +1,481 @@ +package it.aboutbits.springboot.toolbox.util; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.data.util.Streamable; + +import java.util.Arrays; +import java.util.Collections; +import java.util.function.Function; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class CollectUtilTest { + @Nested + class CollectToSet { + @Test + @DisplayName("Should convert Collection to Set using mapper") + void shouldConvertCollectionToSetUsingMapper() { + // given + var numbers = Arrays.asList(1, 2, 3, 4, 5); + var mapper = (Function) Object::toString; + + // when + var result = CollectUtil.collectToSet(numbers, mapper); + + // then + assertThat(result).containsExactlyInAnyOrder("1", "2", "3", "4", "5"); + } + + @Test + @DisplayName("Should convert Collection to Set using mapper") + void shouldConvertCollectionToSetUsingMapperAndFilterNullValues() { + // given + var numbers = Arrays.asList("a", "b", "c", "d"); + var mapper = (Function) Object::toString; + + // when + var result = CollectUtil.collectToSet(numbers, mapper); + + // then + assertThat(result).containsExactlyInAnyOrder("a", "b", "c", "d"); + } + + @Test + @DisplayName("Should handle empty Collection when converting to Set") + void shouldHandleEmptyCollectionWhenConvertingToSet() { + // given + var emptyList = Collections.emptyList(); + var mapper = (Function) Object::toString; + + // when + var result = CollectUtil.collectToSet(emptyList, mapper); + + // then + assertTrue(result.isEmpty()); + } + + @Test + @DisplayName("Should remove duplicates when converting Collection to Set") + void shouldRemoveDuplicatesWhenConvertingCollectionToSet() { + // given + var numbersWithDuplicates = Arrays.asList(1, 2, 2, 3, 3, 3); + var mapper = (Function) Object::toString; + + // when + var result = CollectUtil.collectToSet(numbersWithDuplicates, mapper); + + // then + assertThat(result).containsExactlyInAnyOrder("1", "2", "3"); + assertEquals(3, result.size()); + } + + @Test + @DisplayName("Should convert Streamable to Set using mapper") + void shouldConvertStreamableToSetUsingMapper() { + // given + var numbers = Streamable.of(1, 2, 3, 4, 5); + var mapper = (Function) Object::toString; + + // when + var result = CollectUtil.collectToSet(numbers, mapper); + + // then + assertThat(result).containsExactlyInAnyOrder("1", "2", "3", "4", "5"); + } + + @Test + @DisplayName("Should convert Streamable to Set using mapper") + void shouldConvertStreamableToSetUsingMapperAndFilterNullValues() { + // given + var numbers = Streamable.of("a", "b", "c", "d"); + var mapper = (Function) Object::toString; + + // when + var result = CollectUtil.collectToSet(numbers, mapper); + + // then + assertThat(result).containsExactlyInAnyOrder("a", "b", "c", "d"); + } + + @Test + @DisplayName("Should handle empty Streamable when converting to Set") + void shouldHandleEmptyStreamableWhenConvertingToSet() { + // given + var emptyStreamable = Streamable.empty(); + var mapper = (Function) Object::toString; + + // when + var result = CollectUtil.collectToSet(emptyStreamable, mapper); + + // then + assertTrue(result.isEmpty()); + } + + @Test + @DisplayName("Should convert Stream to Set using mapper") + void shouldConvertStreamToSetUsingMapper() { + // given + var numbers = java.util.stream.Stream.of(1, 2, 3, 4, 5); + var mapper = (Function) Object::toString; + + // when + var result = CollectUtil.collectToSet(numbers, mapper); + + // then + assertThat(result).containsExactlyInAnyOrder("1", "2", "3", "4", "5"); + } + + @Test + @DisplayName("Should handle empty Stream when converting to Set") + void shouldHandleEmptyStreamWhenConvertingToSet() { + // given + var emptyStream = java.util.stream.Stream.empty(); + var mapper = (Function) Object::toString; + + // when + var result = CollectUtil.collectToSet(emptyStream, mapper); + + // then + assertTrue(result.isEmpty()); + } + + @Test + @DisplayName("Should remove duplicates when converting Stream to Set") + void shouldRemoveDuplicatesWhenConvertingStreamToSet() { + // given + var numbersWithDuplicates = java.util.stream.Stream.of(1, 2, 2, 3, 3, 3); + var mapper = (Function) Object::toString; + + // when + var result = CollectUtil.collectToSet(numbersWithDuplicates, mapper); + + // then + assertThat(result).containsExactlyInAnyOrder("1", "2", "3"); + assertEquals(3, result.size()); + } + } + + @Nested + class CollectToList { + @Test + @DisplayName("Should convert Collection to List using mapper") + void shouldConvertCollectionToListUsingMapper() { + // given + var numbers = Arrays.asList(1, 2, 3, 4, 5); + var mapper = (Function) Object::toString; + + // when + var result = CollectUtil.collectToList(numbers, mapper); + + // then + assertThat(result).containsExactly("1", "2", "3", "4", "5"); + } + + @Test + @DisplayName("Should handle empty Collection when converting to List") + void shouldHandleEmptyCollectionWhenConvertingToList() { + // given + var emptyList = Collections.emptyList(); + var mapper = (Function) Object::toString; + + // when + var result = CollectUtil.collectToList(emptyList, mapper); + + // then + assertTrue(result.isEmpty()); + } + + @Test + @DisplayName("Should preserve duplicates when converting Collection to List") + void shouldPreserveDuplicatesWhenConvertingCollectionToList() { + // given + var numbersWithDuplicates = Arrays.asList(1, 2, 2, 3, 3, 3); + var mapper = (Function) Object::toString; + + // when + var result = CollectUtil.collectToList(numbersWithDuplicates, mapper); + + // then + assertThat(result).containsExactly("1", "2", "2", "3", "3", "3"); + assertEquals(6, result.size()); + } + + @Test + @DisplayName("Should convert Streamable to List using mapper") + void shouldConvertStreamableToListUsingMapper() { + // given + var numbers = Streamable.of(1, 2, 3, 4, 5); + var mapper = (Function) Object::toString; + + // when + var result = CollectUtil.collectToList(numbers, mapper); + + // then + assertThat(result).containsExactly("1", "2", "3", "4", "5"); + } + + @Test + @DisplayName("Should handle empty Streamable when converting to List") + void shouldHandleEmptyStreamableWhenConvertingToList() { + // given + var emptyStreamable = Streamable.empty(); + var mapper = (Function) Object::toString; + + // when + var result = CollectUtil.collectToList(emptyStreamable, mapper); + + // then + assertTrue(result.isEmpty()); + } + + @Test + @DisplayName("Should convert Stream to List using mapper") + void shouldConvertStreamToListUsingMapper() { + // given + var numbers = java.util.stream.Stream.of(1, 2, 3, 4, 5); + var mapper = (Function) Object::toString; + + // when + var result = CollectUtil.collectToList(numbers, mapper); + + // then + assertThat(result).containsExactly("1", "2", "3", "4", "5"); + } + + @Test + @DisplayName("Should handle empty Stream when converting to List") + void shouldHandleEmptyStreamWhenConvertingToList() { + // given + var emptyStream = java.util.stream.Stream.empty(); + var mapper = (Function) Object::toString; + + // when + var result = CollectUtil.collectToList(emptyStream, mapper); + + // then + assertTrue(result.isEmpty()); + } + + @Test + @DisplayName("Should preserve duplicates when converting Stream to List") + void shouldPreserveDuplicatesWhenConvertingStreamToList() { + // given + var numbersWithDuplicates = java.util.stream.Stream.of(1, 2, 2, 3, 3, 3); + var mapper = (Function) Object::toString; + + // when + var result = CollectUtil.collectToList(numbersWithDuplicates, mapper); + + // then + assertThat(result).containsExactly("1", "2", "2", "3", "3", "3"); + assertEquals(6, result.size()); + } + } + + @Nested + class CollectToStream { + @Test + @DisplayName("Should convert Collection to Stream using mapper") + void shouldConvertCollectionToStreamUsingMapper() { + // given + var numbers = Arrays.asList(1, 2, 3, 4, 5); + var mapper = (Function) Object::toString; + + // when + var resultStream = CollectUtil.collectToStream(numbers, mapper); + var result = resultStream.toList(); + + // then + assertThat(result).containsExactly("1", "2", "3", "4", "5"); + } + + @Test + @DisplayName("Should handle empty Collection when converting to Stream") + void shouldHandleEmptyCollectionWhenConvertingToStream() { + // given + var emptyList = Collections.emptyList(); + var mapper = (Function) Object::toString; + + // when + var resultStream = CollectUtil.collectToStream(emptyList, mapper); + var result = resultStream.toList(); + + // then + assertTrue(result.isEmpty()); + } + + @Test + @DisplayName("Should convert Streamable to Stream using mapper") + void shouldConvertStreamableToStreamUsingMapper() { + // given + var numbers = Streamable.of(1, 2, 3, 4, 5); + var mapper = (Function) Object::toString; + + // when + var resultStream = CollectUtil.collectToStream(numbers, mapper); + var result = resultStream.toList(); + + // then + assertThat(result).containsExactly("1", "2", "3", "4", "5"); + } + + @Test + @DisplayName("Should handle empty Streamable when converting to Stream") + void shouldHandleEmptyStreamableWhenConvertingToStream() { + // given + var emptyStreamable = Streamable.empty(); + var mapper = (Function) Object::toString; + + // when + var resultStream = CollectUtil.collectToStream(emptyStreamable, mapper); + var result = resultStream.toList(); + + // then + assertTrue(result.isEmpty()); + } + + @Test + @DisplayName("Should convert Stream to Stream using mapper") + void shouldConvertStreamToStreamUsingMapper() { + // given + var numbers = java.util.stream.Stream.of(1, 2, 3, 4, 5); + var mapper = (Function) Object::toString; + + // when + var resultStream = CollectUtil.collectToStream(numbers, mapper); + var result = resultStream.toList(); + + // then + assertThat(result).containsExactly("1", "2", "3", "4", "5"); + } + + @Test + @DisplayName("Should handle empty Stream when converting to Stream") + void shouldHandleEmptyStreamWhenConvertingToStream() { + // given + var emptyStream = java.util.stream.Stream.empty(); + var mapper = (Function) Object::toString; + + // when + var resultStream = CollectUtil.collectToStream(emptyStream, mapper); + var result = resultStream.toList(); + + // then + assertTrue(result.isEmpty()); + } + } + + @Nested + class CollectToMap { + @Test + @DisplayName("Should convert Collection to Map using key and value mappers") + void shouldConvertCollectionToMapUsingMappers() { + // given + var items = Arrays.asList("a", "bb", "ccc"); + + // when + var result = CollectUtil.collectToMap(items, String::length, Function.identity()); + + // then + assertThat(result) + .containsEntry(1, "a") + .containsEntry(2, "bb") + .containsEntry(3, "ccc") + .hasSize(3); + } + + @Test + @DisplayName("Should throw on duplicate keys according to Collectors.toMap default behavior") + void shouldThrowOnDuplicateKeys() { + // given + var items = Arrays.asList("a", "b"); // both have length of 1 + + // when / then + assertThatIllegalStateException().isThrownBy( + () -> + CollectUtil.collectToMap( + items, + String::length, + Function.identity() + ) + ); + } + + @Test + @DisplayName("Should convert Streamable to Map using key and value mappers") + void shouldConvertStreamableToMapUsingMappers() { + // given + var items = Streamable.of("x", "yy"); + + // when + var result = CollectUtil.collectToMap(items, String::length, Function.identity()); + + // then + assertThat(result) + .containsEntry(1, "x") + .containsEntry(2, "yy") + .hasSize(2); + } + + @Test + @DisplayName("Should convert Stream to Map using key and value mappers") + void shouldConvertStreamToMapUsingMappers() { + // given + var items = Stream.of("m", "nn"); + + // when + var result = CollectUtil.collectToMap(items, String::length, Function.identity()); + + // then + assertThat(result) + .containsEntry(1, "m") + .containsEntry(2, "nn") + .hasSize(2); + } + + @Test + @DisplayName("Should return empty map for empty collection") + void shouldReturnEmptyMapForEmptyCollection() { + // given + var items = Collections.emptyList(); + + // when + var result = CollectUtil.collectToMap(items, String::length, Function.identity()); + + // then + assertThat(result).isEmpty(); + } + + @Test + @DisplayName("Should return empty map for empty streamable") + void shouldReturnEmptyMapForEmptyStreamable() { + // given + var items = Streamable.empty(); + + // when + var result = CollectUtil.collectToMap(items, String::length, Function.identity()); + + // then + assertThat(result).isEmpty(); + } + + @Test + @DisplayName("Should return empty map for empty stream") + void shouldReturnEmptyMapForEmptyStream() { + // given + var items = Stream.empty(); + + // when + var result = CollectUtil.collectToMap(items, String::length, Function.identity()); + + // then + assertThat(result).isEmpty(); + } + } +}