Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Documentation and cleanup #8

Merged
merged 2 commits into from
Jul 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 171 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +6,55 @@ A library of useful [Stream Gatherers](https://openjdk.org/jeps/473) (custom int

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.<br/> 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`.<br/>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

(Example, TODO clean this up)
#### Running average of `Stream<BigDecimal>`

**Running Average**
For more options, please see the [specific advice on averaging](#averaging-bigdecimal-objects).

```java
Stream
.of(new BigDecimal("1.0"), new BigDecimal("2.0"), new BigDecimal("10.0"))
.of("1.0", "2.0", "10.0")
.map(BigDecimal::new)
.gather(Gatherers4j.averageBigDecimals())
.toList();

// [1, 1.5, 4.3333333333333333]
```

**Trailing Average**
#### Moving average of `Stream<BigDecimal>`

For more options, please see the [specific advice on averaging](#averaging-bigdecimal-objects)

```java
Stream
.of(new BigDecimal("1.0"), new BigDecimal("2.0"), new BigDecimal("10.0"), new BigDecimal("20.0"), new BigDecimal("30.0"))
.gather(Gatherers4j.averageBigDecimals().trailing(2))
.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]
```


**Removing consecutive duplicate elements:**
#### Remove consecutive duplicate elements

```java
Stream
Expand All @@ -44,7 +65,52 @@ Stream
// ["A", "B", "C", "D", "A", "B", "C"]
```

**Limit the stream to the `last` _n_ elements:**
#### 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::firstName))
.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<String> left = Stream.of("A", "B", "C");
final Stream<String> 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
Expand All @@ -55,8 +121,104 @@ Stream
// ["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<Pair>`

The left and right streams can be of different types.

```java
final Stream<String> left = Stream.of("A", "B", "C");
final Stream<Integer> 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<T>` to a `Stream<List<T>>`

```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.<br/>The default is to only include fully calculated averages. |
| `treatNullAsZero()` | When an element in the `Stream<BigDecimal>` is `null` treat it as `BigDecimal.ZERO` instead of skipping it in the calculation. |
| `treatNullAs(BigDecimal)` | When an element in the `Stream<BigDecimal>` 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

Guidance forthcoming.
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
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public class AveragingBigDecimalGatherer<INPUT>
private RoundingMode roundingMode = RoundingMode.HALF_UP;
private MathContext mathContext = MathContext.DECIMAL64;
private BigDecimal nullReplacement;
private int trailingCount = 1;
private int windowSize = 1;
private boolean includePartialValues;

AveragingBigDecimalGatherer(final Function<INPUT, BigDecimal> mappingFunction) {
Expand All @@ -43,7 +43,7 @@ public class AveragingBigDecimalGatherer<INPUT>

@Override
public Supplier<State> initializer() {
return trailingCount == 1 ? State::new : () -> new TrailingState(trailingCount);
return windowSize == 1 ? State::new : () -> new TrailingState(windowSize);
}

@Override
Expand All @@ -60,40 +60,73 @@ public Integrator<AveragingBigDecimalGatherer.State, INPUT, BigDecimal> integrat
};
}

public AveragingBigDecimalGatherer<INPUT> trailing(int count) {
if (count <= 0) {
throw new IllegalArgumentException("Trailing count must be positive");
/**
* Construct a moving average, with a window of size <code>window</code>.
*
* @param window The size of the window to average values over, must be a positive number.
*/
public AveragingBigDecimalGatherer<INPUT> simpleMovingAverage(int window) {
if (window <= 0) {
throw new IllegalArgumentException("Moving window size must be positive");
}
trailingCount = count;
windowSize = window;
return this;
}

public AveragingBigDecimalGatherer<INPUT> includePartialTailingValues() {
/**
* When creating a moving average and the full size of the window has not yet been reached, the
* gatherer should emit averages for what it has.
* For example, if the trailing average is over 10 values, but the stream has only emitted two
* values, the gatherer should average the two values and emit the answer. The default is to not
* emit anything until the full size of the window has been seen.
*/
public AveragingBigDecimalGatherer<INPUT> includePartialValues() {
includePartialValues = true;
return this;
}

/**
* When encountering a <code>null</code> value in a stream, treat it as `BigDecimal.ZERO` instead.
*/
public AveragingBigDecimalGatherer<INPUT> treatNullAsZero() {
return treatNullAs(BigDecimal.ZERO);
}

/**
* When encountering a <code>null</code> value in a stream, treat it as the given `rule` value instead.
*
* @param rule The value to replace null with
*/
public AveragingBigDecimalGatherer<INPUT> treatNullAs(final BigDecimal rule) {
this.nullReplacement = rule;
return this;
}

/**
* Replace the <code>MathContext</code> used for all mathematical operations in this class.
*
* @param mathContext A non-null <code>MathContext</code>
*/
public AveragingBigDecimalGatherer<INPUT> withMathContext(final MathContext mathContext) {
mustNotBeNull(mathContext, "MathContext must not be null");
this.mathContext = mathContext;
return this;
}

/**
* Replace the <code>RoundingMode</code> used for all mathematical operations in this class.
*
* @param roundingMode A non-null <code>RoundingMode</code>
*/
public AveragingBigDecimalGatherer<INPUT> withRoundingMode(final RoundingMode roundingMode) {
mustNotBeNull(roundingMode, "RoundingMode must not be null");
this.roundingMode = roundingMode;
return this;
}

/**
* Include the original input value from the stream in addition to the calculated average.
*/
public WithOriginalGatherer<INPUT, State, BigDecimal> withOriginal() {
return new WithOriginalGatherer<>(this);
}
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/com/ginsberg/gatherers4j/GathererUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

public class GathererUtils {

public static boolean safeEquals(final Object left, final Object right) {
static boolean safeEquals(final Object left, final Object right) {
if (left == null && right == null) {
return true;
} else if (left == null || right == null) {
Expand All @@ -26,7 +26,7 @@ public static boolean safeEquals(final Object left, final Object right) {
return left.equals(right);
}

public static void mustNotBeNull(final Object subject, final String message) {
static void mustNotBeNull(final Object subject, final String message) {
if (subject == null) {
throw new IllegalArgumentException(message);
}
Expand Down
Loading