JavaRush/Courses/Kotlin SELF/groupingBy().eachCount() — counting frequencies without g...

groupingBy().eachCount() — counting frequencies without group lists

Open

1. Frequencies: what exactly we want to get

When you hear “frequencies” for the first time, you might imagine boring statistics, but in practice this is one of the most useful mini-operations on data. Frequencies are the answer to the question “how many times did it occur…”. How many expenses are in each category, how many times a user entered a certain command, how many words of each type are in a text — all of that is frequencies.

In Kotlin type terms, we usually want to get a structure like this:

  • key — “what we count” (category, word, first letter, number, etc.)
  • value — “how many times it occurred”

That is, the result will be a frequency map:

Map<K, Int>

A “human-friendly” example:

  • "food" -> 12 (there are 12 entries in the food category)
  • "taxi" -> 3
  • "kotlin" -> 2

At this point, your brain often suggests “let’s make a MutableMap and manually increment a counter.” That’s a fine plan, but the Kotlin standard library can do it in one line — and this is exactly where the combination groupingBy { ... }.eachCount() shows up.

2. Why groupBy is not always suitable for counting

Once you know groupBy, a very natural thought comes up: “OK, I’ll group, and then I’ll take size of each group.” Does it work? Yes. Is it convenient? Sometimes. But there’s a nuance: groupBy returns Map<K, List<T>>, meaning for each key it stores an entire list of elements.

And if you only need the answer “how many” — storing lists becomes unnecessary:

  • lists take memory;
  • lists must be created and filled;
  • then you turn the list into a number (size) anyway and throw away the details.

This is a bit like: you need to know “how many people came to the party,” but instead you create a separate room for each name, herd people into those rooms, then count how many are in each room… and immediately kick everyone out. Powerful, but slightly theatrical.

For “just count,” Kotlin offers another path: first say “I can compute a key for each element,” and then ask “count how many elements each key has.” That is exactly what groupingBy { ... }.eachCount() does.

3. What groupingBy { ... } is and why it is not a Map yet

After groupBy, many people expect: “any grouping returns a Map.” But groupingBy works differently. It returns not a map, but a special object of type Grouping, which is a “grouping draft”: it stores the logic for computing the key, and the concrete result appears later when you apply an aggregating operation.

Let’s look at it in terms of how it feels, not just types. We can split the idea into two steps:

  1. “Here is the rule for getting a key from an element”
  2. “Apply an operation to this rule: for example, count the elements”

The code looks like this:

fun main() {
    val words = listOf("one", "two", "one")

    val grouping = words.groupingBy { it } // not a Map yet!
    val freq = grouping.eachCount()        // now it's Map<String, Int>

    println(freq) // {one=2, two=1}
}

Here it’s important to feel this: groupingBy { ... } is like “filing a request,” and eachCount() is like “getting a finished report.”

4. eachCount(): getting a frequency map Map<K, Int>

When you call eachCount(), Kotlin does exactly what we need: it returns a map where each key is associated with the number of elements.

The most basic example: word frequencies

fun main() {
    val words = listOf("one", "two", "one", "three", "two")

    val freq: Map<String, Int> = words
        .groupingBy { it }
        .eachCount()

    println(freq) // {one=2, two=2, three=1}
}

Notice the nice part: we don’t need MutableMap, put, or “if the key exists — increment it, otherwise — set 1.” Kotlin does all of that for us.

Example with a computed key: group by the first letter

This example is good because the key (Char) differs from the original element (String).

fun main() {
    val words = listOf("one", "two", "three", "four", "five")

    val freqByFirstChar = words.groupingBy { it.first() }.eachCount()

    for ((ch, count) in freqByFirstChar) {
        println("$ch -> $count")
        // o -> 1
        // t -> 2
        // f -> 2
    }
}

Here you reinforce three skills at once: the lambda in groupingBy, working with Char, and iterating over a Map via destructuring (key, value).

5. Normalizing the key before counting

In real data, the problem is most often not counting, but the fact that “the same thing” looks different. Spaces, letter case, random extra characters — all of that creates “fake distinct keys.”

And here you should remember a simple rule: normalize the key right inside groupingBy { ... }, because that’s where it is decided what is considered “the same.”

fun main() {
    val drinks = listOf("Tea", " tea ", "TEA", "Coffee")

    val freq = drinks
        .groupingBy { it.trim().lowercase() }
        .eachCount()

    println(freq) // {tea=3, coffee=1}
}

This is one of those moments where Kotlin literally nudges you toward good design: the “grouping key” is a separate concept. Not “as in the original input,” but “as in your internal data world.”

If in your project categories noticeably “drift” (for example, the user typed " Taxi " instead of "taxi"), normalization doesn’t just improve stats — it saves the entire meaning of the report.

6. Practice: counting the number of expenses by category

From this day on, we already have a practical console project in the course (we started it with collections): a simple expense tracker. Still without OOP, so we will store records as simply as possible: as Triple(category, amount, note).

Let’s make a small data stub and a function that counts “how many records are in each category.” Note: today we count the number of records, not the sum of money. The sum is also important, but that’s a different operation.

Data and counting by category

