Search code examples
javajava-8java-stream

Cumulative sum of multiple object attributes in a Java Stream


I have a list of object sorted on a month-year String attribute.

My object class defination looks like:

public class Obj {
    String year;
    Long membercount;
    Long nonmembercount;
    Double memberpayment;
    Double nonmemberpayment;
}
new Obj("9-2015",100,20,10,5),
new Obj("10-2015",220,40,20,55),
new Obj("11-2015",300,60,30,45),
new Obj("12-2015",330,30,50,6),
new Obj("1-2016",100,10,10,4)
)

I want to do cumulative sum on membercount, nonmembercount, memberpayment, and nonmemberpayment.

So my new List of objects would be like below:

new Obj("9-2015",100,20,10,5)
new Obj("10-2015",320,60,30,60)
new Obj("11-2015",620,120,60,105)
new Obj("12-2015",950,150,110,111)
new Obj("1-2016",1050,160,120,115)

I tried with Collectors.summingDouble but it gives me all sum not cumulative.

I would really appreciate any pointers.


Solution

  • There is no direct support for cumulative operations in the Stream API, though it would be possible to implement such an operation via a custom Collector. But it’s worth noting that there is already a direct support for such operations on arrays, which might be sufficient for your case:

    Extending you sketch of Obj to

    public class Obj {
        String year;
        Long membercount;
        Long nonmembercount;
        Double memberpayment;
        Double nonmemberpayment;
    
        public Obj(String year, long membercount, long nonmembercount,
                double memberpayment, double nonmemberpayment) {
            this.year = year;
            this.membercount = membercount;
            this.nonmembercount = nonmembercount;
            this.memberpayment = memberpayment;
            this.nonmemberpayment = nonmemberpayment;
        }
    
        @Override
        public String toString() {
            return "Obj("+year+", "+membercount+", "+nonmembercount
                +", "+memberpayment+", "+nonmemberpayment+')';
        }
    }
    

    the solution can look like:

    // test data
    List<Obj> list=Arrays.asList(
        new Obj("9-2015", 100, 20, 10, 5),
        new Obj("10-2015", 220, 40, 20, 55),
        new Obj("11-2015", 300, 60, 30, 45),
        new Obj("12-2015", 330, 30, 50, 6),
        new Obj("1-2016", 100, 10, 10, 4));
    
    // creating an array as need for the operation, it will contain the
    // result afterwards, whereas the source list is not modified
    Obj[] array = list.toArray(new Obj[0]);
    
    // the actual operation
    Arrays.parallelPrefix(array, (a,b) -> new Obj(b.year,
        a.membercount + b.membercount,
        a.nonmembercount + b.nonmembercount,
        a.memberpayment + b.memberpayment,
        a.nonmemberpayment + b.nonmemberpayment
    ));
    
    // just print the result
    Arrays.asList(array).forEach(System.out::println);
    

    the last line will print

    Obj(9-2015, 100, 20, 10.0, 5.0)
    Obj(10-2015, 320, 60, 30.0, 60.0)
    Obj(11-2015, 620, 120, 60.0, 105.0)
    Obj(12-2015, 950, 150, 110.0, 111.0)
    Obj(1-2016, 1050, 160, 120.0, 115.0)
    

    While this operation is unlikely to benefit from parallel processing for this small number of elements, there is unfortunately no sequential version of this operation. So you might consider using an ordinary loop solution instead…


    For completeness, here is a Stream Collector based solution for cumulative operations. Like with Arrays.parallelPrefix, the update function must be side-effect-free and associative, which is the case for a function returning a new object with summed up properties.

    public static <T> Collector<T,?,List<T>> cumulative(BinaryOperator<T> update) {
        return Collector.of(ArrayList::new,
            (l,o) -> {
                if(!l.isEmpty()) o=update.apply(l.get(l.size()-1), o);
                l.add(o);
            },
            (l,m) -> {
                if(l.isEmpty()) return m;
                if(!m.isEmpty()) {
                    T a = l.get(l.size()-1);
                    for(T b: m) l.add(update.apply(a, b));
                }
                return l;
            });
    }
    

    using it with the setup as above:

    List<Obj> result = list.stream().collect(cumulative((a,b) -> new Obj(b.year,
        a.membercount + b.membercount,
        a.nonmembercount + b.nonmembercount,
        a.memberpayment + b.memberpayment,
        a.nonmemberpayment + b.nonmemberpayment
    )));