user7859969
user7859969

Reputation: 53

Java 8 Streams : Count the occurrence of elements(List<String> list1) from list of text data(List<String> list2)

Input :

List<String> elements= new ArrayList<>();
        elements.add("Oranges");
        elements.add("Figs");
        elements.add("Mangoes");
        elements.add("Apple");

List<String> listofComments = new ArrayList<>();
        listofComments.add("Apples are better than Oranges");
        listofComments.add("I love Mangoes and Oranges");
        listofComments.add("I don't know like Figs. Mangoes are my favorites");
        listofComments.add("I love Mangoes and Apples");

Output : [Mangoes, Apples, Oranges, Figs] -> Output must be in descending order of the number of occurrences of the elements. If elements appear equal no. of times then they must be arranged alphabetically.

I am new to Java 8 and came across this problem. I tried solving it partially; I couldn't sort it. Can anyone help me with a better code?

My piece of code:

Function<String, Map<String, Long>> function = f -> {
            Long count = listofComments.stream()
                    .filter(e -> e.toLowerCase().contains(f.toLowerCase())).count();
            Map<String, Long> map = new HashMap<>(); //creates map for every element. Is it right?
            map.put(f, count);
            return map;
        };

elements.stream().sorted().map(function).forEach(e-> System.out.print(e));

Output: {Apple=2}{Figs=1}{Mangoes=3}{Oranges=2}

Upvotes: 5

Views: 6717

Answers (3)

Holger
Holger

Reputation: 298311

In real life scenarios you would have to consider that applying an arbitrary number of match operations to an arbitrary number of comments can become quiet expensive when the numbers grow, so it’s worth doing some preparation:

Map<String,Predicate<String>> filters = elements.stream()
    .sorted(String.CASE_INSENSITIVE_ORDER)
    .map(s -> Pattern.compile(s, Pattern.LITERAL|Pattern.CASE_INSENSITIVE))
    .collect(Collectors.toMap(Pattern::pattern, Pattern::asPredicate,
        (a,b) -> { throw new AssertionError("duplicates"); }, LinkedHashMap::new));

The Predicate class is quiet valuable even when not doing regex matching. The combination of the LITERAL and CASE_INSENSITIVE flags enables searches with the intended semantic without the need to convert entire strings to lower case (which, by the way, is not sufficient for all possible scenarios). For this kind of matching, the preparation will include building the necessary data structure for the Boyer–Moore Algorithm for more efficient search, internally.

This map can be reused.

For your specific task, one way to use it would be

filters.entrySet().stream()
    .map(e -> Map.entry(e.getKey(), listofComments.stream().filter(e.getValue()).count()))
    .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
    .forEachOrdered(e -> System.out.printf("%-7s%3d%n", e.getKey(), e.getValue()));

which will print for your example data:

Mangoes  3
Apple    2
Oranges  2
Figs     1

Note that the filters map is already sorted alphabetically and the sorted of the second stream operation is stable for streams with a defined encounter order, so it only needs to sort by occurrences, the entries with equal elements will keep their relative order, which is the alphabetical order from the source map.

Map.entry(…) requires Java 9 or newer. For Java 8, you’d have to use something like
new AbstractMap.SimpleEntry(…) instead.

Upvotes: 3

Naman
Naman

Reputation: 31958

You can still modify your function to store Map.Entry instead of a complete Map

Function<String, Map.Entry<String, Long>> function = f -> Map.entry(f, listOfComments.stream()
        .filter(e -> e.toLowerCase().contains(f.toLowerCase())).count());

and then sort these entries before performing a terminal operation forEach in your case to print

elements.stream()
        .map(function)
        .sorted(Comparator.comparing(Map.Entry<String, Long>::getValue)
                .reversed().thenComparing(Map.Entry::getKey))
        .forEach(System.out::println);

This will then give you as output the following:

Mangoes=3
Apples=2
Oranges=2
Figs=1

Upvotes: 1

vbezhenar
vbezhenar

Reputation: 12336

First thing is to declare an additional class. It'll hold element and count:

class ElementWithCount {
    private final String element;
    private final long count;

    ElementWithCount(String element, long count) {
        this.element = element;
        this.count = count;
    }

    String element() {
        return element;
    }

    long count() {
        return count;
    }
}

To compute count let's declare an additional function:

static long getElementCount(List<String> listOfComments, String element) {
    return listOfComments.stream()
            .filter(comment -> comment.contains(element))
            .count();
}

So now to find the result we need to transform stream of elements to stream of ElementWithCount objects, then sort that stream by count, then transform it back to stream of elements and collect it into result list.

To make this task easier, let's define comparator as a separate variable:

Comparator<ElementWithCount> comparator = Comparator
        .comparing(ElementWithCount::count).reversed()
        .thenComparing(ElementWithCount::element);

and now as all parts are ready, final computation is easy:

List<String> result = elements.stream()
        .map(element -> new ElementWithCount(element, getElementCount(listOfComments, element)))
        .sorted(comparator)
        .map(ElementWithCount::element)
        .collect(Collectors.toList());

You can use Map.Entry instead of a separate class and inline getElementCount, so it'll be "one-line" solution:

List<String> result = elements.stream()
        .map(element ->
                new AbstractMap.SimpleImmutableEntry<>(element,
                        listOfComments.stream()
                                .filter(comment -> comment.contains(element))
                                .count()))
        .sorted(Map.Entry.<String, Long>comparingByValue().reversed().thenComparing(Map.Entry.comparingByKey()))
        .map(Map.Entry::getKey)
        .collect(Collectors.toList());

But it's much harder to understand in this form, so I recommend to split it to logical parts.

Upvotes: 0

Related Questions