-3

I have an input array as ["Setra","Mercedes","Volvo","Mercedes","Skoda","Iveco","MAN",null,"Skoda","Iveco"]

expected output should be

{Iveco=2, Mercedes=2, Skoda=2, MAN=1, Setra=1, Volvo=1}

meaning a map with the key as Vehicle brands and the value as their occurrence count and the similar valued elements should be sorted by the keys alphabetically and value.

I have tried like

public static String busRanking(List<String> busModels) {
    Map<String, Long> counters = busModels.stream().skip(1).filter(Objects::nonNull)
            .filter(bus ->  !bus.startsWith("V"))
             .collect(Collectors.groupingBy(bus-> bus, Collectors.counting()));
    Map<String, Long> finalMap = new LinkedHashMap<>(); 
    counters.entrySet().stream()
                       .sorted(Map.Entry.comparingByValue(
                                      Comparator.reverseOrder()))
                       .forEachOrdered(
                                  e -> finalMap.put(e.getKey(), 
                                                    e.getValue()));
    return finalMap.toString();
}

public static void main(String[] args) {
    List<String> busModels = Arrays.asList( "Setra","Mercedes","Volvo","Mercedes","Skoda","Iveco","MAN",null,"Skoda","Iveco");
    String busRanking = busRanking(busModels);
    System.out.println(busRanking);
}

And the output I am getting {Skoda=2, Mercedes=2, Iveco=2, Setra=1, MAN=1}

Any suggestion? And the output has to be obtained using single stream()

Łukasz Rzeszotarski
  • 5,791
  • 6
  • 37
  • 68
Bhabadyuti
  • 1,249
  • 1
  • 9
  • 13
  • 2
    Why do you expect `Volvo` to be an invalid map entry without a value after filtering everything out that starts with `V`? – baao Jun 28 '19 at 08:18
  • ... seems like you're looking for `thenComparing` using the key in the `sorted` operation. – Naman Jun 28 '19 at 08:19
  • 3
    Possible duplicate of [Java count occurrence of each item in an array](https://stackoverflow.com/questions/8098601/java-count-occurrence-of-each-item-in-an-array) – baao Jun 28 '19 at 08:19
  • @chris p bacon No. thats part of requirement that any string start with V has to be ignored. – Bhabadyuti Jun 28 '19 at 08:20
  • 2
    It's still in your expected output though – baao Jun 28 '19 at 08:20
  • @Chris No I have to use single stream. also the same valued elements has to be sorted. should be { Iveco=2, Mercedes=2,Skoda=2,MAN=1, Setra=1 } – Bhabadyuti Jun 28 '19 at 08:30
  • 1
    @BhabadyutiBal you can always edit the question to improve the information there. Edited the expected output based on the above comment. – Naman Jun 28 '19 at 09:08
  • @Naman But why it has been marked negative ? – Bhabadyuti Jun 28 '19 at 09:10
  • @BhabadyutiBal The website does not impose people downvoting to leave a reason behind... and one has to deal with that to onboard it. But yes, mostly unclear questions or questions to be closed are downvoted quickly. That's why it's worth improving the question with edits and the votes can be reverted as well. To understand things further - [How To Ask](https://stackoverflow.com/help/how-to-ask) – Naman Jun 28 '19 at 09:15

3 Answers3

1

I think the nice sollution would be:

public static void main(String... args) {
    List<String> busModels = Arrays.asList( "Setra","Mercedes","Volvo","Mercedes","Skoda","Iveco","MAN",null,"Skoda","Iveco");

    Map<String, Long> collect = busModels.stream()
        .filter(Objects::nonNull)
        .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));

    TreeMap<String, Long> stringLongTreeMap = new TreeMap<>(collect);

    Set<Map.Entry<String, Long>> entries = stringLongTreeMap.entrySet();

    ArrayList<Map.Entry<String, Long>> list = new ArrayList<>(entries);

    list.sort((o1, o2) -> o2.getValue().compareTo(o1.getValue()));

    String busRanking = list.toString();

    System.out.println(busRanking);
}
Łukasz Rzeszotarski
  • 5,791
  • 6
  • 37
  • 68
  • Your output: {Iveco=2, MAN=1, Mercedes=2, Setra=1, Skoda=2, Volvo=1} I need {Iveco=2, Mercedes=2, Skoda=2, MAN=1, Setra=1, Volvo=1} – Bhabadyuti Jun 28 '19 at 08:37
  • @BhabadyutiBal however it is not done in one stream transformation. I do not believe it would be possible. See https://stackoverflow.com/questions/39013437/grouping-java8-stream-without-collecting-it. If it was not your requirement can we delete it from your question? – Łukasz Rzeszotarski Jun 28 '19 at 09:44
  • Requirement was to do with single stream – Bhabadyuti Jun 28 '19 at 09:46
  • Seems not to be possible with Collectors.groupingBy, because you would have to collect it without consuming the Stream. What is not possible. – Łukasz Rzeszotarski Jun 28 '19 at 09:49
