2

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);
NikNik
  • 2,191
  • 2
  • 15
  • 34

4 Answers4

2

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();
Ruslan
  • 6,090
  • 1
  • 21
  • 36
  • In my real case, I've used comparingByValue so in my opinion, this solution is the simplest and flexible – NikNik Mar 10 '20 at 07:32
2

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
Andreas
  • 154,647
  • 11
  • 152
  • 247
  • 1
    @ernest_k So few people use the "lookup nearest" methods of `TreeMap` (and `TreeSet`): `first`, `lower` (<), `floor` (<=), `ceiling` (>=), `higher` (>), `last`. – Andreas Mar 05 '20 at 10:00
  • @ernest_k I was considering your solution as well :) – NikNik Mar 05 '20 at 10:44
  • @Andreas it seems that for me it's ordering the keys, so the age, and you will get a different value – NikNik Mar 05 '20 at 10:44
  • Ex. if you use UserA, 40 (instead if UserA, 20) and UserD, 40 (instead of UserD, 20) , you will get a different result – NikNik Mar 05 '20 at 10:50
  • @NikNik I can undelete if you need to look at it again, I just found that you already have good and simpler solution in these other answers. – ernest_k Mar 05 '20 at 10:50
  • @NikNik If you change both `UserA` and `UserD` from age 20 to age 40, then of course you get a different result, because they are no longer the youngest persons ("minimum age"), `UserC` is at age 23, so the new result is `30`, as it should be. – Andreas Mar 05 '20 at 11:12
  • @Andreas Ahn I realized that the question is slightly different from what I wanted :D anyways it helped me a lot to find the final solution and yes, it's working for what I asked :). Thanks – NikNik Mar 05 '20 at 11:18
  • With this solution, you're creating a new `TreeMap` data structure each time you call `questionsByYoungest`, right? With that approach, you're better off just looping through the whole `Map` to find the right key and then loop again (if needed) to do the operation. – Spidey Mar 05 '20 at 12:49
  • 1
    @Spidey you [can do it in single pass](https://stackoverflow.com/a/60549731/2711488) – Holger Mar 05 '20 at 16:22
  • @Holger that's why there's a parenthesis saying `(if needed)`. Most of the time not needed and even if you do it in two loops it won't make the overall complexity a lot worse (and the code readability will be a lot better). – Spidey Mar 06 '20 at 00:03
2

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())));
Holger
  • 285,553
  • 42
  • 434
  • 765
1

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.

Ali Ben Zarrouk
  • 1,891
  • 16
  • 24
  • 1
    I would highly recommend not using `forEach()` here and use a normal `for` loop, so you don't have to do that bad array stuff to circumvent the effectively-final requirement of lambda expressions. – Andreas Mar 05 '20 at 09:57
  • Totally agree, will change it. Also saves cost of streaming. – Ali Ben Zarrouk Mar 05 '20 at 09:58
  • 2
    You get even more efficient when you declare `minAge` and `total` as `int`. It eliminates unboxing and reboxing for `<` and `+=` and also allows replacing `me.getKey().age.equals(minAge)` with `me.getKey().age == minAge`. – Holger Mar 05 '20 at 17:37