JavaRush/Courses/Kotlin SELF/Post-processing results — mapValues, sorting, and output ...

Post-processing results — mapValues, sorting, and output preparation

Open

1. Why post-processing results matters

When you first get a result from groupBy or eachCount, it can feel like: “Oh, the data is right here! Now just println(map) —and we’re done.” And yes, technically that will produce output. But if you’ve ever read a server log on a Friday evening (when you just want to exist peacefully), you know: “output” and a “report” are two different psychological states.

Post-processing is the step where we solve two problems: what to turn the values into (for example, list → list size), and in what order to show the result (by key, by frequency, top‑N). Ideally, we also make the output convenient: align columns, add headers, and make the report readable by humans—not only by the compiler.

Let’s imagine we’re continuing our console mini-app with a list of expenses (we keep them in memory, without classes and without a database—so far everything is honest and simple). Let an expense be a pair (category, amount):

fun main() {
    val expenses = listOf(
        "food" to 120,
        "food" to 80,
        "taxi" to 250,
        "books" to 600,
        "taxi" to 140
    )

    println(expenses) // [(food, 120), (food, 80), (taxi, 250), (books, 600), (taxi, 140)]
}

The list is fine. But as soon as we want a “report by category,” real life starts: we need groupings, counters, sorting, and only then—text.

How to choose: eachCount() or groupBy().mapValues { size }

When you see code like this:

val counts = list.groupBy { key }.mapValues { it.value.size }

a reasonable question comes up: “Why do we even need eachCount, if we can do it like this?” And conversely: “Why group into lists if we only need numbers?”

To avoid confusion, it helps to keep a small comparison table in mind:

Task Tool What you get When it’s convenient
Show details inside each group
groupBy
Map<K, List<T>>
When you will actually iterate over the group’s elements
Count “how many elements” per key
groupingBy().eachCount()
Map<K, Int>
When you don’t need the group’s element list at all
You already have
groupBy
, but now you want a summary
mapValues { size }
Map<K, Int>
When you don’t want to rebuild the pipeline from scratch

So eachCount() is a more direct path to counters. And mapValues { size } is a lifesaver when you already have groups and want to “squeeze” summary numbers out of them.

2. mapValues: transform map values

When you do groupBy, you almost certainly end up with a map whose values are lists: Map<K, List<T>>. That’s logical: a group is a set of elements. But a report often doesn’t want the entire set—it wants a “derived” value: group size, average, a list of names, first/last, and so on.

For such transformations, there is mapValues: a function that keeps the keys as-is but changes the values. It’s a direct continuation of the “collection transformation chains” idea: most operations create a new result and do not mutate the original object.

The most common scenario: “I want the group size”

Let’s group expenses by category:

fun main() {
    val expenses = listOf(
        "food" to 120,
        "food" to 80,
        "taxi" to 250,
        "books" to 600,
        "taxi" to 140
    )

    val byCategory: Map<String, List<Pair<String, Int>>> =
        expenses.groupBy { it.first }

    println(byCategory["taxi"]) // [(taxi, 250), (taxi, 140)]
}

Now we want a map “category → number of entries”:

