🚗 · Grouping

4 min read · Updated on by

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 groupBygroupingBy 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.

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

The Kotlin Primer