Skip to content

Commit

Permalink
Documentation and cleanup (#8)
Browse files Browse the repository at this point in the history
* Misc cleanups and documentation

+ Documenting usages in README
+ JavaDocs for user-facing functions
+ Move util functions to package
+ Rename `zip` to `zipWith`
+ Remove `mapWithIndex`, not useful (use `map`)
+ Added test cases
+ Changed argument-related NullPointerException to IllegalArgumentException

* More documentation for averaging
  • Loading branch information
tginsberg authored Jul 21, 2024
1 parent 12eec9c commit f284cd6
Show file tree
Hide file tree
Showing 11 changed files with 316 additions and 152 deletions.
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

0 comments on commit f284cd6

Please sign in to comment.