8

If I have a collection:

List<Long> numbers = asList(2, 2, 4, 5);

How can I map/process these to build up a running total. To produce something like:

List<Long> runningTotals = asList(2, 4, 8, 13);

Even better, how can I build a list of something (like a tuple) so I can preserve the orignals:

((2 -> 2), (2 -> 4), (4 -> 8), (5 -> 13));
Toby
  • 8,665
  • 6
  • 31
  • 56

5 Answers5

11

You don't need Java 8 to do this. Indeed, this problem doesn't lend itself well to streams, because the calculation is stateful, in that it depends upon the sum of previous elements, so you don't get an advantage from things like parallelization.

You may as well just use a plain old loop:

ListIterator<Long> it = list.listIterator();
Long previous = it.next();  // Assuming the list isn't empty.
while (it.hasNext()) {
  it.set(previous += it.next());
}

In terms of the second requirement, do you really need to preserve the original elements in tuples? You can simply subtract each element in the running total list from its predecessor to recover the original:

AbstractList<Long> something = new AbstractList<Long>() {
  @Override public int size() { return list.size(); }
  @Override public Long get(int i) {
    if (i > 0) {
      return list.get(i) - list.get(i - 1);
    } else {
      return list.get(i);
    }
  }
};
Andy Turner
  • 122,430
  • 10
  • 138
  • 216
  • The first answer doesn't see to work: it would incorrectly calculate the running total as 2, 4, 6, 9 whereas the answer should be 2, 4, 8, 13 (using the inputs from the question) -ah, that was before the edit. Works now :) – Toby Mar 18 '19 at 21:44
5

Update: As Holger pointed out in the comments using Stream.reduce() for this purpose is not correct. See Reduction and Mutable Reduction or Java 8 Streams - collect vs reduce for more information.

You can use Java Stream.collect() instead to generate your list with sums:

List<Long> numbers = Arrays.asList(2L, 2L, 4L, 5L);
List<Pair> results = numbers.stream()
        .collect(ArrayList::new, (sums, number) -> {
            if (sums.isEmpty()) {
                sums.add(new Pair(number, number));
            } else {
                sums.add(new Pair(number, number + sums.get(sums.size() - 1).getSum()));
            }
        }, (sums1, sums2) -> {
            if (!sums1.isEmpty()) {
                long sum = sums1.get(sums1.size() - 1).getSum();
                sums2.forEach(p -> p.setSum(p.getSum() + sum));
            }
            sums1.addAll(sums2);
        });

This combines all the numbers and creates a pair for each number with the addition to the previous sum. It uses the following Pair class as helper:

public class Pair {
    private long number;
    private long sum;

    public Pair(long number, long sum) {
        this.number = number;
        this.sum = sum;
    }

    public long getNumber() {
        return number;
    }

    public void setSum(long sum) {
        this.sum = sum;
    }

    public long getSum() {
        return sum;
    }
}

You can easily change that helper class if you want to add some more information.

The result at the end is:

