NikNik
NikNik

Reputation: 2301

Grouping by to find a minimum stream

I have the following (simplifying my real classes) HashMap<User, Counter> and

class User {
  private String name;
  private Integer age;
}

class Counter {
  private Integer numberOfQuestions;
  private Long maxQuestionId;
}

I would like to find the sum of number of questions for the minimum age.

Ex.

UserA, 20 -> 11, 100L;
UserB, 25 -> 15, 100L;
UserC, 23 -> 30, 100L;
UserD, 20 -> 11, 100L,
UserE, 25 -> 15, 100L;

The result should be 22 -> sum of the minimum number of question per age (age: 20, nOfQuestions 11+11=22)

I tried with java streams:

    Integer min = userQuestionHashMap.entrySet().stream()
// not working    .collect(groupingBy(Map.Entry::getKey::getAge), 
                      summingInt(Map.Entry::getValue::getNumOfQuestion))
                   )
// missing
                    .mapToInt(value -> value.getValue().getNumOfQuestion())
                    .min().orElse(0);

Upvotes: 2

Views: 292

Answers (4)

Ali Ben Zarrouk
Ali Ben Zarrouk

Reputation: 2020

I think this is the fastest way:

    int minAge = Integer.MAX_VALUE;
    int total = 0;

    for (Map.Entry me : map.entrySet()) {
      if (me.getKey().age < minAge) {
        minAge = me.getKey().age;
        total = me.getValue().numberOfQuestions;
      } else if (me.getKey().age.equals(minAge)) {
        total += me.getValue().numberOfQuestions;
      }
    }

You only go once through the map.

Upvotes: 1

Holger
Holger

Reputation: 298123

An efficient solution not requiring to build an entire map, is to use the custom collector of this answer.

Since it’s written for max elements, we have to either, reverse the comparator:

int min = userQuestionHashMap.entrySet().stream()
    .collect(maxAll(Collections.reverseOrder(
                        Map.Entry.comparingByKey(Comparator.comparingInt(User::getAge))),
                 Collectors.summingInt(e -> e.getValue().getNumberOfQuestions())));

or create a specialized min version

public static <T, A, D> Collector<T, ?, D> minAll(
    Comparator<? super T> comparator, Collector<? super T, A, D> downstream) {
    Supplier<A> downSupplier = downstream.supplier();
    BiConsumer<A, ? super T> downAccumulator = downstream.accumulator();
    BinaryOperator<A> downCombiner = downstream.combiner();
    Function<A, D> downFinisher = downstream.finisher();
    class Container {
        A acc;
        T obj;
        boolean hasAny;

        Container(A acc) {
            this.acc = acc;
        }
    }
    return Collector.of(() -> new Container(downSupplier.get()),
        (c, t) -> {
            int cmp = c.hasAny? comparator.compare(t, c.obj): -1;
            if (cmp > 0) return;
            if(cmp != 0) {
                c.acc = downSupplier.get();
                c.obj = t;
                c.hasAny = true;
            }
            downAccumulator.accept(c.acc, t);
        },
        (c1, c2) -> {
            if(!c1.hasAny) return c2;
            int cmp = c2.hasAny? comparator.compare(c1.obj, c2.obj): -1;
            if(cmp > 0) return c2;
            if(cmp == 0) c1.acc = downCombiner.apply(c1.acc, c2.acc);
            return c1;
        },
        c -> downFinisher.apply(c.acc));
}

Then, you can use it like

int min = userQuestionHashMap.entrySet().stream()
    .collect(minAll(Map.Entry.comparingByKey(Comparator.comparingInt(User::getAge)),
        Collectors.summingInt(e -> e.getValue().getNumberOfQuestions())));

or alternatively

int min = userQuestionHashMap.entrySet().stream()
    .collect(minAll(Comparator.comparingInt(e -> e.getKey().getAge()),
        Collectors.summingInt(e -> e.getValue().getNumberOfQuestions())));

Upvotes: 2

Andreas
Andreas

Reputation: 159086

You can do it like this:

static int questionsByYoungest(Map<User, Counter> inputMap) {
    if (inputMap.isEmpty())
        return 0;
    return inputMap.entrySet().stream()
            .collect(Collectors.groupingBy(
                    e -> e.getKey().getAge(),
                    TreeMap::new,
                    Collectors.summingInt(e -> e.getValue().getNumberOfQuestions())
            ))
            .firstEntry().getValue();
}

Test

Map<User, Counter> inputMap = Map.of(
        new User("UserA", 20), new Counter(11, 100L),
        new User("UserB", 25), new Counter(15, 100L),
        new User("UserC", 23), new Counter(30, 100L),
        new User("UserD", 20), new Counter(11, 100L),
        new User("UserE", 25), new Counter(15, 100L));
System.out.println(questionsByYoungest(inputMap)); // prints 22

Upvotes: 2

Ruslan
Ruslan

Reputation: 6290

You could group by user age and using summingInt as a downstream. Then find min from result map by key:

Integer sum = map.entrySet().stream()
            .collect(groupingBy(entry -> entry.getKey().getAge(),
                    summingInt(value -> value.getValue().getNumberOfQuestions())))
            .entrySet().stream()
            .min(Map.Entry.comparingByKey())
            .orElseThrow(RuntimeException::new)
            .getValue();

Upvotes: 2

Related Questions