Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to compute two aggregate functions with Java streams?

I have a list of "Request" objects (id, amount, price) like this

List<Request> requests = Arrays.asList(
        new Request(id++, 20, 59.28),
        new Request(id++, 10, 61.23),
        new Request(id++, 30, 60.67),
        new Request(id++, 25, 60.16),
        new Request(id++, 60, 59.67));

and I want to compute two metrics - sum(amount) and sum(amount * price) - in one iteration. I need them to calculate the average price: sum(amount * price) / sum(amount).

Taking into account that I want to use Java 8 streams, the only variant that I found is mapping value into Pair object and implementing a custom consumer:

static class Aggregate implements Consumer<Pair<Double, Double>> {
    private double count = 0L;
    private double sum = 0L;

    public double average() {
        return count > 0 ? sum/(double) count : 0;
    }

    public void combine(Aggregate other) {
        count += other.count;
        sum += other.sum;
    }

    @Override
    public void accept(Pair<Double, Double> data) {
        this.count += data.getLeft();
        this.sum += data.getLeft() * data.getRight();
    }
}

Double avgPrice = requests.stream()
        .map(e -> Pair.<Double, Double>of(e.getAmount(), e.getPrice()))
        .collect(Aggregate::new, Aggregate::accept, Aggregate::combine)
        .average();

This approach looks quite messy - we have to create extra Pair object for each entry :(

Does anyone know a better solution?

like image 857
mikhail Avatar asked Nov 01 '22 12:11

mikhail


1 Answers

Sure. You'll need the custom aggregation, but not the Pair:

 static class Aggregate {
   private long count = 0L;
   private double sum = 0L;
   double average() { return sum / count; }
   void merge(Aggregate other) {
     count += other.count;
     sum += other.sum;
   }
   void add(int count, double value) {
     this.count += count;
     this.sum += count * value;
   }
 }
}

requests.stream().collect(
   Aggregate::new,
   (aggr, request) -> aggr.add(request.getCount(), request.getPrice()),
   Aggregate::merge)
 .average();

And you don't actually need to implement Consumer.

Though honestly, the multi-pass solution is probably very nearly as fast and much simpler...

requests.stream()
      .mapToDouble(request -> request.getCount() * request.getPrice())
      .sum()
   / requests.stream().mapToLong(Request::getCount).sum();
like image 100
Louis Wasserman Avatar answered Nov 14 '22 07:11

Louis Wasserman