[
    Pair{number=2, sum=2}, 
    Pair{number=2, sum=4}, 
    Pair{number=4, sum=8}, 
    Pair{number=5, sum=13}
]
Samuel Philipp
  • 9,552
  • 12
  • 27
  • 45
  • Easy to read answer, reminds me of foldLeft. Even without the Pair type, it does what I want and makes it flexible in so much as I can easily swap out the + operator for other operators (which is actually what I want to do) – Toby Mar 19 '19 at 09:31
  • 2
    This is violating the contract of `reduce` in several ways (reduce is *not* a foldLeft). And when you remove everything that is wrong, you end up at a plain simple loop: `List results=new ArrayList<>(); for(Long number: numbers) results.add(new Pair(number, sums.isEmpty()? number: number + sums.get(sums.size() - 1).getSum());` – Holger Mar 19 '19 at 14:28
  • @Holger Thank you for pointing that out, I updated my answer using `Stream.collect()`. – Samuel Philipp Mar 19 '19 at 15:30
  • 1
    `collect` is much better for the job, but `ArrayList::addAll` still is not the right combiner for the operation. You would need something like `(list1, list2) -> { if(!list1.isEmpty()) { long sum = list1.get(list1.size()-1).getSum(); list2.replaceAll(p -> new Pair(p.getNumber(), p.getSum()+sum)); } list1.addAll(list2); }` – Holger Mar 19 '19 at 15:35
  • @Holger Thanks again, for noticing. I edited my post again. Now it should work. – Samuel Philipp Mar 19 '19 at 17:19
  • What does it violate in terms of the contract? – Toby Mar 19 '19 at 20:59
  • @Toby The first version using using `reduce()` manipulates the list in reduce, but `reduce()` should only be used with immutable result objects. – Samuel Philipp Mar 19 '19 at 21:06
  • @Toby `collect()` works with mutable objects, so this was the correct choice. This solution also works with parallel streams. – Samuel Philipp Mar 19 '19 at 21:08
  • 2
    The objects used in `reduce` do not need to be strictly immutable, but they have to be used that way in the reduction function. The first argument (the `ArrayList`) is supposed to be an identity value but after adding elements, it does not fulfill this role. A correct solution would need to create new objects in this scenario, which would be rather expensive. So `collect` solves this issue as it is specifically designed for such scenarios. – Holger Mar 20 '19 at 08:45
  • Why do they need to be immutable? – Toby Mar 20 '19 at 20:26
5

You can use Arrays#parallelPrefix to accomplish your goal:

List<Long> numbers = Arrays.asList(2L, 2L, 4L, 5L);
long[] copiedArray = numbers.stream().mapToLong(Long::longValue).toArray();

Arrays.parallelPrefix(copiedArray, Long::sum);

System.out.println(IntStream.range(0, numbers.size())
        .mapToObj(i -> "(" + numbers.get(i) + " -> " + copiedArray[i] + ")")
        .collect(Collectors.joining(", ", "[", "]")));

Output:

[(2 -> 2), (2 -> 4), (4 -> 8), (5 -> 13)]
Jacob G.
  • 26,421
  • 5
  • 47
  • 96
0

A simple demonstration -

import java.util.Arrays;
import java.util.List;

public class RunningAdditionDemo{
     private static Integer total = 0;
     private String strOutput = new String();

     public static void main(String []args){
        RunningAdditionDemo demo = new RunningAdditionDemo();
        String output = demo.doRunningAddition();

        System.out.println(output);
     }

     public String doRunningAddition() {
        List<Integer> numbers = Arrays.asList(2, 2, 4, 5);
        numbers.stream().forEach(this::addNumber);

        return String.format("( %s )", strOutput.replaceFirst("..$",""));
     }

     private void addNumber(Integer number) {
        total += number;
        strOutput += String.format("( %d -> %d ), ", number, total);
     }
}
Rahul R.
  • 92
  • 5
0

I just used forEach feature of Java 8. I initialized the input List of Long type. I created a temp ArrayList (runningSum) that just store running sums whose value initialized to 0.(at index 1). ValuePair creates number and its runningSum for that position and it is stored in result (List) and displayed. Hope this helps

package net.javapedia;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

class ValuePairs {
    public ValuePairs(Long n, Long s) {
        number = n;
        sum = s;
    }

    Long number;
    Long sum;

    @Override
    public String toString() {

        return new StringBuilder("(").append(this.number).append(" -> ").append(this.sum).append(")").toString();
    }
}

public class RunningSum {

    public static void main(String[] args) {

        List<Long> numbers = Arrays.asList(2L, 2L, 4L, 5L);
        List<Long> tempRunningSum = new ArrayList<>();
        List<ValuePairs> result = new ArrayList<>();
        tempRunningSum.add(0L);
        numbers.stream().forEach(i -> {
            tempRunningSum.set(0, tempRunningSum.get(0) + i);
            result.add(new ValuePairs(i, tempRunningSum.get(0)));
        });

        System.out.println(result);
    }

}

Output:

enter image description here

javapedia.net
  • 1,547
  • 3
  • 14
  • 31