Skip to content

Commit

Permalink
Add windowSliding() Gatherer with customizable step parameter (#45)
Browse files Browse the repository at this point in the history
  • Loading branch information
pivovarit authored Oct 15, 2024
1 parent 659881d commit 6f65a65
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 0 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ Provided `Gatherers`:
- takes elements until a change is detected
- `MoreGatherers.distinctUntilChanged(Function<T, R>)`
- takes elements until a change is detected based on a key extractor function
- `MoreGatherers.windowSliding(int, int)`
- creates a sliding window of a fixed size with a fixed step, extends `Gatherers.windowSliding(int)` by adding a step parameter

### Philosophy

Expand Down
26 changes: 26 additions & 0 deletions src/main/java/com/pivovarit/gatherers/MoreGatherers.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.pivovarit.gatherers;

import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.BiFunction;
Expand Down Expand Up @@ -217,4 +219,28 @@ private MoreGatherers() {
public static <T> Gatherer<T, ?, Map.Entry<T, Long>> zipWithIndex() {
return new ZipWithIndexGatherer<>();
}

/**
* Creates a {@link Gatherer} that collects elements into sliding windows of a specified size.
* Each window captures a subset of elements from the input, and windows slide by a specified step.
*
* <p>For example, if the window size is 3 and the step is 1, the gatherer will collect
* windows of size 3, sliding by 1 element at a time. This means each subsequent window overlaps
* with the previous one by two elements.</p>
*
* <p>Common use cases include moving averages, trend analysis, and any scenario requiring
* overlapping or rolling window operations on a list of elements.</p>
*
* @param <TR> the type of elements in the input and output list
* @param windowSize the size of each window (must be a positive integer)
* @param step the number of elements to slide the window by (must be a positive integer)
*
* @return a {@link Gatherer} that collects elements into sliding windows
*
* @throws IllegalArgumentException if {@code windowSize} is less than one or {@code step} is less than zero, or greater than {@code windowSize}
* @apiNote this {@link Gatherer} extends {@link java.util.stream.Gatherers#windowSliding(int)} by allowing to customize the step
*/
public static <TR> Gatherer<TR, ?, List<TR>> windowSliding(int windowSize, int step) {
return new WindowSlidingGatherer<>(windowSize, step);
}
}
70 changes: 70 additions & 0 deletions src/main/java/com/pivovarit/gatherers/WindowSlidingGatherer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.pivovarit.gatherers;

import java.util.Arrays;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Supplier;
import java.util.stream.Gatherer;

record WindowSlidingGatherer<T>(int windowSize, int step)
implements Gatherer<T, WindowSlidingGatherer.SlidingWindow, List<T>> {
WindowSlidingGatherer {
if (windowSize < 1) {
throw new IllegalArgumentException("'windowSize' must be greater than zero");
}

if (step < 0) {
throw new IllegalArgumentException("'step' must be greater than or equal to zero");
}

if (step > windowSize) {
throw new IllegalArgumentException("'step' must be less than or equal to 'windowSize'");
}
}

@Override
public Supplier<WindowSlidingGatherer.SlidingWindow> initializer() {
return WindowSlidingGatherer.SlidingWindow::new;
}

@Override
public Integrator<WindowSlidingGatherer.SlidingWindow, T, List<T>> integrator() {
return Integrator.ofGreedy((state, e, downstream) -> state.integrate(e, downstream));
}

@Override
public BiConsumer<WindowSlidingGatherer.SlidingWindow, Downstream<? super List<T>>> finisher() {
return SlidingWindow::finish;
}

class SlidingWindow {
Object[] window = new Object[windowSize];
int at = 0;
boolean firstWindow = true;

boolean integrate(T element, Downstream<? super List<T>> downstream) {
window[at++] = element;
if (at < windowSize) {
return true;
} else {
final var oldWindow = window;
final var newWindow = new Object[windowSize];
System.arraycopy(oldWindow, step, newWindow, 0, windowSize - step);
window = newWindow;
at -= step;
firstWindow = false;
return downstream.push((List<T>) Arrays.asList(oldWindow));
}
}

void finish(Downstream<? super List<T>> downstream) {
if (firstWindow && at > 0 && !downstream.isRejecting()) {
var lastWindow = new Object[at];
System.arraycopy(window, 0, lastWindow, 0, at);
window = null;
at = 0;
downstream.push((List<T>) Arrays.asList(lastWindow));
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.pivovarit.gatherers.blackbox;

import com.pivovarit.gatherers.MoreGatherers;
import org.junit.jupiter.api.Test;

import java.util.stream.Stream;

import static java.util.List.of;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class WindowSlidingTest {

@Test
void shouldRejectInvalidWindowSize() {
assertThatThrownBy(() -> MoreGatherers.windowSliding(0, 1))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("'windowSize' must be greater than zero");
}

@Test
void shouldRejectInvalidStep() {
assertThatThrownBy(() -> MoreGatherers.windowSliding(1, -1))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("'step' must be greater than or equal to zero");
}

@Test
void shouldWindowSlidingEmpty() {
assertThat(Stream.empty().gather(MoreGatherers.windowSliding(2, 1))).isEmpty();
}

@Test
void shouldWindowSlidingWithStep1() {
assertThat(Stream.of(1, 2, 3, 4, 5).gather(MoreGatherers.windowSliding(2, 1)))
.containsExactly(of(1, 2), of(2, 3), of(3, 4), of(4, 5));
}

@Test
void shouldWindowSlidingWithStep2() {
assertThat(Stream.of(1, 2, 3, 4, 5).gather(MoreGatherers.windowSliding(2, 2)))
.containsExactly(of(1, 2), of(3, 4));
}
}

0 comments on commit 6f65a65

Please sign in to comment.