diff --git a/CHANGELOG.md b/CHANGELOG.md index ade26c0..f09e2f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ ### 0.6.0 -+ TBD ++ Implement `dropLast(n)` ### 0.5.0 + Implement `reverse()` diff --git a/README.md b/README.md index 7f09931..ed8cfab 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ implementation("com.ginsberg:gatherers4j:0.6.0") | `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` | | `interleave(stream)` | Creates a stream of alternating objects from the input stream and the argument stream | @@ -139,6 +140,16 @@ Stream // [Person("Todd", "Ginsberg"), Person("Emma", "Ginsberg")] ``` +#### Keep all but the last `n` elements + +```java +Stream.of("A", "B", "C", "D", "E") + .gather(Gatherers4j.dropLast(2)) + .toList(); + +// ["A", "B", "C"] +``` + #### Ensure the stream is exactly `n` elements long ```java diff --git a/src/main/java/com/ginsberg/gatherers4j/DropLastGatherer.java b/src/main/java/com/ginsberg/gatherers4j/DropLastGatherer.java new file mode 100644 index 0000000..05bf1c2 --- /dev/null +++ b/src/main/java/com/ginsberg/gatherers4j/DropLastGatherer.java @@ -0,0 +1,61 @@ +/* + * 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.ArrayList; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Supplier; +import java.util.stream.Gatherer; + +public class DropLastGatherer implements Gatherer, INPUT> { + + private final int count; + + DropLastGatherer(final int count) { + if (count <= 0) { + throw new IllegalArgumentException("DropLast count must be positive"); + } + this.count = count; + } + + @Override + public Supplier> initializer() { + return State::new; + } + + @Override + public Integrator, INPUT, INPUT> integrator() { + return (state, element, downstream) -> { + state.elements.add(element); + return !downstream.isRejecting(); + }; + } + + @Override + public BiConsumer, Downstream> finisher() { + return (inputState, downstream) -> { + for (int i = 0; i < inputState.elements.size() - count && !downstream.isRejecting(); i++) { + downstream.push(inputState.elements.get(i)); + } + }; + } + + public static class State { + final List elements = new ArrayList<>(); + } +} diff --git a/src/main/java/com/ginsberg/gatherers4j/Gatherers4j.java b/src/main/java/com/ginsberg/gatherers4j/Gatherers4j.java index 30b0305..167920f 100644 --- a/src/main/java/com/ginsberg/gatherers4j/Gatherers4j.java +++ b/src/main/java/com/ginsberg/gatherers4j/Gatherers4j.java @@ -75,6 +75,15 @@ public static ThrottlingGatherer debounce(final int amount, final return new DistinctGatherer<>(function); } + /** + * Keep all elements except the last count elements of the stream. + * + * @param count A positive number of elements to drop from the end of the stream + */ + public static DropLastGatherer dropLast(final int count) { + return new DropLastGatherer<>(count); + } + /** * Ensure the input stream is exactly size elements long, and emit all elements * if so. If not, throw an IllegalStateException. diff --git a/src/test/java/com/ginsberg/gatherers4j/DropLastGathererTest.java b/src/test/java/com/ginsberg/gatherers4j/DropLastGathererTest.java new file mode 100644 index 0000000..5d34737 --- /dev/null +++ b/src/test/java/com/ginsberg/gatherers4j/DropLastGathererTest.java @@ -0,0 +1,60 @@ +/* + * 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.Test; + +import java.util.List; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class DropLastGathererTest { + + @Test + void countMustBePositive() { + assertThatThrownBy(() -> Stream.of("A") + .gather(Gatherers4j.dropLast(0)).toList()) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void dropLast() { + // Arrange + final Stream input = Stream.of("A", "B", "C"); + + // Act + final List output = input.gather(Gatherers4j.dropLast(2)).toList(); + + // Assert + assertThat(output).containsExactly("A"); + } + + @Test + void dropLastLongerThanStreamReturnsEmpty() { + // Arrange + final Stream input = Stream.of("A", "B", "C"); + + // Act + final List output = input.gather(Gatherers4j.dropLast(4)).toList(); + + // Assert + assertThat(output).isEmpty(); + } + +} \ No newline at end of file