fun main() {
    val expenses = listOf(
        "food" to 120,
        "food" to 80,
        "taxi" to 250,
        "books" to 600,
        "taxi" to 140
    )

    val countsByCategory: Map<String, Int> = expenses
        .groupBy { it.first }
        .mapValues { entry -> entry.value.size }

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

Here it’s important to feel the mechanics: mapValues receives an entry (a key+value pair), and entry.value is the very list of elements in the group, while size is simply its length.

If it’s easier for you to read via destructuring, you can do this:

fun main() {
    val expenses = listOf("food" to 120, "food" to 80, "taxi" to 250)

    val countsByCategory = expenses
        .groupBy { it.first }
        .mapValues { (_, group) -> group.size }

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

mapKeys is nearby, but not needed today

Sometimes you want to “fix the keys” (for example, convert them to lowercase). For that, there is mapKeys, but today our focus is specifically on post-processing grouping results, and there you more often change the value: list → number, list → string, number → string, and so on.

3. Sorting: work with entries

After counting frequencies, a second pain point appears for beginners: “I want to print it nicely: most frequent categories first.” And then a typical real-life scene happens: you try to “sort a Map,” the compiler looks at you with a touch of sadness, and you realize that Map is not an “ordered list of strings,” but a key → value structure.

The correct move: for sorting, we almost always take entries (the set of key/value records) and turn it into a list that we can then sort and print in the desired order.

Sorting by key

fun main() {
    val counts = mapOf("taxi" to 2, "food" to 5, "books" to 1)

    val sortedByKey = counts.entries.sortedBy { it.key }
    for (e in sortedByKey) {
        println("${e.key} -> ${e.value}")
        // books -> 1
        // food -> 5
        // taxi -> 2
    }
}

Here we sort by it.key. This is convenient for a “reference” report: everything is alphabetical, easy to scan with your eyes.

Sorting by value in descending order

fun main() {
    val counts = mapOf("taxi" to 2, "food" to 5, "books" to 1)

    val sortedByCountDesc = counts.entries.sortedByDescending { it.value }
    for (e in sortedByCountDesc) {
        println("${e.key} -> ${e.value}")
        // food -> 5
        // taxi -> 2
        // books -> 1
    }
}

Top‑N via take(n)

After sorting, it’s easy to take the first N entries:

fun main() {
    val counts = mapOf("taxi" to 2, "food" to 5, "books" to 1, "games" to 4)

    val top2 = counts.entries
        .sortedByDescending { it.value }
        .take(2)

    println(top2.map { it.key to it.value }) // [(food, 5), (games, 4)]
}

take(n) is the basic “take the first N elements” operation for a list.

4. Output preparation: strings and StringBuilder

Sorting and mapValues give us a data structure, but the “final report format” is already a string (or a set of strings). And here you really don’t want to glue everything into one huge println(...); instead, you want readable code: prepare the report lines separately and print them separately.

This is especially useful in our console app: today you print to the console, tomorrow you’ll want to return a string from a function, and the day after tomorrow (in a distant bright future) write it to a file. If the line-building logic is separated, life is easier.

List of strings → join via joinToString

fun main() {
    val counts = mapOf("food" to 5, "taxi" to 2, "books" to 1)

    val lines = counts.entries
        .sortedByDescending { it.value }
        .map { "${it.key}: ${it.value}" }

    println(lines.joinToString(separator = "\n"))
    // food: 5
    // taxi: 2
    // books: 1
}

This is already good: readable, predictable. But sometimes you need more control: a header, numbering, alignment.

StringBuilder: when the report is a multi-line document

We’ve already used StringBuilder before to build large strings without endless concatenation. And a nice Kotlin bonus is that StringBuilder has convenient extension functions like appendLine, which make assembling multi-line text almost “literary.”

Let’s build a simple report with a header:

import kotlin.text.appendLine

fun main() {
    val counts = mapOf("food" to 5, "taxi" to 2, "books" to 1)

    val report = StringBuilder()
        .appendLine("Expenses by category (count)")
        .appendLine("---------------------------")
        .apply {
            for (e in counts.entries.sortedByDescending { it.value }) {
                appendLine("${e.key}: ${e.value}")
            }
        }
        .toString()

    println(report)
    // Expenses by category (count)
    // ---------------------------
    // food: 5
    // taxi: 2
    // books: 1
}

Note: we are not discussing scope functions as a topic today, but you can treat apply here simply as a “convenient block to keep writing into the builder.” If it’s easier for you so far, you can do it without apply:

import kotlin.text.appendLine

fun main() {
    val counts = mapOf("food" to 5, "taxi" to 2)

    val sb = StringBuilder()
    sb.appendLine("Report")
    for (e in counts.entries.sortedByDescending { it.value }) {
        sb.appendLine("${e.key}: ${e.value}")
    }

    println(sb.toString())
    // Report
    // food: 5
    // taxi: 2
}

Aligning columns with padEnd and padStart

Sometimes a report is much easier to read if numbers line up under numbers. For that, it’s convenient to use padEnd/padStart.

import kotlin.text.appendLine

fun main() {
    val counts = mapOf("food" to 12, "taxi" to 2, "books" to 100)

    val sb = StringBuilder()
    sb.appendLine("Category        Count")
    sb.appendLine("---------------------")

    for (e in counts.entries.sortedByDescending { it.value }) {
        val category = e.key.padEnd(14)
        val count = e.value.toString().padStart(5)
        sb.appendLine("$category$count")
    }

    println(sb.toString())
    // Category        Count
    // ---------------------
    // books            100
    // food              12
    // taxi               2
}

This is not “cosmetics for cosmetics’ sake.” It’s real care for your future self: when the report is large, alignment reduces cognitive load.

5. Practical example: category report

Now let’s make a cohesive piece of logic that looks like a real step in a project: we have a list of expenses, and we want to get a report “how many entries per category” in sorted form.

First, a data-preparation function. We won’t complicate the model: still Pair<String, Int>.

fun buildCategoryCounts(expenses: List<Pair<String, Int>>): Map<String, Int> {
    return expenses
        .groupBy { (category, _) -> category.trim().lowercase() }
        .mapValues { (_, group) -> group.size }
}

Now, a function that turns the map into nice text:

import kotlin.text.appendLine

fun formatCategoryCounts(counts: Map<String, Int>, topN: Int): String {
    val sorted = counts.entries
        .sortedByDescending { it.value }
        .take(topN)

    val sb = StringBuilder()
    sb.appendLine("Top categories by number of expenses")
    sb.appendLine("-----------------------------------")

    for ((category, count) in sorted) {
        sb.appendLine("${category.padEnd(15)} ${count.toString().padStart(4)}")
    }

    return sb.toString()
}

And finally, main, which demonstrates the whole pipeline:

fun main() {
    val expenses = listOf(
        " Food " to 120,
        "food" to 80,
        "taxi" to 250,
        "BOOKS" to 600,
        "taxi" to 140,
        "food" to 30
    )

    val counts = buildCategoryCounts(expenses)
    val report = formatCategoryCounts(counts, topN = 3)

    println(report)
    // Top categories by number of expenses
    // -----------------------------------
    // food              3
    // taxi              2
    // books             1
}

Here we did exactly what we talked about: post-processing grouping results turns data into a form suitable for human reading. And yes, that human is you in a week.

6. Common mistakes when post-processing Map

Mistake #1: expecting mapValues to “change the map in place.”
Beginners often think: “I called mapValues, so the map is updated.” But most collection operations in Kotlin return a new result, and do not touch the original collections. So if you did counts.mapValues{...} and never stored the result anywhere, you just computed into the void—like writing a report and forgetting to click “Save.”

Mistake #2: trying to sort a Map instead of entries.
Sorting is an operation on an ordered sequence. A Map by itself is not the place where an output order “must” exist, and printing a map is not meant to be a report. A good habit is: for output, take map.entries, sort it as a list of records (by key or by value), and then print that. This solves both the ordering problem and the “why is the output slightly different each time” problem.

Mistake #3: doing too much logic right inside mapValues { ... }.
mapValues is handy, but if you have half a screen of code inside the lambda with loops, checks, and formatting, reading turns into a quest. In such cases, the code becomes clearer if you extract the complex value computation into a separate function (for example, fun groupSum(group: List<Pair<String, Int>>): Int) and leave a single short line in mapValues.

Mistake #4: sorting the result and then printing the original map.
A funny situation happens: you carefully did val sorted = map.entries.sortedByDescending{...}, and then somewhere below you accidentally wrote println(map). As a result, the console output isn’t sorted, and you start suspecting Kotlin of a conspiracy—when the conspiracy was on the side of a single line. A good habit: for a report, print only what you actually prepared for the report (usually that’s sorted or an already-built string report).

Mistake #5: hoping that “it’ll be fine” without normalizing keys.
If keys come from user input or from “dirty” strings, then "Food", " food ", and "FOOD" will become three different keys, and your “top categories” will look as if you ate in three different universes. Normalization (trim().lowercase()) should happen before grouping and before counting—otherwise you will nicely sort and print… incorrect statistics.

1
Task
Kotlin SELF,  24 level,  2 lecture
Locked
Letter summary
Letter summary
1
Task
Kotlin SELF,  24 level,  2 lecture
Locked
Average grades
Average grades
1
Task
Kotlin SELF,  24 level,  2 lecture
Locked
Alphabetical report
Alphabetical report
1
Task
Kotlin SELF,  24 level,  2 lecture
Locked
Sales leaderboard
Sales leaderboard
Комментарии
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны sign in
This page has no comments yet