7

Given the following class and data structure below, I want to calculate the sum of count for each consecutive 3 element similar to following results:

public class SaleTxn {
    private int id;
    private String txnDate;
    private int amount;

}

Data as below

 id   txnDate        amount
    1    2018-10-10     100
    2    2018-10-11     200
    3    2018-10-12     100
    4    2018-10-13     100
    5    2018-10-14     200
    6    2018-10-15     200
    ... ...

And the window size is 3, means just sum of the past 3 element, and the expected result like below

2018-10-10 ~ 2018-10-12   Total: 100+200+100 = 400
2018-10-13 ~ 2018-10-14   Total: 100+200+200 = 500
...

I have a List code below:

List<SaleTxn> myList; //
myList.stream().filter(x -> ??????)
                .mapToInt(SouthboundShareholding::getAmount)
                .sum();

How can I implement?

Hülya
  • 3,353
  • 2
  • 12
  • 19
user1625812
  • 187
  • 4

6 Answers6

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]
TongChen
  • 1,414
  • 1
  • 11
  • 21
2

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 + "]";
}
Hülya
  • 3,353
  • 2
  • 12
  • 19
  • @Holger I don't wanna say 'Oh right!' again but Oh right!!! I don't have a solid idea about how good having a list as a key but I updated my answer. Maybe a custom object with paramaters as subList and sum can be created. – Hülya Jan 14 '21 at 17:23
  • 2
    The `List` interface defines equality and hash code, so they work, as long as they don’t get modified after putting into the map. But dedicated key types may perform a bit better. – Holger Jan 14 '21 at 19:22
2

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)
    ...
  • 2
    `tryAdvance` must return `true` when there was an element. It is therefore wrong to call `downstreamAction.accept(array)`, followed by `return false;` for the last element. – Holger Jan 14 '21 at 16:18
  • 2
    Likewise, it is not correct to return `true` when you didn’t call `accept`. One `tryAdvance` invocation should try to fetch as many elements from the source as necessary to fill an array and do either, call `accept` with the array and return `true`, or just return `false`. – Holger Jan 14 '21 at 16:24
  • @Holger I seem to have realized it now. I always have "old iterators background" in my mind when implementing spliterators, so I treated the previous returns as if I were writing an iterator: `true` -- there is something to arrive, `false` -- no more elements (this is why I also I was wondering _why_ the consuming accept does not detect if it was called itself, however it can from the calling side). Once I "folded" the one-step approach to an as-many-as-possible approach, I realized why the return value matters. I guess so. Not sure if I get the whole idea right in the fixed implementation. – terrorrussia-keeps-killing Jan 14 '21 at 17:45
  • 2
    This version seems to be correct, but can be simplified: `if( i == 0 ) { return false; } downstreamAction.accept(i == size? array: Arrays.copyOf(array, i)); return true;` – Holger Jan 15 '21 at 08:12
  • @Holger I usually prefer `if`/`else` over `?`/`:` where dealing with `final` variables even for simple cases (I find it somewhat more safe and diff-friendly when "flattened"), but your suggestion for `Arrays.copyOf` is great at least for two reasons: 1) I saved on an array factory method call; 2) I learned to use a _new_ method which I didn't know about using `System.arraycopy` for everything. I'll keep it all in my head now. Thank you! – terrorrussia-keeps-killing Jan 15 '21 at 10:06
0

You can do this:

List<Integer> sums = new ArrayList<Integer>();
for (int i = 0; i <= myList.size() - windowSize; i+=windowSize) {
    if (i % windowSize == 0) {
        sums.add( mylist.stream().skip(i).limit(windowSize).mapToInt(SouthboundShareholding::getAmount).sum() );
    }
}
TimonNetherlands
  • 1,033
  • 1
  • 6
  • 6
  • 3
    It’s important to keep in mind that each `stream().skip(i)` starts iterating from the beginning again. See also [Schlemiel the Painter's Algorithm](https://en.wikichip.org/wiki/schlemiel_the_painter%27s_algorithm)… – Holger Jan 14 '21 at 11:27
-1

You can try some thing like this:

for (int i = 0; i < myList.size(); i+=3) { System.out.println(myList.stream().skip(i).limit(3).mapToInt(SaleTxn::getAmount).sum());
}

Abdelhak
  • 8,299
  • 4
  • 22
  • 36
-2

You can use Collectors.groupingBy like this:

final int windowSize = 3;
final Map<Integer, List<SaleTxn>> collect = saleTxns.stream().collect(Collectors.groupingBy(saletxn -> (saletxn.id-1) / windowSize));

You will obtain a map with a key for each window. This will group the SaleTxn according to their ID value into windows of 3 starting from 0. Then, with the map you should be able to sum the value or do other business if needed.

T.Lucas
  • 848
  • 4
  • 8
  • 1
    No, a division is needed her, not a modulo. You want to have the ID of the window not the position in the window. – T.Lucas Jan 14 '21 at 10:35
  • IMO this should be the accepted answer. Sry for the downvote but I'm not able to undo it. – dbl Jan 14 '21 at 13:30