Skip to content

Commit

Permalink
Implement maxBy(fn) and minBy(fn) (#38)
Browse files Browse the repository at this point in the history
  • Loading branch information
tginsberg authored Oct 13, 2024
1 parent 819d5f0 commit b58699c
Show file tree
Hide file tree
Showing 5 changed files with 284 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
### 0.5.0
+ Implement `reverse()`
+ Implement `maxBy(fn)` and `minBy(fn)`

### 0.4.0
+ Implement `suffle()` and `shuffle(RandomGenerator)`
Expand Down
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ implementation("com.ginsberg:gatherers4j:0.5.0")
| `filterWithIndex(predicate)` | Filter the stream with the given `predicate`, which takes an `element` and its `index` |
| `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` |
Expand Down Expand Up @@ -168,6 +170,30 @@ Stream
// ["E", "F", "G"]
```

#### Find the object with the maximum mapped value

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

streamOfEmployees
.gather(Gatherers4j.maxBy(Employee:salary))
.toList();

// Employee("Big Shot", 1_000_000)
```

#### Find the object with the minimum mapped value

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

streamOfPeople
.gather(Gatherers4j.minBy(Person:age))
.toList();

// Person("Baby", 1)
```

#### Reverse the order of the stream

```java
Expand Down
28 changes: 28 additions & 0 deletions src/main/java/com/ginsberg/gatherers4j/Gatherers4j.java
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,34 @@ public static <INPUT> LastGatherer<INPUT> last(final int count) {
return new LastGatherer<>(count);
}

/**
* Return a Stream containing the single maximum value of the input stream, according to
* the given mapping function. In the case where a stream has more than one mapped value
* that is the maximum, the first one encountered makes up the stream. This does not
* evaluate null values or null mappings.
*
* @param function A mapping function, the results of which must implement <code>Comparable</code>
*/
public static <INPUT, MAPPED extends Comparable<MAPPED>> MinMaxGatherer<INPUT, MAPPED> maxBy(
final Function<INPUT, MAPPED> function
) {
return new MinMaxGatherer<>(true, function);
}

/**
* Return a Stream containing the single minimum value of the input stream, according to
* the given mapping function. In the case where a stream has more than one mapped value
* that is the minimum, the first one encountered makes up the stream. This does not
* evaluate null values or null mappings.
*
* @param function A mapping function, the results of which must implement <code>Comparable</code>
*/
public static <INPUT, MAPPED extends Comparable<MAPPED>> MinMaxGatherer<INPUT, MAPPED> minBy(
final Function<INPUT, MAPPED> function
) {
return new MinMaxGatherer<>(false, function);
}

/**
* Reverse the order of the input Stream.
*
Expand Down
75 changes: 75 additions & 0 deletions src/main/java/com/ginsberg/gatherers4j/MinMaxGatherer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright 2024 Todd Ginsberg
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.ginsberg.gatherers4j;

import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Gatherer;

public class MinMaxGatherer<INPUT, MAPPED extends Comparable<MAPPED>>
implements Gatherer<INPUT, MinMaxGatherer.State<INPUT, MAPPED>, INPUT> {

private final Function<INPUT, MAPPED> mappingFunction;
private final boolean max;

MinMaxGatherer(final boolean max, final Function<INPUT, MAPPED> mappingFunction) {
this.mappingFunction = mappingFunction;
this.max = max;
}

@Override
public Supplier<State<INPUT, MAPPED>> initializer() {
return State::new;
}

@Override
public Integrator<State<INPUT, MAPPED>, INPUT, INPUT> integrator() {
return (state, element, downstream) -> {
final MAPPED mapped = element == null ? null : mappingFunction.apply(element);
if (mapped == null) {
return !downstream.isRejecting();
}
if (state.bestSoFar == null) {
state.bestSoFar = element;
state.mappedField = mapped;
} else {
final int compared = mapped.compareTo(state.mappedField);
if ((compared > 0 && max) || (compared < 0 && !max)) {
state.bestSoFar = element;
state.mappedField = mapped;
}
}
return !downstream.isRejecting();
};
}

@Override
public BiConsumer<State<INPUT, MAPPED>, Downstream<? super INPUT>> finisher() {
return (state, downstream) -> {
if (state.bestSoFar != null) {
downstream.push(state.bestSoFar);
}
// TODO: What happens if we have no best?
};
}

public static class State<INPUT, MAPPED extends Comparable<MAPPED>> {
private INPUT bestSoFar;
private MAPPED mappedField;
}
}
154 changes: 154 additions & 0 deletions src/test/java/com/ginsberg/gatherers4j/MinMaxGathererTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/*
* Copyright 2024 Todd Ginsberg
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.ginsberg.gatherers4j;

import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

import java.util.List;
import java.util.stream.Stream;

import static org.assertj.core.api.Assertions.assertThat;

class MinMaxGathererTest {

private record TestObject(String a, int b) {
}

@Nested
class Max {

@Test
void doesNotTestNullMappings() {
// Arrange
final Stream<TestObject> input = Stream.of(new TestObject(null, 1));

// Act
final List<TestObject> output = input.gather(Gatherers4j.maxBy(TestObject::a)).toList();

// Assert
assertThat(output).isEmpty();
}

@Test
void emptyStreamHasNoBest() {
// Arrange
final Stream<TestObject> input = Stream.empty();

// Act
final List<TestObject> output = input.gather(Gatherers4j.maxBy(TestObject::b)).toList();

// Assert
assertThat(output).isEmpty();
}

@Test
void firstValueSelectedWhenMultipleExist() {
// Arrange
final Stream<TestObject> input = Stream.of(
new TestObject("A", 1),
new TestObject("B", 1)
);

// Act
final List<TestObject> output = input.gather(Gatherers4j.maxBy(TestObject::b)).toList();

// Assert
assertThat(output).containsExactly(new TestObject("A", 1));
}

@Test
void maxValue() {
// Arrange
final Stream<TestObject> input = Stream.of(
new TestObject("A", 1),
new TestObject("B", 2),
new TestObject("C", 3),
new TestObject("E", 2),
new TestObject("E", 1)
);

// Act
final List<TestObject> output = input.gather(Gatherers4j.maxBy(TestObject::b)).toList();

// Assert
assertThat(output).containsExactly(new TestObject("C", 3));
}
}

@Nested
class Min {

@Test
void doesNotTestNullMappings() {
// Arrange
final Stream<TestObject> input = Stream.of(new TestObject(null, 1));

// Act
final List<TestObject> output = input.gather(Gatherers4j.minBy(TestObject::a)).toList();

// Assert
assertThat(output).isEmpty();
}

@Test
void emptyStreamHasNoBest() {
// Arrange
final Stream<TestObject> input = Stream.empty();

// Act
final List<TestObject> output = input.gather(Gatherers4j.minBy(TestObject::b)).toList();

// Assert
assertThat(output).isEmpty();
}

@Test
void firstValueSelectedWhenMultipleExist() {
// Arrange
final Stream<TestObject> input = Stream.of(
new TestObject("A", 1),
new TestObject("B", 1)
);

// Act
final List<TestObject> output = input.gather(Gatherers4j.minBy(TestObject::b)).toList();

// Assert
assertThat(output).containsExactly(new TestObject("A", 1));
}

@Test
void minValue() {
// Arrange
final Stream<TestObject> input = Stream.of(
new TestObject("A", 3),
new TestObject("B", 2),
new TestObject("C", 1),
new TestObject("E", 2),
new TestObject("E", 3)
);

// Act
final List<TestObject> output = input.gather(Gatherers4j.minBy(TestObject::b)).toList();

// Assert
assertThat(output).containsExactly(new TestObject("C", 1));
}
}

}

0 comments on commit b58699c

Please sign in to comment.