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:
- Group numbers from
1tonby the sum of their digits - 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?
sumOfis designed for this purposeit - '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(): IntWhy 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
- Prefer Immutability: Use
groupByinstead of manual mutable collections. - Leverage Kotlin’s Stdlib:
sumOf,maxOf, andgroupBysimplify common tasks. - Extension Functions: Make APIs more intuitive.
- Functional Style: Chain operations with
letfor 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!