Packrat
Packrat is a Java library that provides various Gatherer implementations for the Stream API. Gatherers can enhance streams with custom intermediate operations.
You can think of the gatherers like of the custom map, filter, etc. stateful operations which you can insert into the usual streams. No need to use external AtomicLong for counting, for example.
This library can be used as is or as a source of inspiration for writing your own gatherers.
Introduction to the Gatherers by Viktor Klang
Availability
Requires JDK 24 or later.
JDK 22 and 23 support gatherers as a preview feature.
Usage
Maven
<dependency>
<groupId>io.github.jhspetersson</groupId>
<artifactId>packrat</artifactId>
<version>0.2.1</version>
</dependency>
Gradle
implementation("io.github.jhspetersson:packrat:0.2.1")
Gatherers
Filtering and mapping operations
| Name | Description |
|---|---|
| distinctBy | Distinct values with custom mapper |
| filterBy | Filter with custom mapper and (optionally) predicate |
| filterEntries | Filter Map.Entry elements using a BiPredicate on key and value |
| removeBy | Remove with custom mapper and (optionally) predicate |
| removeEntries | Remove Map.Entry elements using a BiPredicate on key and value |
| removeDuplicates | Removes consecutive duplicates from a stream |
| flatMapIf | Optional flatMap depending on predicate |
| minBy | The smallest element compared after mapping applied |
| maxBy | The greatest element compared after mapping applied |
Sequence operations
| Name | Description |
|---|---|
| increasing | Increasing sequence, other elements dropped |
| increasingOrEqual | Increasing (or equal) sequence, other elements dropped |
| decreasing | Decreasing sequence, other elements dropped |
| decreasingOrEqual | Decreasing (or equal) sequence, other elements dropped |
| reverse | All elements in reverse order |
| rotate | All elements rotated left or right |
| shuffle | All elements in random order |
Mapping with position operations
| Name | Description |
|---|---|
| mapFirst | Maps first element with mapper, other unchanged |
| mapN | Maps n elements, other unchanged |
| skipAndMap | Skips n elements, maps others |
| skipAndMapN | Skips skipN elements, maps mapN others |
| mapWhile | Maps elements using the supplied function while the predicate evaluates to true. |
| mapUntil | Maps elements using the supplied function until the predicate evaluates to false. |
Collection and chunking operations
| Name | Description |
|---|---|
| increasingChunks | Lists of increasing values |
| increasingOrEqualChunks | Lists of increasing or equal values |
| equalChunks | Lists of equal values |
| decreasingChunks | Lists of decreasing values |
| decreasingOrEqualChunks | Lists of decreasing or equal values |
| nCopies | Copies every element n times |
| repeat | Collects the whole stream and repeats it n times |
| atLeast | Distinct values that appear at least n times |
| atMost | Distinct values that appear at most n times |
Indexing and zipping operations
| Name | Description |
|---|---|
| zip | Zips values with zipper, leftovers dropped |
| mapWithIndex or zipWithIndex | Maps/zips values with an increasing index |
| peekWithIndex | Peek at each element with its index |
| filterWithIndex | Filter elements based on their index and a predicate |
| removeWithIndex | Remove elements based on their index and a predicate |
| windowSlidingWithIndex | Returns fixed-size windows of elements along with their indices |
| windowFixedWithIndex | Returns fixed-size non-overlapping windows of elements along with their indices |
Element selection operations
| Name | Description |
|---|---|
| sample | Sample of the specified size |
| randomFilter | Randomly accepts each element with a given probability |
| nth | Takes nth element from the stream |
| dropNth | Drops every nth element from the stream |
| last | Last n elements |
| lastUnique | Last n unique elements |
| lastUniqueBy | Last n unique elements by a mapping function |
| dropLast | Drops last n elements |
Text processing operations
| Name | Description |
|---|---|
| chars | String split by Unicode graphemes |
| words | String split by words |
| sentences | String split by sentences |
Utility operations
| Name | Description |
|---|---|
| asGatherer | Converts Collector into Gatherer |
| identity | Passes elements through unchanged |
Validation operations
| Name | Description |
|---|---|
| throwIfNotIncreasing | Validates that stream is strictly increasing, throws on violation |
| throwIfNotIncreasingBy | Validates that stream is strictly increasing, uses mapping function, throws on violation |
| throwIfNotIncreasingOrEqual | Validates that stream is non-decreasing, throws on violation |
| throwIfNotIncreasingOrEqualBy | Validates that stream is non-decreasing, uses mapping function, throws on violation |
| throwIfNotDecreasing | Validates that stream is strictly decreasing, throws on violation |
| throwIfNotDecreasingBy | Validates that stream is strictly decreasing, uses mapping function, throws on violation |
| throwIfNotDecreasingOrEqual | Validates that stream is non-increasing, throws on violation |
| throwIfNotDecreasingOrEqualBy | Validates that stream is non-increasing, uses mapping function, throws on violation |
Filtering and mapping operations
distinctBy
distinctBy(mapper) - returns elements with distinct values that result from a mapping by the supplied function
import static io.github.jhspetersson.packrat.Packrat.distinctBy;
var oneOddOneEven = IntStream.range(1, 10).boxed().gather(distinctBy(i -> i % 2)).toList();
System.out.println(oneOddOneEven);
[1, 2]
filterBy
filterBy(mapper, value) - filters mapped elements based on the equality to the value, stream continues with original elements
import static io.github.jhspetersson.packrat.Packrat.filterBy;
var oneDigitNumbers = IntStream.range(0, 100).boxed().gather(filterBy(i -> i.toString().length(), 1)).toList();
System.out.println(oneDigitNumbers);
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
filterBy(mapper, value, predicate) - filters mapped elements based on the predicate test against the value, stream continues with original elements
import static io.github.jhspetersson.packrat.Packrat.filterBy;
var ffValue = IntStream.range(0, 1000).boxed().gather(filterBy(Integer::toHexString, "ff", String::equalsIgnoreCase)).toList();
System.out.println(ffValue);
[255]
filterEntries
filterEntries(predicate) - filters Map.Entry elements using a BiPredicate that tests the key and value of each entry
import static io.github.jhspetersson.packrat.Packrat.filterEntries;
var map = Map.of("one", 1, "two", 2, "three", 3, "four", 4, "five", 5);
// Filter entries where the value is even
var evenValues = map.entrySet().stream()
.gather(filterEntries((key, value) -> value % 2 == 0))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
System.out.println(evenValues);
removeBy
removeBy(mapper, value) - removes mapped elements based on the equality to the value, stream continues with original elements
import static io.github.jhspetersson.packrat.Packrat.removeBy;
var oneDigitNumbers = IntStream.range(0, 100).boxed().gather(removeBy(i -> i.toString().length(), 2)).toList();
System.out.println(oneDigitNumbers);
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
removeBy(mapper, value, predicate) - removes mapped elements based on the predicate test against the value, stream continues with original elements
import static io.github.jhspetersson.packrat.Packrat.removeBy;
var ageDivisibleByThree = getEmployees().gather(removeBy(emp -> emp.age() % 3, 0, (i, value) -> !Objects.equals(i, value))).toList();
System.out.println(ageDivisibleByThree);
[Employee[name=Mark Bloom, age=21], Employee[name=Rebecca Schneider, age=24]]
removeEntries
removeEntries(predicate) - removes Map.Entry elements using a BiPredicate that tests the key and value of each entry
import static io.github.jhspetersson.packrat.Packrat.removeEntries;
var map = Map.of("one", 1, "two", 2, "three", 3, "four", 4, "five", 5);
// Remove entries where the value is even
var oddValues = map.entrySet().stream()
.gather(removeEntries((key, value) -> value % 2 == 0))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
System.out.println(oddValues);
removeDuplicates
removeDuplicates() - removes consecutive duplicates from a stream, only adjacent elements that are equal will be considered duplicates
import static io.github.jhspetersson.packrat.Packrat.removeDuplicates;
var listWithCopies = List.of(0, 1, 2, 2, 3, 4, 5, 5, 6, 7, 8, 8, 8, 9, 8, 7, 7, 6, 5, 4, 4, 4, 3, 2, 1, 0);
var unique = listWithCopies.stream().gather(removeDuplicates()).toList();
System.out.println(unique);
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
removeDuplicatesBy
removeDuplicatesBy(mapper) - removes consecutive duplicates from a stream based on a mapping function, only adjacent elements that have equal mapped values will be considered duplicates
import static io.github.jhspetersson.packrat.Packrat.removeDuplicatesBy;
var people = List.of(
new Person("John", 25),
new Person("Alice", 30),
new Person("Bob", 30),
new Person("Charlie", 30),
new Person("David", 40),
new Person("Eve", 40)
);
var uniqueByAge = people.stream().gather(removeDuplicatesBy(Person::age)).toList();
System.out.println(uniqueByAge);
[Person[name=John, age=25], Person[name=Alice, age=30], Person[name=David, age=40]]
flatMapIf
flatMapIf(mapper, predicate) - optionally flattens elements mapped to streams depending on the supplied predicate
import static io.github.jhspetersson.packrat.Packrat.flatMapIf;
var strings = Stream.of("A", "BC", "DEF");
var result = strings.gather(flatMapIf(s -> Arrays.stream(s.split("")), s -> s.length() >= 3)).toList();
System.out.println(result);
[A, BC, D, E, F]
minBy
minBy(mapper) - returns the smallest element in the stream, comparing is done after mapping function applied.
import static io.github.jhspetersson.packrat.Packrat.minBy;
var check = Stream.of("2", "1", "-12", "22", "10").gather(minBy(Long::parseLong)).toList();
System.out.println(check);
[-12]
However, resulting list contains an original element of type String;
minBy(mapper, comparator) - returns the smallest element in the stream, comparing with given comparator is done after mapping function applied.
[!CAUTION] This gatherer will consume the entire stream before producing any output.
maxBy
maxBy(mapper) - returns the greatest element in the stream, comparing is done after mapping function applied.
import static io.github.jhspetersson.packrat.Packrat.maxBy;
var check = Stream.of("2", "1", "-12", "22", "10").gather(maxBy(Long::parseLong)).toList();
System.out.println(check);
[22]
However, resulting list contains an original element of type String;
maxBy(mapper, comparator) - returns the greatest element in the stream, comparing with given comparator is done after mapping function applied.
[!CAUTION] This gatherer will consume the entire stream before producing any output.
Sequence operations
increasing
increasing() - returns elements in an increasing sequence, elements out of the sequence, as well as repeating values, are dropped
import static io.github.jhspetersson.packrat.Packrat.increasing;
var numbers = Stream.of(1, 2, 2, 5, 4, 2, 6, 9, 3, 11, 0, 1, 20);
var increasingNumbers = numbers.gather(increasing()).toList();
System.out.println(increasingNumbers);
[1, 2, 5, 6, 9, 11, 20]
increasingOrEqual
increasingOrEqual() - returns elements in an increasing sequence, repeating values are preserved, elements out of the sequence are dropped
decreasing
decreasing() - returns elements in a decreasing sequence, elements out of the sequence, as well as repeating values, are dropped
decreasingOrEqual
decreasingOrEqual() - returns elements in a decreasing sequence, repeating values are preserved, elements out of the sequence are dropped
reverse
reverse() - reverses the elements
import static io.github.jhspetersson.packrat.Packrat.reverse;
var reverseOrdered = IntStream.range(0, 10).boxed().gather(reverse()).toList();
System.out.println(reverseOrdered);
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
[!CAUTION] This gatherer will consume the entire stream before producing any output.
rotate
rotate(distance) - rotates the elements
import static io.github.jhspetersson.packrat.Packrat.rotate;
var positiveRotation = IntStream.range(0, 10).boxed().gather(rotate(3)).toList();
System.out.println(positiveRotation);
var negativeRotation = IntStream.range(0, 10).boxed().gather(rotate(-4)).toList();
System.out.println(negativeRotation);
[7, 8, 9, 0, 1, 2, 3, 4, 5, 6]
[4, 5, 6, 7, 8, 9, 0, 1, 2, 3]
[!CAUTION] This gatherer will consume the entire stream before producing any output.
shuffle
shuffle() - shuffle the elements
import static io.github.jhspetersson.packrat.Packrat.shuffle;
var randomlyOrdered = IntStream.range(0, 10).boxed().gather(shuffle()).toList();
System.out.println(randomlyOrdered);
[2, 7, 6, 9, 8, 5, 1, 3, 0, 4]
[!CAUTION] This gatherer will consume the entire stream before producing any output.
Mapping with position operations
mapFirst
mapFirst(mapper) - returns all elements, the first element is mapped with the supplied mapping function
import static io.github.jhspetersson.packrat.Packrat.mapFirst;
var mapped = IntStream.rangeClosed(1, 10).boxed().gather(mapFirst(n -> n * 10)).toList();
System.out.println(mapped);
[10, 2, 3, 4, 5, 6, 7, 8, 9, 10]
mapN
mapN(n, mapper) - returns all elements, the first n elements are mapped with the supplied mapping function
import static io.github.jhspetersson.packrat.Packrat.mapN;
var mapped = IntStream.rangeClosed(1, 10).boxed().gather(mapN(5, n -> n * 10)).toList();
System.out.println(mapped);
[10, 20, 30, 40, 50, 6, 7, 8, 9, 10]
skipAndMap
skipAndMap(n, mapper) - returns all elements that after the first n are mapped with the supplied mapping function
import static io.github.jhspetersson.packrat.Packrat.skipAndMap;
var mapped = IntStream.rangeClosed(1, 10).boxed().gather(skipAndMap(3, n -> n * 10)).toList();
System.out.println(mapped);
[1, 2, 3, 40, 50, 60, 70, 80, 90, 100]
skipAndMapN
skipAndMapN(skipN, mapN, mapper) - returns all elements, after skipN elements the first mapN elements are mapped with the supplied mapping function
import static io.github.jhspetersson.packrat.Packrat.skipAndMapN;
var mapped = IntStream.rangeClosed(1, 10).boxed().gather(skipAndMapN(3, 5, n -> n * 10)).toList();
System.out.println(mapped);
[1, 2, 3, 40, 50, 60, 70, 80, 9, 10]
mapWhile
mapWhile(predicate, mapper) - maps elements using the supplied function while the predicate evaluates to true
import static io.github.jhspetersson.packrat.Packrat.mapWhile;
var numbers = IntStream.rangeClosed(1, 10).boxed().gather(mapWhile(n -> n * 10, n -> n < 5)).toList();
System.out.println(numbers);
[10, 20, 30, 40, 5, 6, 7, 8, 9, 10]
mapUntil
mapUntil(predicate, mapper) - maps elements using the supplied function until the predicate evaluates to false
import static io.github.jhspetersson.packrat.Packrat.mapUntil;
var numbers = IntStream.rangeClosed(1, 10).boxed().gather(mapUntil(n -> n * 10, n -> n == 5)).toList();
System.out.println(numbers);
[10, 20, 30, 40, 5, 6, 7, 8, 9, 10]
Collection and chunking operations
increasingChunks
increasingChunks() - returns lists ("chunks") of elements, where each next element is greater than the previous one
import static io.github.jhspetersson.packrat.Packrat.increasingChunks;
var numbers = Stream.of(1, 2, 2, 5, 4, 2, 6, 9, 3, 11, 0, 1, 20);
var result = numbers.gather(increasingChunks()).toList();
System.out.println(result);
[[1, 2], [2, 5], [4], [2, 6, 9], [3, 11], [0, 1, 20]]
increasingOrEqualChunks
increasingOrEqualChunks() - returns lists ("chunks") of elements, where each next element is greater or equal than the previous one
import static io.github.jhspetersson.packrat.Packrat.increasingOrEqualChunks;
var numbers = Stream.of(1, 2, 2, 5, 4, 2, 6, 9, 3, 11, 0, 1, 20);
var result = numbers.gather(increasingOrEqualChunks()).toList();
System.out.println(result);
[[1, 2, 2, 5], [4], [2, 6, 9], [3, 11], [0, 1, 20]]
equalChunks
equalChunks() - returns lists ("chunks") of elements, where all elements in a chunk are equal to each other
import static io.github.jhspetersson.packrat.Packrat.equalChunks;
var numbers = Stream.of(1, 1, 2, 2, 2, 3, 4, 4, 5, 5, 5, 5, 6);
var result = numbers.gather(equalChunks()).toList();
System.out.println(result);
[[1, 1], [2, 2, 2], [3], [4, 4], [5, 5, 5, 5], [6]]
equalChunksBy(mapper) - returns lists ("chunks") of elements, where all elements in a chunk have equal values after applying the mapper function
import static io.github.jhspetersson.packrat.Packrat.equalChunks;
var strings = Stream.of("apple", "apricot", "banana", "blueberry", "cherry", "date");
var result = strings.gather(equalChunks(s -> s.charAt(0))).toList();
System.out.println(result);
[[apple, apricot], [banana, blueberry], [cherry], [date]]
equalChunks(comparator) - returns lists ("chunks") of elements, where all elements in a chunk are equal according to the supplied comparator
import static io.github.jhspetersson.packrat.Packrat.equalChunks;
// Case-insensitive string comparison
var strings = Stream.of("Apple", "apple", "Banana", "banana", "Cherry", "cherry");
var result = strings.gather(equalChunks(String.CASE_INSENSITIVE_ORDER)).toList();
System.out.println(result);
[[Apple, apple], [Banana, banana], [Cherry, cherry]]
equalChunksBy(mapper, comparator) - returns lists ("chunks") of elements, where all elements in a chunk have equal values after applying the mapper function, with equality determined by the supplied comparator
import static io.github.jhspetersson.packrat.Packrat.equalChunks;
record Person(String name, String id) {}
// Group people by the first letter of their ID, case-insensitive
var people = Stream.of(
new Person("John", "A123"),
new Person("Alice", "a456"),
new Person("Bob", "B789"),
new Person("Charlie", "b012"),
new Person("David", "C345"),
new Person("Eve", "c678")
);
var result = people.gather(equalChunks(
p -> p.id().substring(0, 1), // Map to first letter of ID
String.CASE_INSENSITIVE_ORDER // Compare case-insensitive
)).toList();
System.out.println(result);
[[Person[name=John, id=A123], Person[name=Alice, id=a456]], [Person[name=Bob, id=B789], Person[name=Charlie, id=b012]], [Person[name=David, id=C345], Person[name=Eve, id=c678]]]
decreasingChunks
decreasingChunks() - returns lists ("chunks") of elements, where each next element is less than the previous one
decreasingOrEqualChunks
decreasingOrEqualChunks() - returns lists ("chunks") of elements, where each next element is less or equal than the previous one
nCopies
nCopies(n) - returns n copies of every element, n less than or equal to zero effectively empties the stream
import static io.github.jhspetersson.packrat.Packrat.nCopies;
var numbers = IntStream.of(5).boxed().gather(nCopies(10)).toList();
System.out.println(numbers);
[5, 5, 5, 5, 5, 5, 5, 5, 5, 5]
repeat
repeat(n) - collects the whole stream and repeats it n times, n equal to zero effectively empties the stream
import static io.github.jhspetersson.packrat.Packrat.repeat;
var numbers = Stream.of(1, 2, 3).gather(repeat(2)).toList();
System.out.println(numbers);
[1, 2, 3, 1, 2, 3]
[!CAUTION] This gatherer will consume the entire stream before producing any output.
atLeast
atLeast(n) - returns distinct elements that appear at least n times in the stream
import static io.github.jhspetersson.packrat.Packrat.atLeast;
var numbers = Stream.of(1, 2, 3, 3, 3, 4, 5, 5, 6, 7, 8, 8, 8, 8, 9, 10);
var atLeastThree = numbers.gather(atLeast(3)).toList();
System.out.println(atLeastThree);
[3, 3, 3, 8, 8, 8, 8]
atMost
atMost(n) - returns distinct elements that appear at most n times in the stream
import static io.github.jhspetersson.packrat.Packrat.atMost;
var numbers = Stream.of(1, 2, 3, 3, 3, 4, 5, 5, 6, 7, 8, 8, 8, 8, 9, 10);
var atMostTwo = numbers.gather(atMost(2)).toList();
System.out.println(atMostTwo);
[1, 2, 4, 5, 5, 6, 7, 9, 10]
atMostBy(n, mapper) - returns distinct elements mapped by the supplied function that appear at most n times in the stream
import static io.github.jhspetersson.packrat.Packrat.atMostBy;
var strings = Stream.of("apple", "banana", "cherry", "date", "elderberry", "fig", "grape");
var uniqueLengths = strings.gather(atMostBy(1, String::length)).toList();
System.out.println(uniqueLengths);
[date, elderberry, fig]
[!CAUTION] This gatherer will consume the entire stream before producing any output.
Indexing and zipping operations
zip
zip(input, mapper) - returns elements mapped ("zipped") with the values from some other stream, iterable or iterator.
import static io.github.jhspetersson.packrat.Packrat.zip;
var names = List.of("Anna", "Mike", "Sandra");
var ages = Stream.of(20, 30, 40, 50, 60, 70, 80, 90);
var users = names.stream().gather(zip(ages, User::new)).toList();
System.out.println(users);
[User[name=Anna, age=20], User[name=Mike, age=30], User[name=Sandra, age=40]]
zip(input) - zips current stream and input into Map entries.
import static io.github.jhspetersson.packrat.Packrat.zip;
var names = List.of("Anna", "Mike", "Sandra");
var ages = Stream.of(20, 30, 40);
var users = names.stream().gather(zip(ages)).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
System.out.println(users);
zipWithIndex
zipWithIndex() - zips current stream with an increasing index into Map entries.
import static io.github.jhspetersson.packrat.Packrat.zipWithIndex;
var names = List.of("Anna", "Mike", "Sandra");
var users = names.stream().gather(zipWithIndex()).toList();
[0=Anna, 1=Mike, 2=Sandra]
zipWithIndex(startIndex) - zips current stream with an increasing index (beginning with startIndex) into Map entries.
import static io.github.jhspetersson.packrat.Packrat.zipWithIndex;
var names = List.of("Anna", "Mike", "Sandra");
var users = names.stream().gather(zipWithIndex(10)).toList();
[10=Anna, 11=Mike, 12=Sandra]
mapWithIndex(mapper) or zipWithIndex(mapper) - maps/zips current stream with an increasing index, mapping function receives the index as the first argument.
import static io.github.jhspetersson.packrat.Packrat.zipWithIndex;
var names = List.of("Anna", "Mike", "Sandra");
var users = names.stream().gather(zipWithIndex(User::new)).toList();
[User[index=0, name=Anna], User[index=1, name=Mike], User[index=2, name=Sandra]]
mapWithIndex(mapper, startIndex) or zipWithIndex(mapper, startIndex) - maps/zips current stream with an increasing index (beginning with startIndex), mapping function receives the index as the first argument.
import static io.github.jhspetersson.packrat.Packrat.zipWithIndex;
var names = List.of("Anna", "Mike", "Sandra");
var users = names.stream().gather(zipWithIndex(User::new, 10)).toList();
[User[index=10, name=Anna], User[index=11, name=Mike], User[index=12, name=Sandra]]
peekWithIndex
peekWithIndex(consumer) - peeks at each element along with its index (starting from 0), but passes the original element downstream unchanged
import static io.github.jhspetersson.packrat.Packrat.peekWithIndex;
var names = List.of("Anna", "Mike", "Sandra");
var result = names.stream().gather(peekWithIndex((index, name) ->
System.out.println("Element at index " + index + ": " + name))).toList();
System.out.println(result);
Element at index 0: Anna Element at index 1: Mike Element at index 2: Sandra [Anna, Mike, Sandra]
peekWithIndex(consumer, startIndex) - peeks at each element along with its index (beginning with startIndex), but passes the original element downstream unchanged
import static io.github.jhspetersson.packrat.Packrat.peekWithIndex;
var names = List.of("Anna", "Mike", "Sandra");
var result = names.stream().gather(peekWithIndex((index, name) ->
System.out.println("Element at index " + index + ": " + name), 10)).toList();
System.out.println(result);
Element at index 10: Anna Element at index 11: Mike Element at index 12: Sandra [Anna, Mike, Sandra]
filterWithIndex
filterWithIndex(predicate) - filters elements based on their index and a predicate, the index starts from 0
import static io.github.jhspetersson.packrat.Packrat.filterWithIndex;
var numbers = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
var result = numbers.gather(filterWithIndex((index, element) -> index % 2 == 0)).toList();
System.out.println(result);
[1, 3, 5, 7, 9]
filterWithIndex(predicate, startIndex) - filters elements based on their index and a predicate, the index starts from startIndex
import static io.github.jhspetersson.packrat.Packrat.filterWithIndex;
var numbers = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
var result = numbers.gather(filterWithIndex((index, element) -> index % 2 == 0, 1)).toList();
System.out.println(result);
[2, 4, 6, 8, 10]
removeWithIndex
removeWithIndex(predicate) - removes elements based on their index and a predicate, the index starts from 0
import static io.github.jhspetersson.packrat.Packrat.removeWithIndex;
var numbers = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
var result = numbers.gather(removeWithIndex((index, element) -> index % 2 == 0)).toList();
System.out.println(result);
[2, 4, 6, 8, 10]
removeWithIndex(predicate, startIndex) - removes elements based on their index and a predicate, the index starts from startIndex
import static io.github.jhspetersson.packrat.Packrat.removeWithIndex;
var numbers = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
var result = numbers.gather(removeWithIndex((index, element) -> index % 2 == 0, 1)).toList();
System.out.println(result);
[1, 3, 5, 7, 9]
windowSlidingWithIndex
windowSlidingWithIndex(windowSize) - returns fixed-size windows of elements along with their indices, the index starts from 0
import static io.github.jhspetersson.packrat.Packrat.windowSlidingWithIndex;
var numbers = IntStream.rangeClosed(1, 5).boxed();
var result = numbers.gather(windowSlidingWithIndex(3)).toList();
System.out.println(result);
[0=[1, 2, 3], 1=[2, 3, 4], 2=[3, 4, 5]]
windowSlidingWithIndex(windowSize, startIndex) - returns fixed-size windows of elements along with their indices, the index starts from startIndex
import static io.github.jhspetersson.packrat.Packrat.windowSlidingWithIndex;
var numbers = IntStream.rangeClosed(1, 5).boxed();
var result = numbers.gather(windowSlidingWithIndex(3, 10)).toList();
System.out.println(result);
[10=[1, 2, 3], 11=[2, 3, 4], 12=[3, 4, 5]]
windowSlidingWithIndex(windowSize, mapper) - returns fixed-size windows of elements along with their indices
windowSlidingWithIndex(windowSize, mapper, startIndex) - returns fixed-size windows of elements along with their indices, the index starts from startIndex
windowFixedWithIndex
windowFixedWithIndex(windowSize) - returns fixed-size non-overlapping windows of elements along with their indices, the index starts from 0
import static io.github.jhspetersson.packrat.Packrat.windowFixedWithIndex;
var numbers = IntStream.rangeClosed(1, 10).boxed();
var result = numbers.gather(windowFixedWithIndex(3)).toList();
System.out.println(result);
[0=[1, 2, 3], 1=[4, 5, 6], 2=[7, 8, 9]]
windowFixedWithIndex(windowSize, startIndex) - returns fixed-size non-overlapping windows of elements along with their indices, the index starts from startIndex
import static io.github.jhspetersson.packrat.Packrat.windowFixedWithIndex;
var numbers = IntStream.rangeClosed(1, 6).boxed();
var result = numbers.gather(windowFixedWithIndex(2, 10)).toList();
System.out.println(result);
[10=[1, 2], 11=[3, 4], 12=[5, 6]]
windowFixedWithIndex(windowSize, mapper) - returns fixed-size non-overlapping windows of elements along with their indices
windowFixedWithIndex(windowSize, mapper, startIndex) - returns fixed-size non-overlapping windows of elements along with their indices, the index starts from startIndex
Element selection operations
sample
sample(n) - returns a sample of the specified size from the stream of elements.
import static io.github.jhspetersson.packrat.Packrat.sample;
var source = IntStream.range(0, 100).boxed().gather(sample(10)).toList();
System.out.println(source);
[0, 8, 27, 33, 65, 66, 88, 90, 93, 96]
sample(n, maxSpan) - returns a sample of the specified size from the stream of elements, inspects first maxSpan elements.
randomFilter
randomFilter(probability) - accepts each incoming element independently with the given probability in the inclusive range [0.0, 1.0].
[!NOTE] When
probabilityis 0.0 no elements pass; when it is 1.0 all elements pass. Elements that are not accepted are skipped; the relative order of accepted elements is preserved.
import static io.github.jhspetersson.packrat.Packrat.randomFilter;
var some = IntStream.range(0, 20).boxed().gather(randomFilter(0.3)).toList();
System.out.println(some);
[3, 6, 9, 14, 15, 18]
nth
nth(n) - takes every nth element from the stream
import static io.github.jhspetersson.packrat.Packrat.nth;
var numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
var result = numbers.stream().gather(nth(3)).toList();
System.out.println(result);
[3, 6, 9]
dropNth
dropNth(n) - drops every nth element from the stream
import static io.github.jhspetersson.packrat.Packrat.dropNth;
var numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
var result = numbers.stream().gather(dropNth(3)).toList();
System.out.println(result);
[1, 2, 4, 5, 7, 8, 10]
last
last() - returns last element from the stream.
import static io.github.jhspetersson.packrat.Packrat.last;
var integers = IntStream.range(0, 100).boxed().gather(last()).toList();
System.out.println(integers);
[99]
last(n) - returns n last elements from the stream.
import static io.github.jhspetersson.packrat.Packrat.last;
var integers = IntStream.range(0, 100).boxed().gather(last(10)).toList();
System.out.println(integers);
[90, 91, 92, 93, 94, 95, 96, 97, 98, 99]
[!CAUTION] This gatherer will consume the entire stream before producing any output.
lastUnique
lastUnique(n) - returns n last unique elements from the stream.
import static io.github.jhspetersson.packrat.Packrat.lastUnique;
var integers = List.of(1, 2, 3, 4, 5, 4, 1, 1, 1, 2, 2, 6).stream().gather(lastUnique(3)).toList();
System.out.println(integers);
[1, 2, 6]
[!CAUTION] This gatherer will consume the entire stream before producing any output.
lastUniqueBy
lastUniqueBy(n, mapper) - returns n last unique elements from the stream determined by the supplied mapping function.
import static io.github.jhspetersson.packrat.Packrat.lastUniqueBy;
record Employee(String name, int age) {}
var employees = java.util.List.of(
new Employee("Ann Smith", 35),
new Employee("John Rodgers", 40),
new Employee("Mark Bloom", 21),
new Employee("Rebecca Schneider", 24),
new Employee("Luke Norman", 21)
);
var result = employees.stream().gather(lastUniqueBy(3, Employee::age)).toList();
System.out.println(result);
[Employee[name=John Rodgers, age=40], Employee[name=Mark Bloom, age=21], Employee[name=Rebecca Schneider, age=24]]
[!CAUTION] This gatherer will consume the entire stream before producing any output.
dropLast
dropLast() - drops last element.
dropLast(n) - drops last n elements from the stream.
import static io.github.jhspetersson.packrat.Packrat.dropLast;
var integers = IntStream.range(0, 10).boxed().gather(dropLast(3)).toList();
System.out.println(integers);
[0, 1, 2, 3, 4, 5, 6]
[!CAUTION] This gatherer will consume the entire stream before producing any output.
Text processing operations
chars
chars() - returns characters as strings parsed from the stream elements
import static io.github.jhspetersson.packrat.Packrat.chars;
var charStrings = Stream.of("Hello, \uD83D\uDC22!").gather(chars()).toList();
System.out.println(charStrings);
[H, e, l, l, o, ,, , 🐢, !]
words
words() - returns words as strings parsed from the stream elements
import static io.github.jhspetersson.packrat.Packrat.words;
var wordStrings = Stream.of("Another test!").gather(words()).toList();
System.out.println(wordStrings);
[Another, test, !]
sentences
sentences() - returns sentences as strings parsed from the stream elements
import static io.github.jhspetersson.packrat.Packrat.sentences;
var sentenceStrings = Stream.of("And another one. How many left?").gather(sentences()).toList();
System.out.println(sentenceStrings);
[And another one. , How many left?]
Utility operations
asGatherer
asGatherer(collector) - provides the result of the supplied collector as a single element into the stream, effectively converts any Collector into a Gatherer
import static io.github.jhspetersson.packrat.Packrat.asGatherer;
var numbers = Stream.of(1, 2, 3, 4, 5);
var listOfCollectedList = numbers.gather(asGatherer(Collectors.toList())).toList();
System.out.println(listOfCollectedList);
[[1, 2, 3, 4, 5]]
identity
identity() - returns a gatherer that passes elements through unchanged
import static io.github.jhspetersson.packrat.Packrat.identity;
var numbers = IntStream.range(0, 5).boxed();
var sameNumbers = numbers.gather(identity()).toList();
System.out.println(sameNumbers);
[0, 1, 2, 3, 4]
Validation gatherers
throwIfNotIncreasingOrEqual
throwIfNotIncreasingOrEqual() — validates that the incoming elements are ordered in non-decreasing order, if a violation is detected, an exception is thrown immediately, terminating the pipeline
import static io.github.jhspetersson.packrat.Packrat.throwIfNotIncreasingOrEqual;
var result = Stream.of(1, 1, 2, 3, 5)
.gather(Packrat.throwIfNotIncreasingOrEqual())
.toList();
The stream is ordered, no exception thrown.
throwIfNotIncreasingOrEqualBy
throwIfNotIncreasingOrEqualBy(mapper) — validates that the incoming mapped elements are ordered in non-decreasing order, if a violation is detected, an exception is thrown immediately, terminating the pipeline
import static io.github.jhspetersson.packrat.Packrat.throwIfNotIncreasingOrEqualBy;
record Person(String name, int age) {}
var people = Stream.of(new Person("Ann", 20), new Person("Bob", 25), new Person("Cara", 25));
var ordered = people
.gather(Packrat.throwIfNotIncreasingOrEqualBy(Person::age))
.toList();
The stream is ordered by the age of the employees, no exception thrown.
throwIfNotIncreasing
throwIfNotIncreasing() — validates that the incoming elements are strictly increasing (no equal neighbors), if a violation is detected, an exception is thrown immediately, terminating the pipeline
import static io.github.jhspetersson.packrat.Packrat.throwIfNotIncreasing;
// OK
var ok = Stream.of(1, 2, 3, 5)
.gather(throwIfNotIncreasing())
.toList();
// Violates due to equal neighbors (2, 2)
assertThrows(IllegalStateException.class, () ->
Stream.of(1, 2, 2, 3).gather(throwIfNotIncreasing()).toList());
throwIfNotIncreasingBy
throwIfNotIncreasingBy(mapper) — validates that the incoming elements are strictly increasing by a mapped comparable key (no equal neighbors by the mapped value), if a violation is detected, an exception is thrown immediately, terminating the pipeline
import static io.github.jhspetersson.packrat.Packrat.throwIfNotIncreasingBy;
record Person(String name, int age) {}
// OK — strictly increasing by age
var ok = Stream.of(new Person("Ann", 20), new Person("Bob", 25), new Person("Cara", 30))
.gather(throwIfNotIncreasingBy(Person::age))
.toList();
// Violates due to equal ages (25, 25)
assertThrows(IllegalStateException.class, () ->
Stream.of(new Person("Ann", 20), new Person("Bob", 25), new Person("Cara", 25))
.gather(throwIfNotIncreasingBy(Person::age))
.toList());
throwIfNotDecreasing
throwIfNotDecreasing() — validates that the incoming elements are strictly decreasing (no equal neighbors), if a violation is detected, an exception is thrown immediately, terminating the pipeline
import static io.github.jhspetersson.packrat.Packrat.throwIfNotDecreasing;
// OK
var ok = Stream.of(5, 3, 1).gather(throwIfNotDecreasing()).toList();
// Violates due to 3 -> 4 step
assertThrows(IllegalStateException.class, () ->
Stream.of(5, 3, 4).gather(throwIfNotDecreasing()).toList());
throwIfNotDecreasingBy
throwIfNotDecreasingBy(mapper) — validates that the incoming elements are strictly decreasing by a mapped comparable key (no equal neighbors by the mapped value), if a violation is detected, an exception is thrown immediately, terminating the pipeline
import static io.github.jhspetersson.packrat.Packrat.throwIfNotDecreasingBy;
record Person(String name, int age) {}
// OK — strictly decreasing by age
var ok = Stream.of(new Person("Zoe", 30), new Person("Yuri", 25), new Person("Xena", 20))
.gather(throwIfNotDecreasingBy(Person::age))
.toList();
// Violates due to equal ages (25, 25)
assertThrows(IllegalStateException.class, () ->
Stream.of(new Person("Zoe", 30), new Person("Yuri", 25), new Person("Yara", 25))
.gather(throwIfNotDecreasingBy(Person::age))
.toList());
throwIfNotDecreasingOrEqual
throwIfNotDecreasingOrEqual() — validates that the incoming elements are non-increasing (decreasing or equal), if a violation is detected, an exception is thrown immediately, terminating the pipeline
import static io.github.jhspetersson.packrat.Packrat.throwIfNotDecreasingOrEqual;
// OK (equal allowed)
var ok = Stream.of(5, 5, 3, 3, 1).gather(throwIfNotDecreasingOrEqual()).toList();
// Violates due to 3 -> 4 step
assertThrows(IllegalStateException.class, () ->
Stream.of(5, 3, 4).gather(throwIfNotDecreasingOrEqual()).toList());
throwIfNotDecreasingOrEqualBy
throwIfNotDecreasingOrEqualBy(mapper) — validates that the incoming elements are non-increasing (decreasing or equal) by a mapped comparable key, if a violation is detected, an exception is thrown immediately, terminating the pipeline
import static io.github.jhspetersson.packrat.Packrat.throwIfNotDecreasingOrEqualBy;
record Person(String name, int age) {}
// OK — non-increasing by age (equals allowed)
var ok = Stream.of(new Person("Zoe", 30), new Person("Yuri", 25), new Person("Yara", 25))
.gather(throwIfNotDecreasingOrEqualBy(Person::age))
.toList();
// Violates due to increase by mapped key (25 -> 26)
assertThrows(IllegalStateException.class, () ->
Stream.of(new Person("Zoe", 30), new Person("Yuri", 25), new Person("Xena", 26))
.gather(throwIfNotDecreasingOrEqualBy(Person::age))
.toList());
License
Apache-2.0
Supported by JetBrains IDEA open source license.