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
1
ton
by 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 MutableList
s 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 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(): 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
- Prefer Immutability: Use
groupBy
instead of manual mutable collections. - Leverage Kotlin’s Stdlib:
sumOf
,maxOf
, andgroupBy
simplify common tasks. - Extension Functions: Make APIs more intuitive.
- 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
!