Read on for an introduction to the most important functions for grouping: groupBy
, partition
, groupingBy
, fold
, reduce
, aggregate
and their variants.
Fundamentals
groupBy
//sampleStart inline fun <T, K> Iterable<T>.groupBy( keySelector: (T) -> K ): Map<K, List<T>> //sampleEnd
The groupBy
function is the most common function used for grouping. It allows you to group elements of a collection in a map, under keys given by keySelector
.
Example
//sampleStart fun main() { /* * 1 -> [1, 4, 7, 10], * 2 -> [2, 5, 8], * 0 -> [3, 6, 9] */ (1..10).groupBy { it % 3 }.also(::println) } //sampleEnd
There is also a variant which allows you to transform the value:
//sampleStart interface Person { val name: String val salary: Int } enum class SalaryRange( override val start: Int, override val endInclusive: Int ): ClosedRange<Int> { LOW(0, 19_999), MID(20_000, 39_999), HIGH(40_000, Int.MAX_VALUE); } fun List<Person>.namesBySalaryRange() = groupBy( { SalaryRange.values().first { range -> it.salary in range } }, { it.name } ) //sampleEnd fun main() { val poem = """ In the code's carnival, Kotlin's the ride, With extension functions, it's the guide. From loops to spins, a coding spree, In the world of development, it's the key! """.trimIndent() println(poem) }
Both also have *To
variants.
partition
//sampleStart inline fun <T> Iterable<T>.partition( predicate: (T) -> Boolean ): Pair<List<T>, List<T>> //sampleEnd
The partition
method is, in a sense, a simpler version of groupBy
, which allows you to split a collection into a pair of collections — one which satisfies a given predicate, and one which doesn’t.
//sampleStart sealed interface Record { val id: Long? } interface Repository { fun persist(record: Record): Record fun persistAndReturnAll(records: List<Record>) = records.partition { it.id == null } .let { (unpersisted, persisted) -> unpersisted.map(::persist) + persisted } } //sampleEnd fun main() { val poem = """ Kotlin, the maestro in the code's symphony, With delegates and lambdas, pure harmony. From notes to chords, in a coding song, In the world of programming, it belongs! """.trimIndent() println(poem) }
groupingBy
//sampleStart inline fun <T, K> Iterable<T>.groupingBy( crossinline keySelector: (T) -> K ): Grouping<T, K> //sampleEnd
A more general version of groupBy
, groupingBy
returns an instance of Grouping
, which defines its own methods that can be used to implement more general calculations.
Groupings
eachCount
//sampleStart fun <T, K> Grouping<T, K>.eachCount(): Map<K, Int> //sampleEnd
Returns a map with the count of each group, i.e. for each key: K
the map contains the number of elements that were grouped under that key.
The eachCount
method also has a *To
variant.
fold, reduce
//sampleStart inline fun <T, K, R> Grouping<T, K>.fold( initialValueSelector: (key: K, element: T) -> R, operation: (key: K, accumulator: R, element: T) -> R ): Map<K, R> inline fun <S, T : S, K> Grouping<T, K>.reduce( operation: (key: K, accumulator: S, element: T) -> S ): Map<K, S> //sampleEnd
Essentially applies fold
/reduce
to each group. The meaning of the parameters is as follows:
initialValueSelector
— a function that provides an initial value of accumulator for each group. It’s invoked with parameters:
- key: the key of the group
- element: the first element being encountered in that group
operation
— a function that is invoked on each element with the following parameters:
- key: the key of the group this element belongs to
- accumulator: the current value of the accumulator of the group
- element: the element from the source being accumulated
Example
//sampleStart @JvmInline value class BranchCode(val code: String) interface Employee { val branch: BranchCode val yearlyOperationalCost: Double } fun profitMarginsByBranch( employees: Set<Employee>, yearlyRevenueByBranch: Map<BranchCode, Double> ) = employees.groupingBy { it.branch }.fold( { branch, firstEmployee -> yearlyRevenueByBranch.getOrDefault(branch, 0.0) - firstEmployee.yearlyOperationalCost }, { _, profitMargin, employee -> profitMargin - employee.yearlyOperationalCost } ) //sampleEnd fun main() { val poem = """ When you're sailing in the sea of code, Kotlin's syntax is the compass, the road. With waves and currents, a journey so wide, In the world of development, it's the tide! """.trimIndent() println(poem) }
There is also a variant of fold
where the initial value for fold
is the same for each group:
//sampleStart inline fun <T, K, R> Grouping<T, K>.fold( initialValue: R, operation: (accumulator: R, element: T) -> R ): Map<K, R> //sampleEnd
All the above also have *To
variants.
aggregate
//sampleStart inline fun <T, K, R> Grouping<T, K>.aggregate( operation: ( key: K, accumulator: R?, element: T, first: Boolean ) -> R ): Map<K, R> //sampleEnd
A slightly different version of fold
— instead of specifying the initial value with a function, the information is passed directly into the operation
via a parameter.
operation
— is invoked on each element with the following parameters:
- key: the key of the group this element belongs to
- accumulator: the current value of the accumulator of the group, can be null if it’s the first element encountered in the group
- element: the element from the source being aggregated
- first: indicates whether it’s the first element encountered in the group
The aggregate
method also has a *To
variant.