1

If you can use a third party library, this should work using Streams with Eclipse Collections:

Comparator<ObjectIntPair<String>> comparator = 
    Comparators.fromFunctions(each -> -each.getTwo(), ObjectIntPair::getOne);

String[] strings = {"Setra", "Mercedes", "Volvo", "Mercedes", "Skoda", "Iveco", 
    "MAN", null, "Skoda", "Iveco"};

List<ObjectIntPair<String>> pairs = Stream.of(strings).collect(Collectors2.toBag())
        .select(Predicates.notNull())
        .collectWithOccurrences(PrimitiveTuples::pair, Lists.mutable.empty())
        .sortThis(comparator);

System.out.println(pairs);

Outputs:

[Iveco:2, Mercedes:2, Skoda:2, MAN:1, Setra:1, Volvo:1]

This can also be simplified slightly by not using Streams.

List<ObjectIntPair<String>> pairs = Bags.mutable.with(strings)
        .select(Predicates.notNull())
        .collectWithOccurrences(PrimitiveTuples::pair, Lists.mutable.empty())
        .sortThis(comparator);

Note: I am a committer for Eclipse Collections

Donald Raab
  • 6,458
  • 2
  • 36
  • 44
0

'One-liner' using the standard Java API:

Map<String, Long> map = busModels.stream()
    .filter(Objects::nonNull)
    .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
    .entrySet().stream()
    .sorted(Comparator
        .comparing((Entry<String, Long> e) -> e.getValue()).reversed()
        .thenComparing(e -> e.getKey()))
    .collect(Collectors.toMap(Entry::getKey, Entry::getValue, (left, right) -> left + right, LinkedHashMap::new));

Here's what happens:

  • It first filters out nulls.
  • Then it collects into a map, grouping by the number of occurrences.
  • Then it walks over the entries of the resulting map, in order to sort it by the value first, and then comparing the key (so if two number of occurrences are the same, then Iveco comes before MAN).
  • At last, we're extracting the entries into a LinkedHashMap, which preserves the insertion order.

Output:

{Iveco=2, Mercedes=2, Skoda=2, MAN=1, Setra=1, Volvo=1}

This uses a single, method-chaining statement. I wouldn't, however, call this a single stream operation, since in fact two streams are created, one to collect the occurrences, and a new one of the entry set to sort.

Streams are normally not designed to be traversed more than once, at least not in the standard Java API.

Can it be done with StreamEx?

There exists a library called StreamEx, which perhaps contains a method which allows us to do it with a single stream operation.

Map<String, Long> map = StreamEx.of(buses)
    .filter(Objects::nonNull)
    .map(t -> new AbstractMap.SimpleEntry<>(t, 1L))
    .sorted(Comparator.comparing(Map.Entry::getKey))
    .collapse(Objects::equals, (left, right) -> new AbstractMap.SimpleEntry<>(left.getKey(), left.getValue() + right.getValue()))
    .sorted(Comparator
        .comparing((Entry<String, Long> e) -> e.getValue()).reversed()
        .thenComparing(e -> e.getKey()))
    .collect(Collectors.toMap(Entry::getKey, Entry::getValue, (left, right) -> left + right, LinkedHashMap::new));

Here's what happens:

  • We filter out nulls.
  • We map it to a stream of Entrys, each key being the bus name, and each value being the initial number of occurrences, which is 1.
  • Then we sort the stream by each key, making sure that all the same buses are adjacent in the stream.
  • That allows us to use the collapse method, which merges series of adjacent elements which satisfy the given predicate using the merger function. So Mercedes => 1 + Mercedes => 1 becomes Mercedes => 2.
  • Then we sort and collect as described above.

This at least seems to be doing in a single stream operation.

MC Emperor
  • 22,334
  • 15
  • 80
  • 130