Java counting in a period of a window size(e.g. size=3)
I think the core problem is partition list,if you can use Google Guava this will be very simple like the bellow code:
Code:
List<SaleTxn> saleTxns = new ArrayList<>();
saleTxns.add(new SaleTxn(1, "2018-10-10", 100));
saleTxns.add(new SaleTxn(2, "2018-10-11", 200));
saleTxns.add(new SaleTxn(3, "2018-10-12", 100));
saleTxns.add(new SaleTxn(4, "2018-10-13", 100));
saleTxns.add(new SaleTxn(5, "2018-10-14", 200));
saleTxns.add(new SaleTxn(6, "2018-10-15", 200));
// implement of filter
saleTxns = saleTxns.stream().filter(saleTxn -> true).collect(Collectors.toList());
// partition the list and sum all value
List<Integer> result = Lists.partition(saleTxns, 3).stream()
.mapToInt(value -> value.stream().mapToInt(SaleTxn::getAmount).sum())
.boxed()
.collect(Collectors.toList());
System.out.println(result);
The output of code:
[400, 500]
You can achieve this with only myList.size()/3
iterations. Use an IntStream
to iterate with custom increments (we use 3 which is the window size):
IntStream.iterate(0, n -> n + 3).limit(myList.size() / 3)
Then collect the entries in a map:
Collectors.toMap(i -> myList.subList(i, i + 3), i -> (myList.get(i).getAmount()
+ myList.get(i + 1).getAmount() + myList.get(i + 2).getAmount()))
Let's assume I've added an extra item to myList
:
myList.add(new SaleTxn(7, "2018-10-16", 300));
Now myList.size()
equals to 7 and the solution only works if the size is 3, 6, or 9,... (myList.size() % 3 == 0
)
To cover the items left we need to check if there is items remaining:
if (myList.size() % 3 != 0) {
List<SaleTxn> remainderList = new ArrayList<>();
int remainderSum = 0;
for (int j = myList.size() % 3; j > 0; j--) {
remainderSum += myList.get(myList.size() - j).getAmount();
remainderList.add(myList.get(myList.size() - j));
}
result.put(remainderList, remainderSum);
}
Full code:
Map<List<SaleTxn>, Integer> result = IntStream.iterate(0, n -> n + 3).limit(myList.size() / 3).boxed()
.collect(Collectors.toMap(i -> myList.subList(i, i + 3), i -> (myList.get(i).getAmount()
+ myList.get(i + 1).getAmount() + myList.get(i + 2).getAmount())));
if (myList.size() % 3 != 0) {
List<SaleTxn> remainderList = new ArrayList<>();
int remainderSum = 0;
for (int j = myList.size() % 3; j > 0; j--) {
remainderSum += myList.get(myList.size() - j).getAmount();
remainderList.add(myList.get(myList.size() - j));
}
result.put(remainderList, remainderSum);
}
result.entrySet().forEach(e -> System.out.println(e));
Output:
[SaleTxn [id=4, txnDate=2018-10-13, amount=100], SaleTxn [id=5, txnDate=2018-10-14, amount=200], SaleTxn [id=6, txnDate=2018-10-15, amount=200]]=500
[SaleTxn [id=1, txnDate=2018-10-10, amount=100], SaleTxn [id=2, txnDate=2018-10-11, amount=200], SaleTxn [id=3, txnDate=2018-10-12, amount=100]]=400
[SaleTxn [id=7, txnDate=2018-10-16, amount=300]]=300
I added a toString()
method to SaleTxn
class to get the output above:
@Override
public String toString() {
return "SaleTxn [id=" + id + ", txnDate=" + txnDate + ", amount=" + amount + "]";
}
It's not necessarily possible to have a collected list in advance to use List.partition
from Google Guava (additionally, this is a third party component that is not necessarily included to the codebase). The only known to me way of doing it for streams of arbitrary size is wrapping a stream to a windowed stream, and probably this can't (? I don't really know, to be honest) be done by using chained methods reduce
, collect
, but probably can be done using flatMap
(? again, not an expert in streams). Another "pro" for implementing it using streams and not loops is that streams can go through method boundaries, so I'm just assuming the OP's example is just very simple and truly can be better implemented using old good loops.
public final class MoreStreams {
private MoreStreams() {
}
public static <E> Stream<E[]> windowed(final Stream<? extends E> upstream, final int size, final IntFunction<? extends E[]> createArray)
throws IllegalArgumentException {
if ( size < 1 ) {
throw new IllegalArgumentException("Illegal window size: " + size);
}
if ( size == 1 ) {
return upstream
.map(e -> {
final E[] array = createArray.apply(1);
array[0] = e;
return array;
});
}
final Spliterator<E[]> spliterator = new Spliterators.AbstractSpliterator<E[]>(Long.MAX_VALUE, Spliterator.NONNULL | Spliterator.ORDERED) {
private final Iterator<? extends E> upstreamIterator = upstream.iterator();
@Override
public boolean tryAdvance(final Consumer<? super E[]> downstreamAction) {
final E[] array = createArray.apply(size);
int i;
for ( i = 0; i < size && upstreamIterator.hasNext(); i++ ) {
array[i] = upstreamIterator.next();
}
if ( i == 0 ) {
return false;
}
downstreamAction.accept(i == size ? array : Arrays.copyOf(array, i));
return true;
}
};
return StreamSupport.stream(spliterator, false);
}
}
Not sure how the above works for parallel streams, but the idea of wrapping streams into each other makes sense in certain cases at least for sequential streams. Now the test
public final class MoreStreamsTest {
@Test
public void testWindowed() {
final Iterable<Integer> actual = MoreStreams.windowed(generate(), 3, SaleTxn[]::new)
.mapToInt(saleTxns -> Stream.of(saleTxns)
.mapToInt(saleTxn -> saleTxn.amount)
.sum()
)
// for test only
.mapToObj(Integer::valueOf)
.collect(Collectors.toList());
Assertions.assertIterableEquals(ImmutableList.of(400, 500, 1000), actual);
}
private static Stream<SaleTxn> generate() {
return Stream.of(
new SaleTxn(1, "2018-10-10", 100),
new SaleTxn(2, "2018-10-11", 200),
new SaleTxn(3, "2018-10-12", 100),
new SaleTxn(4, "2018-10-13", 100),
new SaleTxn(5, "2018-10-14", 200),
new SaleTxn(6, "2018-10-15", 200),
new SaleTxn(7, "tail", 1000)
);
}
}
If the codebase is using Lombok, this can even look like this if extension methods support is enabled:
generate()
.windowed(3, SaleTxn[]::new)
...