fun main() {
    val expenses: List<Triple<String, Int, String>> = listOf(
        Triple("food", 120, "groceries"),
        Triple("Food ", 80, "snack"),
        Triple("taxi", 250, "airport"),
        Triple("TAXI", 90, "home")
    )

    val countsByCategory = expenses
        .groupingBy { (category, _, _) -> category.trim().lowercase() }
        .eachCount()

    println(countsByCategory) // {food=2, taxi=2}
}

It’s useful to pause here and read the lambda: (category, _, _) -> .... This is regular destructuring of Triple, which you already know from the day about Pair/Triple. We explicitly say: “the key is the normalized category.”

Extracting into a function (so main doesn’t bloat)

fun countExpensesByCategory(expenses: List<Triple<String, Int, String>>): Map<String, Int> {
    return expenses
        .groupingBy { (category, _, _) -> category.trim().lowercase() }
        .eachCount()
}

fun main() {
    val expenses = listOf(
        Triple("food", 120, "groceries"),
        Triple("Food ", 80, "snack"),
        Triple("taxi", 250, "airport")
    )

    val counts = countExpensesByCategory(expenses)
    println(counts) // {food=2, taxi=1}
}

From a practical point of view, this is a very important step: countExpensesByCategory(...) becomes a small building block for reports. It returns a pure result (a map), and does not print everything itself — which means later we can format the output nicely in any way we want.

7. The key can be anything: strings, numbers, booleans

Sometimes it seems that frequencies are “only for words.” But in fact, the key can be any type: Int, Char, Boolean, even more complex things (but we won’t complicate it yet — we’re still protecting our sanity).

Number frequencies

fun main() {
    val numbers = listOf(10, 10, 20, 30, 30, 30)

    val freq = numbers.groupingBy { it }.eachCount()

    for ((n, count) in freq) {
        println("$n -> $count")
        // 10 -> 2
        // 20 -> 1
        // 30 -> 3
    }
}

“Even / odd” frequencies

The key will be Boolean: true for even numbers, false for odd numbers. It sounds weird until you see how convenient it is.

fun main() {
    val numbers = listOf(1, 2, 3, 4, 5, 6)

    val freq = numbers
        .groupingBy { it % 2 == 0 }
        .eachCount()

    println(freq) // {false=3, true=3}
}

If later you want to print this in human words (“there are N even ones”), that becomes a formatting question, not a counting question — i.e., a proper separation of responsibilities.

Manual counter vs eachCount()

Sometimes it’s useful to see “how it would be manually” once, to better appreciate what the standard library does. Imagine you were given the task “count category frequencies,” and you write it using MutableMap.

fun main() {
    val categories = listOf("food", "food", "taxi")

    val counts = mutableMapOf<String, Int>()
    for (c in categories) {
        val old = counts[c] ?: 0
        counts[c] = old + 1
    }

    println(counts) // {food=2, taxi=1}
}

That’s fine code, and it’s even useful to understand the algorithm’s essence. But in a real project you want it shorter and more expressive:

fun main() {
    val categories = listOf("food", "food", "taxi")

    val counts = categories.groupingBy { it }.eachCount()
    println(counts) // {food=2, taxi=1}
}

Key idea: eachCount() is not “magic from outer space.” It’s just a neat, standard implementation of the “counter by key” pattern that makes code less noisy.

8. Typical mistakes when working with groupingBy().eachCount()

Mistake #1: expecting groupingBy { ... } to immediately return a Map.
After groupBy, your hand automatically writes val m = list.groupingBy { ... } and you want to immediately do m["key"]. But groupingBy returns an intermediate object (conceptually — a “grouping plan”), and you only get a map after an operation like eachCount(). If you find yourself thinking “why can’t I access it like a Map,” it means you forgot the second step.

Mistake #2: counting frequencies by a “dirty key” and being surprised by weird results.
If categories "Taxi", " taxi ", and "TAXI" are treated as different, the problem is not eachCount(): it honestly counts what you told it to count. The solution is to normalize the key in groupingBy: most often trim().lowercase() is enough. This is especially critical in reports: wrong key = wrong stats.

Mistake #3: confusing “number of records” with “sum of values.”
eachCount() counts how many elements ended up in a group. If you have expenses ("food", 100) and ("food", 200), the result for the key "food" will be 2, not 300. This is not a bug or a trick — these are different tasks. Today we’re solving “how many,” not “how much total.”

Mistake #4: printing a map and expecting a nice order.
When you do println(freq), Kotlin prints the map “however it comes out” (the order depends on the internal implementation). That’s fine for debugging, but for a report you almost always need a separate step: sort, format, align. If you feel “everything is correct, but it looks ugly,” you’ve run into a presentation task, not a counting problem — and that’s completely expected.

Mistake #5: making the key too complex right inside the lambda and losing readability.
Sometimes you want to write half a screen of logic in groupingBy. Formally you can, but the code quickly becomes unreadable. If the key computation is complex, it’s better to extract it into a separate function (for example, normalizeCategory(category: String): String) and keep just one clear line in groupingBy.

1
Task
Kotlin SELF,  24 level,  1 lecture
Locked
Drink orders
Drink orders
1
Task
Kotlin SELF,  24 level,  1 lecture
Locked
Starting letters
Starting letters
1
Task
Kotlin SELF,  24 level,  1 lecture
Locked
Hashtags at the booth
Hashtags at the booth
1
Task
Kotlin SELF,  24 level,  1 lecture
Locked
Command log
Command log
Комментарии
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны sign in
This page has no comments yet