Skip to content

Commit

Permalink
Release v0.7.0 (#72)
Browse files Browse the repository at this point in the history
* Branch for v0.7.0

* JavaDoc fixes (#58)

* Fix #57: Use greedy integrators where possible (#59)

+ Greedy integrators can be used anywhere the integrator does not itself initiate a short circuit (returns false). Passing along the downstream rejecting flag is allowed for greedy integrators.

* Implement JSpecify nullability annotations (#60)

+ Implement JSpecify nullability annotations
+ Add ErrorProne and NullAway to build for compile-time checks
+ Add more tests to `MinMaxGatherer` as highlighted by coverage report

* Make nullable lists more sane, update JEP reference (#61)

* Implement ordering by frequency (#62)

* Implement movingSum, movingSumBy, movingProduct, and movingProductBy (#63)

* Misc Cleanups (#66)

+ Nullability annotations in Gatherers4j.java
+ Minimum windowSize is 1 in a lot of places, not zero.
+ Add constructor to GroupingByGatherer and handle null mapping function to make nullability checks happier
+ Parameterize tests where multiple independent assertions were taking place

* #64 - Change order of arguments so functions are last (#67)

+ More Kotlin friendly, if people end up using this from Kotlin instead of extension functions on Stream
+ More consistent, easier for me to remember the order

* Remove min/max (#68)

* Broader support for stream sizes (#69)

+ Rename `exactSize(n)` to `sizeExactly(n)`
+ Add `sizeGreaterThan(n)`
+ Add `sizeGreaterThanOrEqualTo(n)`
+ Add `sizeLessThan(n)`
+ Add `sizeLessThanOrEqualTo(n)`

* Misc fixes (#70)

* Use default implementation of checkFinalLength when tryAccept will fail-fast.

* Eliminate warning

* Release v0.7.0
  • Loading branch information
tginsberg authored Jan 6, 2025
1 parent 5b46f28 commit f08e0eb
Show file tree
Hide file tree
Showing 47 changed files with 1,747 additions and 574 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
### 0.7.0
+ Use greedy integrators where possible (Fixes #57)
+ Add [JSpecify](https://jspecify.dev/) annotations for static analysis
+ Implement `orderByFrequencyAscending()` and `orderByFrequencyDescending()`
+ Implement `movingProduct()` and `movingProductBy()`
+ Implement `movingSum()` and `movingSumBy()`
+ Remove `maxBy(fn)` and `minBy(fn)`, can be done with JDK methods trivially
+ Rename `exactSize()` to `sizeExactly()`
+ Implement `sizeLessThan`, `sizeLessThanOrEqualTo`, `sizeGreaterThan`, and `sizeGreaterThanOrEqualTo`
+ API Style - Functions, when used as arguments, should come last for consistency and to play nice with Kotlin (Fixes #64)

### 0.6.0
+ Implement `dropLast(n)`
+ Implement `grouping()` and `groupingBy(fn)`
Expand Down
149 changes: 103 additions & 46 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Gatherers4j

A library of useful [Stream Gatherers](https://openjdk.org/jeps/473) (custom intermediate operations) for Java 23+.
A library of useful [Stream Gatherers](https://openjdk.org/jeps/485) (custom intermediate operations) for Java 23+.

# Installing

Expand All @@ -15,7 +15,7 @@ Add the following dependency to `pom.xml`.
<dependency>
<groupId>com.ginsberg</groupId>
<artifactId>gatherers4j</artifactId>
<version>0.6.0</version>
<version>0.7.0</version>
</dependency>
```

Expand All @@ -24,44 +24,52 @@ Add the following dependency to `pom.xml`.
Add the following dependency to `build.gradle` or `build.gradle.kts`

```groovy
implementation("com.ginsberg:gatherers4j:0.6.0")
implementation("com.ginsberg:gatherers4j:0.7.0")
```

# Gatherers In This Library

### Streams

| Function | Purpose |
|------------------------------|--------------------------------------------------------------------------------------------------------------------------------|
| `debounce(amount, duration)` | Limit stream elements to `amount` elements over `duration`, dropping any elements over the limit until a new `duration` starts |
| `dedupeConsecutive()` | Remove consecutive 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` |
| `dropLast(n)` | Keep all but the last `n` elements of the stream |
| `exactSize(n)` | Ensure the stream is exactly `n` elements long, or throw an `IllegalStateException` |
| `filterWithIndex(predicate)` | Filter the stream with the given `predicate`, which takes an `element` and its `index` |
| `grouping()` | Group consecute identical elements into lists |
| `groupingBy(fn)` | Group consecutive elements that are identical according to `fn` into lists |
| `interleave(iterable)` | Creates a stream of alternating objects from the input stream and the argument iterable |
| `interleave(iterator)` | Creates a stream of alternating objects from the input stream and the argument iterator |
| `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 |
| `maxBy(fn)` | Return a stream containing a single element, which is the maximum value returned by the mapping function `fn` |
| `minBy(fn)` | Return a stream containing a single element, which is the minimum value returned by the mapping function `fn` |
| `reverse()` | Reverse the order of the stream |
| `shuffle()` | Shuffle the stream into a random order using the platform default `RandomGenerator` |
| `shuffle(rg)` | Shuffle the stream into a random order using the specified `RandomGenerator` |
| `throttle(amount, duration)` | Limit stream elements to `amount` elements over `duration`, pausing until a new `duration` period starts |
| `withIndex()` | Maps all elements of the stream as-is along with their 0-based index |
| `zipWith(iterable)` | Creates a stream of `Pair` objects whose values come from the input stream and argument iterable |
| `zipWith(iterator)` | Creates a stream of `Pair` objects whose values come from the input stream and argument iterator |
| `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 |
| Function | Purpose |
|-------------------------------|--------------------------------------------------------------------------------------------------------------------------------|
| `debounce(amount, duration)` | Limit stream elements to `amount` elements over `duration`, dropping any elements over the limit until a new `duration` starts |
| `dedupeConsecutive()` | Remove consecutive 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` |
| `dropLast(n)` | Keep all but the last `n` elements of the stream |
| `filterWithIndex(predicate)` | Filter the stream with the given `predicate`, which takes an `element` and its `index` |
| `grouping()` | Group consecute identical elements into lists |
| `groupingBy(fn)` | Group consecutive elements that are identical according to `fn` into lists |
| `interleave(iterable)` | Creates a stream of alternating objects from the input stream and the argument iterable |
| `interleave(iterator)` | Creates a stream of alternating objects from the input stream and the argument iterator |
| `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 |
| `orderByFrequencyAscending() | Returns a stream where elements are ordered from least to most frequent as `WithCount<T>` wrapper objects. |
| `orderByFrequencyDescending() | Returns a stream where elements are ordered from most to least frequent as `WithCount<T>` wrapper objects. |
| `reverse()` | Reverse the order of the stream |
| `shuffle()` | Shuffle the stream into a random order using the platform default `RandomGenerator` |
| `shuffle(rg)` | Shuffle the stream into a random order using the specified `RandomGenerator` |
| `sizeExactly(n)` | Ensure the stream is exactly `n` elements long, or throw an `IllegalStateException` |
| `sizeGreaterThan(n)` | Ensure the stream is greater than `n` elements long, or throw an `IllegalStateException` |
| `sizeGreaterThanOrEqualTo(n)` | Ensure the stream is greater than or equal to `n` elements long, or throw an `IllegalStateException` |
| `sizeLessThan(n)` | Ensure the stream is less than `n` elements long, or throw an `IllegalStateException` |
| `sizeLessThanOrEqualTo(n)` | Ensure the stream is less than or equal to `n` elements long, or throw an `IllegalStateException` |
| `throttle(amount, duration)` | Limit stream elements to `amount` elements over `duration`, pausing until a new `duration` period starts |
| `withIndex()` | Maps all elements of the stream as-is along with their 0-based index |
| `zipWith(iterable)` | Creates a stream of `Pair` objects whose values come from the input stream and argument iterable |
| `zipWith(iterator)` | Creates a stream of `Pair` objects whose values come from the input stream and argument iterator |
| `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 |

### Mathematics/Statistics

| Function | Purpose |
|--------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------|
| `movingProduct(window)` | Create a moving product of `BigDecimal` values over the previous `window` values. |
| `movingProductBy(window, fn)` | Create a moving product of `BigDecimal` values over the previous `window` values, as mapped via `fn`. |
| `movingSum(window)` | Create a moving sum of `BigDecimal` values over the previous `window` values. |
| `movingSumBy(window, fn)` | Create a moving sum of `BigDecimal` values over the previous `window` values, as mapped via `fn`. |
| `runningPopulationStandardDeviation()` | Create a stream of `BigDecimal` objects representing the running population standard deviation. |
| `runningPopulationStandardDeviationBy(fn)` | Create a stream of `BigDecimal` objects as mapped from the input via `fn`, representing the running population standard deviation. |
| `runningProduct()` | Create a stream of `BigDecimal` objects representing the running product. | |
Expand All @@ -71,7 +79,7 @@ implementation("com.ginsberg:gatherers4j:0.6.0")
| `runningSum()` | Create a stream of `BigDecimal` objects representing the running sum. |
| `runningSumBy(fn)` | Create a stream of `BigDecimal` objects as mapped from the input via `fn`, representing the running sum. |
| `simpleMovingAverage(window)` | Create a moving average of `BigDecimal` values over the previous `window` values. See below for options. |
| `simpleMovingAverageBy(fn, window)` | Create a moving average of `BigDecimal` values over the previous `window` values, as mapped via `fn`. |
| `simpleMovingAverageBy(window, fn)` | Create a moving average of `BigDecimal` values over the previous `window` values, as mapped via `fn`. |
| `simpleRunningAverage()` | Create a running average of `BigDecimal` values. See below for options. |
| `simpleRunningAverageBy(fn)` | Create a running average of `BigDecimal` values as mapped via `fn`. |

Expand Down Expand Up @@ -161,11 +169,64 @@ Stream.of("A", "B", "C", "D", "E")
```java
// Good

Stream.of("A", "B", "C").gather(Gatherers4j.exactSize(3)).toList();
Stream.of("A", "B", "C").gather(Gatherers4j.sizeExactly(3)).toList();
// ["A", "B", "C"]

// Bad
Stream.of("A").gather(Gatherers4j.exactSize(3)).toList();
Stream.of("A").gather(Gatherers4j.sizeExactly(3)).toList();
// IllegalStateException
```

#### Ensure the stream is greater than `n` elements long

```java
// Good

Stream.of("A", "B", "C").gather(Gatherers4j.sizeGreaterThan(2)).toList();
// ["A", "B", "C"]

// Bad
Stream.of("A", "B").gather(Gatherers4j.sizeGreaterThan(2)).toList();
// IllegalStateException
```

#### Ensure the stream is greater than or equal to `n` elements long

```java
// Good

Stream.of("A", "B").gather(Gatherers4j.sizeGreaterThanOrEqualTo(2)).toList();
// ["A", "B"]

// Bad
Stream.of("A").gather(Gatherers4j.sizeGreaterThanOrEqualTo(2)).toList();
// IllegalStateException
```

#### Ensure the stream is less than `n` elements long

```java
// Good

Stream.of("A").gather(Gatherers4j.sizeLessThan(2)).toList();
// ["A"]

// Bad
Stream.of("A", "B").gather(Gatherers4j.sizeLessThan(2)).toList();
// IllegalStateException
```


#### Ensure the stream is less than or equal to `n` elements long

```java
// Good

Stream.of("A", "B").gather(Gatherers4j.sizeLessThanOrEqualTo(2)).toList();
// ["A", "B"]

// Bad
Stream.of("A", "B", "C").gather(Gatherers4j.sizeLessThanOrEqualTo(2)).toList();
// IllegalStateException
```

Expand Down Expand Up @@ -222,28 +283,24 @@ Stream
// ["E", "F", "G"]
```

#### Find the object with the maximum mapped value
#### Order elements by frequency, ascending

```java
record Employee(String name, int salary) {}

streamOfEmployees
.gather(Gatherers4j.maxBy(Employee:salary))
.toList();
Stream.of("A", "A", "A", "B", "B" ,"C")
.gather(Gatherers4j.orderByFrequencyAscending())
.toList()

// Employee("Big Shot", 1_000_000)
// [WithCount("C", 1), WithCount("B", 2), WithCount("C", 3) ]
```

#### Find the object with the minimum mapped value
#### Order elements by frequency, descending

```java
record Person(String name, int age) {}

streamOfPeople
.gather(Gatherers4j.minBy(Person:age))
.toList();
Stream.of("A", "A", "A", "B", "B" ,"C")
.gather(Gatherers4j.orderByFrequencyDescending())
.toList()

// Person("Baby", 1)
// [WithCount("C", 3), WithCount("B", 2), WithCount("A", 1) ]
```

#### Reverse the order of the stream
Expand Down
2 changes: 1 addition & 1 deletion VERSION.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.6.0
0.7.0
18 changes: 18 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import net.ltgt.gradle.errorprone.CheckSeverity
import net.ltgt.gradle.errorprone.errorprone
import java.io.IOException
import java.net.URI

Expand All @@ -6,6 +8,7 @@ plugins {
id("jacoco")
id("java-library")
id("maven-publish")
id("net.ltgt.errorprone") version "4.1.0"
id("signing")
}

Expand All @@ -32,6 +35,9 @@ repositories {
}

dependencies {
api("org.jspecify:jspecify:1.0.0") {
because("Annotating with JSpecify makes static analysis more accurate")
}
testRuntimeOnly("org.junit.platform:junit-platform-launcher") {
because("Starting in Gradle 9.0, this needs to be an explicitly declared dependency")
}
Expand All @@ -41,6 +47,9 @@ dependencies {
testImplementation("org.assertj:assertj-core:3.26.3") {
because("These assertions are clearer than JUnit+Hamcrest")
}

errorprone("com.google.errorprone:error_prone_core:2.36.0")
errorprone("com.uber.nullaway:nullaway:0.12.1")
}

publishing {
Expand Down Expand Up @@ -101,6 +110,15 @@ signing {
tasks {
withType<JavaCompile> {
options.compilerArgs.add(ENABLE_PREVIEW)
options.errorprone {
check("NullAway", CheckSeverity.ERROR)
option("NullAway:AnnotatedPackages", "com.ginsberg.gatherers4j")
}
if (name.lowercase().contains("test")) {
options.errorprone {
disable("NullAway")
}
}
}

jacocoTestReport {
Expand Down
18 changes: 10 additions & 8 deletions src/main/java/com/ginsberg/gatherers4j/BigDecimalGatherer.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,29 @@

package com.ginsberg.gatherers4j;

import org.jspecify.annotations.Nullable;

import java.math.BigDecimal;
import java.math.MathContext;
import java.util.function.Function;
import java.util.stream.Gatherer;

import static com.ginsberg.gatherers4j.GathererUtils.mustNotBeNull;

abstract public class BigDecimalGatherer<INPUT>
abstract public class BigDecimalGatherer<INPUT extends @Nullable Object>
implements Gatherer<INPUT, BigDecimalGatherer.State, BigDecimal> {
private final Function<INPUT, BigDecimal> mappingFunction;
private final Function<INPUT, @Nullable BigDecimal> mappingFunction;
private MathContext mathContext = MathContext.DECIMAL64;
private BigDecimal nullReplacement;
private @Nullable BigDecimal nullReplacement;

BigDecimalGatherer(final Function<INPUT, BigDecimal> mappingFunction) {
super();
BigDecimalGatherer(final Function<INPUT, @Nullable BigDecimal> mappingFunction) {
mustNotBeNull(mappingFunction, "Mapping function must not be null");
this.mappingFunction = mappingFunction;
}

@Override
public Integrator<BigDecimalGatherer.State, INPUT, BigDecimal> integrator() {
return (state, element, downstream) -> {
return Integrator.ofGreedy((state, element, downstream) -> {
final BigDecimal mappedElement = element == null ? nullReplacement : mappingFunction.apply(element);
if (mappedElement != null) {
state.add(mappedElement, mathContext);
Expand All @@ -45,7 +47,7 @@ public Integrator<BigDecimalGatherer.State, INPUT, BigDecimal> integrator() {
}
}
return !downstream.isRejecting();
};
});
}

/// When encountering a `null` value in a stream, treat it as `BigDecimal.ZERO` instead.
Expand All @@ -56,7 +58,7 @@ public BigDecimalGatherer<INPUT> treatNullAsZero() {
/// When encountering a `null` value in a stream, treat it as the given `replacement` value instead.
///
/// @param replacement The value to replace `null` with
public BigDecimalGatherer<INPUT> treatNullAs(final BigDecimal replacement) {
public BigDecimalGatherer<INPUT> treatNullAs(final @Nullable BigDecimal replacement) {
this.nullReplacement = replacement;
return this;
}
Expand Down
Loading

0 comments on commit f08e0eb

Please sign in to comment.