From Beginner to Idiomatic Kotlin: Refactoring a Group Counting Solution

In this article, we’ll refactor a solution to the “Count Largest Group” problem, comparing a beginner’s approach with a more idiomatic Kotlin version.

Problem Statement

Given an integer n, we need to:

  1. Group numbers from 1 to n by the sum of their digits
  2. Return how many groups have the largest size

Example:
For n = 13, the digit-sum groups are:
[1,10] (sum=1), [2,11] (sum=2), ..., [9] (sum=9)
The largest groups have 2 elements, and there are 4 such groups → Return 4.


Original Solution (Beginner Approach)

class Solution {
    fun countLargestGroup(n: Int): Int {
        val groups = mutableListOf<MutableList<Int>>()
        for (x in 1..n) {
            if(x <= 9){
                groups.add(mutableListOf<Int>(x))
            } else if(sumDigits(x)-1 == groups.count()) {
                groups.add(mutableListOf<Int>(x))
            } else {
                val innerList = groups[(sumDigits(x)-1)]
                innerList.add(x)
            }
        }
        return countLargestGroup(groups)
    }
 
    fun countLargestGroup(groups: MutableList<MutableList<Int>>): Int {
         val totCount = groups.map { it.count() }
         val max = totCount.max()
         return totCount.filter { it == max}.count()
    }
    
    fun sumDigits(n: Int): Int {
        return n
        .toString()
        .split("")
        .filter { it.isNotEmpty() }
        .map { it.toInt() }
        .sum()
    }
}

Refactored Solution (Idiomatic Kotlin)

class Solution {
    fun countLargestGroup(n: Int): Int {
        return (1..n)
            .groupBy { it.digitSum() }
            .let { groups ->
                val maxSize = groups.maxOf { it.value.size }
                groups.count { it.value.size == maxSize }
            }
    }
 
    private fun Int.digitSum(): Int = 
        this.toString().sumOf { it - '0' }
}

Step-by-Step Refactoring

1. Replacing Manual Group Tracking with groupBy

Original:
Manages groups using nested MutableLists and checks indices.

val groups = mutableListOf<MutableList<Int>>()
// ...complex logic to add numbers to the right group...

Improved:
Uses Kotlin’s built-in groupBy, which automatically creates a map (e.g., {1=[1,10], 2=[2,11], ...}).

(1..n).groupBy { it.digitSum() }

Why Better?

  • No manual index calculations
  • Immutable: Avoids bugs from shared mutable state

2. Simplifying Digit Sum Calculation

Original:
Splits strings and filters empty values.

n.toString()
  .split("")
  .filter { it.isNotEmpty() }
  .map { it.toInt() }
  .sum()

Improved:
Uses sumOf with char arithmetic:

this.toString().sumOf { it - '0' }

Why Better?

  • sumOf is designed for this purpose
  • it - '0' converts a digit char ('3') to its integer value (3) efficiently

3. Using Extension Functions

Original:
Standalone function:

fun sumDigits(n: Int): Int { ... }

Improved:
Extension on Int:

private fun Int.digitSum(): Int

Why Better?

  • More natural usage: 13.digitSum() vs. sumDigits(13)
  • Discoverable via IDE autocomplete

4. Functional Chains with let

Original:
Stores intermediate results in variables:

val totCount = groups.map { it.count() }
val max = totCount.max()
return totCount.filter { it == max }.count()

Improved:
Uses let to scope operations:

.let { groups ->
    val maxSize = groups.maxOf { it.value.size }
    groups.count { it.value.size == maxSize }
}

Why Better?

  • Avoids temporary variables
  • Clearly separates grouping from analysis

5. Using maxOf for Direct Calculation

Original:
Separate map and max calls:

groups.map { it.count() }.max()

Improved:
Directly computes maximum size:

groups.maxOf { it.value.size }

Why Better?

  • More efficient (single pass)
  • Reads like English: “max of group sizes”

Key Takeaways

  1. Prefer Immutability: Use groupBy instead of manual mutable collections.
  2. Leverage Kotlin’s Stdlib: sumOf, maxOf, and groupBy simplify common tasks.
  3. Extension Functions: Make APIs more intuitive.
  4. Functional Style: Chain operations with let for cleaner code.

The refactored version is shorter (7 vs. 20 lines), more readable, and less error-prone—showcasing Kotlin’s power!

Next Steps: Try rewriting other solutions using associateWith, fold, or withIndex!