RizTech Academy logo
RizTech Academy
CollectionsLesson 5 of 530 min

Practice: transforming a real dataset

Enough rules — this lesson puts the whole module to work on a realistic dataset, the way you will use collections in an actual program. We will take a list of sales from a small Pune tea stall and answer the questions an owner would actually ask, using nothing but the operations you have learned. Every result shown was produced by running the code.

The dataset

A data class (the next module explains these; for now, read it as "a record with named fields") holds each sale:

data class Sale(
    val item: String,
    val category: String,
    val qty: Int,
    val pricePaise: Int,     // price per unit, in paise
    val city: String
)

val sales = listOf(
    Sale("Chai", "beverage", 3, 1500, "Pune"),
    Sale("Vada Pav", "snack", 2, 2500, "Pune"),
    Sale("Chai", "beverage", 5, 1500, "Mumbai"),
    Sale("Samosa", "snack", 4, 2000, "Pune"),
    Sale("Filter Coffee", "beverage", 1, 3000, "Mumbai"),
    Sale("Vada Pav", "snack", 6, 2500, "Nagpur"),
    Sale("Chai", "beverage", 2, 1500, "Nagpur")
)

The questions

Each is one or two lines. Read each as "the answer is this expression".

Total revenue. Sum the quantity times price for every sale:

val totalPaise = sales.sumOf { it.qty * it.pricePaise }
println("Total revenue: ₹${totalPaise / 100}")        // Total revenue: ₹460

sumOf maps each sale to a number and adds them — no loop, no accumulator, no chance of an off-by-one. This is the aggregation pattern you will use most.

Revenue by category. Group the sales by category, then sum each group:

val byCategory = sales
    .groupBy { it.category }
    .mapValues { (_, list) -> list.sumOf { it.qty * it.pricePaise } / 100 }
println(byCategory)      // {beverage=180, snack=280}

groupBy { it.category } turns the flat list into a Map<String, List<Sale>> — beverages together, snacks together. Then mapValues transforms each group's value (the list of sales) into its total. The (_, list) destructures the entry, ignoring the key with _. This two-step — group, then aggregate each group — is one of the most useful patterns in the whole module, and it replaces a fiddly manual loop with a running map.

The best-selling item by units. Group by item, sum quantities, take the maximum:

val topItem = sales
    .groupBy { it.item }
    .mapValues { (_, list) -> list.sumOf { it.qty } }
    .maxByOrNull { it.value }
println("Top item: ${topItem?.key} (${topItem?.value})")   // Top item: Chai (10)

Chai sold 10 units across three cities. Note maxByOrNull returns a nullable entry (the map could be empty), so topItem?.key uses the safe call — the null-safety module, still earning its keep.

Distinct cities served. Map to the city, drop duplicates:

println(sales.map { it.city }.distinct())     // [Pune, Mumbai, Nagpur]

Beverages, as a readable list. Filter, then map to labels:

val bevs = sales.filter { it.category == "beverage" }.map { "${it.item} x${it.qty}" }
println(bevs)     // [Chai x3, Chai x5, Filter Coffee x1, Chai x2]

Units per city, busiest first. Group, sum, sort:

val unitsPerCity = sales.groupBy { it.city }.mapValues { (_, l) -> l.sumOf { it.qty } }
println(unitsPerCity.entries.sortedByDescending { it.value }.map { "${it.key}=${it.value}" })
// [Pune=9, Nagpur=8, Mumbai=6]

Every one of these answers a real question in a line or two, reads like the question, and has no mutable state to get wrong. That is the payoff of the whole module.

The nested-loop trap — a data-structure lesson

Now a performance point that matters in real code, and that the collections you choose decide. Suppose you have a list of "flagged" items and want the sales of any flagged item. The instinct is a nested scan:

val flaggedItems = listOf("Samosa", "Filter Coffee")

// for each sale, scan the whole flagged list — O(sales × flagged)
val flaggedSlow = sales.filter { s -> flaggedItems.any { it == s.item } }

For every sale, any scans the entire flaggedItems list. With 7 sales and 2 flagged items that is 14 comparisons — nothing. But this pattern is a nested loop, and its cost is the product of the two sizes. With 100,000 sales and 1,000 flagged items, it is 100 million comparisons, and your program crawls. A nested loop over two collections is the performance bug people actually meet in production, and it hides behind innocent-looking filter { list.any { } } or filter { list.contains() }.

The fix is a data-structure choice. Put the flagged items in a Set, whose membership test is instant regardless of size:

val flaggedSet = flaggedItems.toSet()          // build once
val flaggedFast = sales.filter { it.item in flaggedSet }   // O(1) membership per sale

Now each sale does one instant lookup instead of scanning a list. The cost drops from "sales × flagged" to "sales" — from 100 million to 100,000 in the large case. Same result, vastly less work. This is the lists-and-maps lesson made concrete: when you repeatedly ask "is this in that collection?", the collection you ask should be a Set (or a Map), not a List. Recognising this one pattern will save you from the most common collection performance mistake there is.

What this module gave you

You can now hold data in the right structure (List, Set, Map), choose read-only by default, transform it with map/filter/groupBy/sumOf instead of loops, reach for a sequence when a large chain short-circuits, and — crucially — recognise when a nested loop should be a Set lookup. That toolkit covers the overwhelming majority of the data-wrangling you will do in Kotlin, and it is exactly what the capstone's settlement logic is built from.

Check your work

The pattern for "revenue by category". groupBy to form groups, then mapValues with sumOf to aggregate each — group, then aggregate.

What groupBy { it.category } returns. A Map from category to the list of sales in it.

What mapValues { (_, list) -> ... } does. Transforms each group's value; _ ignores the key.

Why maxByOrNull returns a nullable. The collection could be empty — handle it with ?..

The nested-loop trap. filter { list.any { } } scans one collection for every element of another — cost is the product of the sizes, which explodes on large data.

The fix. Put the searched collection in a Set (or Map) and use in, turning each lookup from a scan into an instant test.

When to apply the fix. Whenever you repeatedly ask "is this present in that collection?".

Practice

  1. Recreate the sales list and compute the total revenue with sumOf. Confirm ₹460.
  2. Compute revenue by category with groupBy + mapValues. Confirm {beverage=180, snack=280}.
  3. Find the best-selling item by units. Confirm Chai (10).
  4. List the distinct cities, and the beverages as "item xqty" labels.
  5. Produce units-per-city sorted busiest-first.
  6. Add three more sales of your own and re-run every query. Confirm the numbers update sensibly.
  7. Write the flagged-items query the slow way (filter { list.any { } }) and the fast way (Set + in). Confirm they agree.
  8. Reason about the comparison count for each version if there were 100,000 sales and 1,000 flagged items. Write down the two numbers.
  9. Answer one new question about the data that the lesson did not — for example, "the average quantity per sale" or "revenue per city" — using only collection operations.

Official documentation

Next module — Classes and Objects: modelling your domain with Kotlin's class system.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship