Kotlin Collection Aggregation
When working with collections in Kotlin, you'll often need to process multiple elements to produce a single result. This is where collection aggregation operations come in handy. These operations allow you to calculate sums, averages, find minimum and maximum values, or apply custom aggregation logic.
What is Collection Aggregation?
Collection aggregation refers to the process of taking multiple values from a collection and combining them into a single result value. Kotlin's standard library provides numerous functions to perform common aggregation operations efficiently and with minimal code.
Basic Aggregation Functions
Count
The count()
function returns the number of elements in a collection or the number of elements matching a given predicate.
val numbers = listOf(1, 2, 3, 4, 5, 6)
val totalCount = numbers.count()
val evenCount = numbers.count { it % 2 == 0 }
println("Total count: $totalCount") // Output: Total count: 6
println("Even numbers count: $evenCount") // Output: Even numbers count: 3
Sum
For collections of numeric types, Kotlin provides sum()
and sumOf()
functions:
val numbers = listOf(1, 2, 3, 4, 5)
val sum = numbers.sum() // 15
// Using sumOf with a selector function
val squares = numbers.sumOf { it * it } // 1 + 4 + 9 + 16 + 25 = 55
println("Sum: $sum") // Output: Sum: 15
println("Sum of squares: $squares") // Output: Sum of squares: 55
Average
The average()
function calculates the arithmetic mean of numeric collections:
val numbers = listOf(1, 2, 3, 4, 5)
val average = numbers.average()
println("Average: $average") // Output: Average: 3.0
Min and Max
Find the minimum or maximum values in a collection:
val numbers = listOf(5, 2, 10, 4)
val min = numbers.minOrNull()
val max = numbers.maxOrNull()
println("Minimum: $min") // Output: Minimum: 2
println("Maximum: $max") // Output: Maximum: 10
You can also use minByOrNull
and maxByOrNull
with custom selectors:
data class Product(val name: String, val price: Double)
val products = listOf(
Product("Laptop", 999.99),
Product("Phone", 699.99),
Product("Tablet", 399.99)
)
val cheapestProduct = products.minByOrNull { it.price }
val mostExpensiveProduct = products.maxByOrNull { it.price }
println("Cheapest product: ${cheapestProduct?.name}") // Output: Cheapest product: Tablet
println("Most expensive product: ${mostExpensiveProduct?.name}") // Output: Most expensive product: Laptop
Advanced Aggregation Functions
Fold and Reduce
Fold and reduce operations are powerful tools for aggregating collections with custom logic.
Fold
fold()
takes an initial value and a combining function. It then accumulates value starting with the initial value and applying the combining function to the current accumulator value and each element.
val numbers = listOf(1, 2, 3, 4, 5)
val sum = numbers.fold(0) { acc, number -> acc + number }
// Same as sum = 0 + 1 + 2 + 3 + 4 + 5
println("Sum using fold: $sum") // Output: Sum using fold: 15
// Creating a concatenated string with a separator
val names = listOf("Alice", "Bob", "Charlie")
val nameString = names.fold("Names: ") { acc, name -> "$acc$name, " }.dropLast(2)
println(nameString) // Output: Names: Alice, Bob, Charlie
There's also foldRight()
which iterates from right to left:
val chars = listOf('a', 'b', 'c', 'd')
val reverseString = chars.foldRight("") { char, acc -> acc + char }
println("Reversed: $reverseString") // Output: Reversed: dcba
Reduce
reduce()
is similar to fold()
but uses the first element as the initial accumulator value:
val numbers = listOf(1, 2, 3, 4, 5)
val product = numbers.reduce { acc, number -> acc * number }
// Same as product = 1 * 2 * 3 * 4 * 5
println("Product using reduce: $product") // Output: Product using reduce: 120
Like with fold, there's also reduceRight()
for right-to-left iteration:
val numbers = listOf(1, 2, 3, 4, 5)
val resultRightToLeft = numbers.reduceRight { number, acc -> number - acc }
println("Result of right-to-left reduction: $resultRightToLeft")
// Calculates: 1 - (2 - (3 - (4 - 5)))
// = 1 - (2 - (3 - (-1)))
// = 1 - (2 - 4)
// = 1 - (-2)
// = 3
Note: Be careful with reduce()
on empty collections as it will throw an exception. Use foldOrNull()
for potentially empty collections.
Aggregating to Collections
Sometimes you want to aggregate values but still maintain a collection structure.
GroupBy
The groupBy()
function can be used to categorize elements into groups:
data class Student(val name: String, val grade: Char)
val students = listOf(
Student("Alice", 'A'),
Student("Bob", 'B'),
Student("Carol", 'A'),
Student("Dave", 'C'),
Student("Eve", 'B')
)
val studentsByGrade = students.groupBy { it.grade }
println("Students with grade A: ${studentsByGrade['A']?.map { it.name }}")
println("Students with grade B: ${studentsByGrade['B']?.map { it.name }}")
println("Students with grade C: ${studentsByGrade['C']?.map { it.name }}")
// Output:
// Students with grade A: [Alice, Carol]
// Students with grade B: [Bob, Eve]
// Students with grade C: [Dave]
Partition
The partition()
function divides a collection into two parts based on a predicate:
val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
val (evens, odds) = numbers.partition { it % 2 == 0 }
println("Even numbers: $evens") // Output: Even numbers: [2, 4, 6, 8, 10]
println("Odd numbers: $odds") // Output: Odd numbers: [1, 3, 5, 7, 9]
Real-World Examples
Example 1: Sales Analytics
Let's create a simple sales analytics application:
data class SalesTransaction(
val productId: String,
val amount: Double,
val category: String,
val date: String // Format: YYYY-MM-DD for simplicity
)
val transactions = listOf(
SalesTransaction("P001", 125.99, "Electronics", "2023-01-15"),
SalesTransaction("P002", 89.50, "Clothing", "2023-01-20"),
SalesTransaction("P003", 210.75, "Electronics", "2023-01-25"),
SalesTransaction("P004", 45.25, "Groceries", "2023-02-01"),
SalesTransaction("P005", 199.99, "Electronics", "2023-02-05"),
SalesTransaction("P006", 65.00, "Clothing", "2023-02-10")
)
// Total revenue
val totalRevenue = transactions.sumOf { it.amount }
// Revenue by category
val revenueByCategory = transactions.groupBy { it.category }
.mapValues { (_, transactions) -> transactions.sumOf { it.amount } }
// Monthly revenue
val monthlyRevenue = transactions.groupBy { it.date.substring(0, 7) }
.mapValues { (_, transactions) -> transactions.sumOf { it.amount } }
// Average transaction value
val averageTransaction = transactions.map { it.amount }.average()
println("Total Revenue: $${String.format("%.2f", totalRevenue)}")
println("Revenue by Category:")
revenueByCategory.forEach { (category, revenue) ->
println(" - $category: $${String.format("%.2f", revenue)}")
}
println("Monthly Revenue:")
monthlyRevenue.forEach { (month, revenue) ->
println(" - $month: $${String.format("%.2f", revenue)}")
}
println("Average Transaction Value: $${String.format("%.2f", averageTransaction)}")
// Output:
// Total Revenue: $736.48
// Revenue by Category:
// - Electronics: $536.73
// - Clothing: $154.50
// - Groceries: $45.25
// Monthly Revenue:
// - 2023-01: $426.24
// - 2023-02: $310.24
// Average Transaction Value: $122.75
Example 2: Student Grade Calculator
Here's how you might calculate grades for a class:
data class Student(val name: String, val scores: List<Int>)
val students = listOf(
Student("Alice", listOf(95, 87, 92, 88, 90)),
Student("Bob", listOf(72, 65, 81, 77, 70)),
Student("Charlie", listOf(86, 88, 84, 90, 85)),
Student("Diana", listOf(92, 95, 88, 97, 91))
)
// Calculate average score for each student
val studentAverages = students.map { student ->
val average = student.scores.average()
"${student.name}: ${String.format("%.1f", average)}"
}
// Find the highest scoring student
val topStudent = students.maxByOrNull { it.scores.average() }
// Calculate class average
val allScores = students.flatMap { it.scores }
val classAverage = allScores.average()
// Grade distribution (A: 90-100, B: 80-89, C: 70-79, D: 60-69, F: below 60)
val scoreToGrade = { score: Int ->
when(score) {
in 90..100 -> "A"
in 80..89 -> "B"
in 70..79 -> "C"
in 60..69 -> "D"
else -> "F"
}
}
val gradeDistribution = allScores.groupBy(scoreToGrade)
.mapValues { it.value.size }
println("Student Averages:")
studentAverages.forEach { println(" $it") }
println("Top Student: ${topStudent?.name}")
println("Class Average: ${String.format("%.1f", classAverage)}")
println("Grade Distribution:")
gradeDistribution.forEach { (grade, count) ->
println(" $grade: $count")
}
// Output:
// Student Averages:
// Alice: 90.4
// Bob: 73.0
// Charlie: 86.6
// Diana: 92.6
// Top Student: Diana
// Class Average: 85.7
// Grade Distribution:
// A: 8
// B: 7
// C: 5
// D: 0
// F: 0
Summary
Kotlin's collection aggregation functions offer powerful ways to process and analyze data in collections:
- Basic aggregation:
count()
,sum()
,average()
,min()
, andmax()
- Advanced aggregation:
fold()
andreduce()
for custom aggregation logic - Collection aggregation:
groupBy()
andpartition()
to create new collections based on criteria
These functions enable you to write concise, readable code that focuses on what you want to achieve rather than how to implement the logic.
Exercises
To reinforce your understanding, try these exercises:
- Create a list of integers and calculate the sum of all even numbers squared.
- Given a list of words, find the longest word.
- Create a function that takes a list of integers and returns the product of all elements.
- Given a list of people with names and ages, calculate the average age by decade (20s, 30s, etc.).
- Create a shopping cart system that can calculate the total cost, apply discounts, and group items by category.
Additional Resources
- Official Kotlin Documentation on Collections
- Kotlin Playground - Test your aggregation code online
- Kotlin's Collection API Reference
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)