# Gatherers4j
A library of useful [Stream Gatherers](https://openjdk.org/jeps/473) (custom intermediate operations) for Java 22+.
# Installing
TBD, once I start publishing snapshots to Maven Central.
# Gatherers In This Library
| Function | Purpose |
|----------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `averageBigDecimals()` | Create a running or trailing average of `BigDecimal` values. See below for options.
See [specific advice on averaging](#averaging-bigdecimal-objects) |
| `averageBigDecimalsBy(fn)` | Create a running avergage of `BigDecimal` values mapped out of some different object via `fn`.
See [specific advice on averaging](#averaging-bigdecimal-objects) |
| `dedupeConsecutive()` | Remove conescutive duplicates from a stream |
| `dedupeConsecutiveBy(fn)` | Remove consecutive duplicates from a stream as returned by `fn` |
| `distinctBy(fn)` | Emit only distinct elements from the stream, as measured by `fn` |
| `interleave(stream)` | Creates a stream of alternating objects from the input stream and the argument stream |
| `last(n)` | Constrain the stream to the last `n` values |
| `withIndex()` | Maps all elements of the stream as-is, along with their 0-based index. |
| `zipWith(stream)` | Creates a stream of `Pair` objects whose values come from the input stream and argument stream |
| `zipWithNext()` | Creates a stream of `List` objects via a sliding window of width 2 and stepping 1 | |
# Use Cases
#### Running average of `Stream`
For more options, please see the [specific advice on averaging](#averaging-bigdecimal-objects).
```java
Stream
.of("1.0", "2.0", "10.0")
.map(BigDecimal::new)
.gather(Gatherers4j.averageBigDecimals())
.toList();
// [1, 1.5, 4.3333333333333333]
```
#### Moving average of `Stream`
For more options, please see the [specific advice on averaging](#averaging-bigdecimal-objects)
```java
Stream
.of("1.0", "2.0", "10.0", "20.0", "30.0")
.map(BigDecimal::new)
.gather(Gatherers4j.averageBigDecimals().simpleMovingAverage(2))
.toList();
// [1.5, 6, 15, 25]
```
#### Remove consecutive duplicate elements
```java
Stream
.of("A", "A", "A", "B", "B", "C", "C", "D", "A", "B", "C")
.gather(Gatherers4j.dedupeConsecutive())
.toList();
// ["A", "B", "C", "D", "A", "B", "C"]
```
#### Remove consecutive duplicate elements, where duplicate is measured by a function
```java
record Person(String firstName, String lastName) {}
Stream
.of(
new Person("Todd", "Ginsberg"),
new Person("Emma", "Ginsberg"),
new Person("Todd", "Smith")
)
.gather(Gatherers4j.dedupeConsecutiveBy(Person::lastName))
.toList();
// [Person("Todd", "Ginsberg"), Person("Todd", "Smith")]
```
#### Remove duplicate elements, where duplicate is measured by a function
```java
record Person(String firstName, String lastName) {}
Stream
.of(
new Person("Todd", "Ginsberg"),
new Person("Emma", "Ginsberg"),
new Person("Todd", "Smith")
)
.gather(Gatherers4j.distinctBy(Person::firstName))
.toList();
// [Person("Todd", "Ginsberg"), Person("Emma", "Ginsberg")]
```
#### Interleave streams of the same type into one stream
```java
final Stream left = Stream.of("A", "B", "C");
final Stream right = Stream.of("D", "E", "F");
left.gather(Gatherers4j.interleave(right)).toList();
// ["A", "D", "B", "E", "C", "F"]
```
#### Limit the stream to the `last` _n_ elements
```java
Stream
.of("A", "B", "C", "D", "E", "F", "G")
.gather(Gatherers4j.last(3))
.toList();
// ["E", "F", "G"]
```
#### Include index with original stream values
```java
Stream
.of("A", "B", "C")
.gather(Gatherers4j.withIndex())
.toList();
// [IndexedValue(0, "A"), IndexedValue(1, "B"), IndexedValue(2, "C")]
```
#### Zip two streams of together into a `Stream`
The left and right streams can be of different types.
```java
final Stream left = Stream.of("A", "B", "C");
final Stream right = Stream.of(1, 2, 3);
left.gather(Gatherers4j.zip(right)).toList();
// [Pair("A", 1), Pair("B", 2), Pair("C", 3)]
```
#### Zip elements of a stream together
This converts a `Stream` to a `Stream>`
```java
Stream
.of("A", "B", "C", "D", "E")
.gather(Gatherers4j.zipWitNext())
.toList();
// [["A", "B"], ["B", "C"], ["C", "D"], ["D", "E"]]
```
## Averaging `BigDecimal` objects
Functions on `AveragingBigDecimalGatherer` which modify the output.
| Function | Purpose |
|----------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `simpleMovingAverage(window)` | Instead of a cumulative average, calculate a moving average over a trailing `window` |
| `includePartialValues` | When calculating a moving average, include partially calculated values when less than `window` number of values are availabe.
The default is to only include fully calculated averages. |
| `treatNullAsZero()` | When an element in the `Stream` is `null` treat it as `BigDecimal.ZERO` instead of skipping it in the calculation. |
| `treatNullAs(BigDecimal)` | When an element in the `Stream` is `null` treat it as the `BigDecimal` value given instead of skipping it in the calculation. |
| `withMathContext(MathContext)` | Switch the `MathContext` for all calculations to the non-null `MathContext` given. The default is `MathContext.DECIMAL64`. |
| `withRoundingMode(RoundingMode)` | Switch the `RoundingMode` for all calcullations to the non-null `RoundingMode` given. The default is `RoundingMode.HALF_UP`. |
| `withOriginal()` | Include the original value (either a `BigDecimal` or some other object type if using `averageBigDecimalsBy()`) with the calculated average. |
### Example of `averageBigDecimals()`
This example creates a stream of `double`, converts each value to a `BigDecmial`, and takes a `simpleMovingAverage` over 10 trailing values.
It will `includePartialValues` and sets the `RoundingMode` and `MathContext` to the values given. Additionally, nulls
are treated as zeros, and the calculated average is returned along with the original value.
```java
someStreamOfBigDecimal()
.gather(Gatherers4j
.averageBigDecimals()
.simpleMovingAverage(10)
.includePartialValues()
.withRoundingMode(RoundingMode.HALF_EVEN)
.withMathContext(MathContext.DECIMAL32)
.treatNullAsZero()
.withOriginal()
)
.toList();
// Example output:
[
WithOriginal[original=0.8462487, calculated=0.8462487],
WithOriginal[original=0.8923297, calculated=0.8692890],
WithOriginal[original=0.2556937, calculated=0.6647573],
WithOriginal[original=0.2901778, calculated=0.5711125],
WithOriginal[original=0.4945578, calculated=0.5558016],
WithOriginal[original=0.3173066, calculated=0.5160525],
WithOriginal[original=0.6377766, calculated=0.5334417],
WithOriginal[original=0.1729199, calculated=0.4883765],
WithOriginal[original=0.7408201, calculated=0.5164258],
WithOriginal[original=0.7169926, calculated=0.5364825],
WithOriginal[original=0.5174489, calculated=0.5036025],
WithOriginal[original=0.5895662, calculated=0.4733262],
WithOriginal[original=0.4458275, calculated=0.4923396],
// etc...
]
```
# Project Philosophy
1. Consider adding a gatherer if it cannot be implemented with `map`, `filter`, or a collector without enclosing outside state.
2. Resist the temptation to add functions that only exist to provide an alias. They seem fun/handy but add surface area to the API and must be maintained forever.
3. All features should be documented and tested.
# Contributing
Please feel free to file issues for change requests or bugs. If you would like to contribute new functionality, please contact me before starting work!
Copyright © 2024 by Todd Ginsberg