-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Null Safety
-
Section 4: Object-Oriented Kotlin
-
Section 5: Functional Kotlin
-
Section 6: Coroutines
-
Section 7: Collections Deep Dive
-
Section 8: Type System Deep Dive
-
Section 9: Interop and Tooling
-
Section 10: Kotlin DSLs and Patterns
-
Section 11: Testing and Quality
-
Section 12: Server-Side Kotlin
-
Section 13: Practical Projects
-
Section 14: Interview Practice
-
Section 15: More Practice Exercises
-
Section 16: More Standard Library
-
Section 17: Multiplatform Kotlin
-
Section 18: kotlin.collections In Depth
-
Section 19: kotlin.text In Depth
-
Section 20: kotlin.ranges and kotlin.sequences
-
Section 21: kotlin.io and File Handling
-
Section 22: kotlinx.coroutines Deep Dive
-
Section 23: kotlin.reflect
-
Section 24: Android Development with Kotlin Overview
-
Section 25: Kotlin Multiplatform Deep Dive
-
Section 26: Kotlin for Backend Deep Dive
-
Section 27: Kotlin Design Patterns
-
Section 28: Advanced Language Features
-
Section 29: More Practice Exercises
-
Section 30: More Interview Practice
-
Section 31: Kotlin Type System Deep Dive
-
Section 32: Kotlin Null Safety Advanced
-
Section 33: Kotlin Testing Deep Dive
-
Section 34: Kotlin Build Tooling Deep Dive
-
Section 35: Kotlin Serialization
-
Section 36: Kotlin Performance Considerations
-
Section 37: Kotlin Native Overview
-
Section 38: Kotlin for Data and Scripting
-
Section 39: More Coroutines Practice
-
Section 40: More Android-Adjacent Patterns
-
Section 41: More Practical Projects
-
Section 42: More Design and Architecture Practice
-
Section 43: Kotlin Language Evolution
-
Section 44: More Interview and Review
-
Section 45: Kotlin Delegation Patterns Deep Dive
-
Section 46: Kotlin Annotations Deep Dive
-
Section 47: Kotlin for Gradle Plugin Development
-
Section 48: Kotlin Concurrency Beyond Coroutines
-
Section 49: Kotlin Compiler Internals
-
Section 50: Real-World Kotlin Case Studies
-
Section 51: Final Practice Projects
-
Section 52: Kotlin for Server-Side Reactive Programming
-
Section 53: More Practice and Drills
-
Section 54: Kotlin Security Practices
89: String Extension Functions: trim, padStart, chunked
I've seen this a lot when developers are building internal tools or data importers: you're trying to format a raw string into a specific pattern—like a serial number or a product SKU—and you end up writing a mess of while loops and substring calls that eventually crash with a StringIndexOutOfBoundsException.
Take a look at this snippet. This is a classic "over-engineered" approach to formatting a user-provided ID to be exactly 10 digits, padded with zeros, and then split into groups of three for readability.
fun formatId(rawId: String): String {
// Manually removing whitespace
var cleaned = ""
for (char in rawId) {
if (char != ' ') cleaned += char
}
// Manually padding with zeros to reach 10 characters
while (cleaned.length < 10) {
cleaned = "0" + cleaned
}
// Manually chunking into groups of 3
val result = cleaned.substring(0, 3) + "-" +
cleaned.substring(3, 6) + "-" +
cleaned.substring(6, 9) + "-" +
cleaned.substring(9, 10)
return result
}
// This works... until it doesn't.
println(formatId(" 12345 ")) // 0000012345 -> 000-001-234-5
The IndexOutOfBounds Trap
The code above looks fine until you realize how fragile it is. If the input rawId happens to be longer than 10 characters after cleaning, the padding loop is skipped, but the substring calls are hardcoded. If you change the required length to 11, you have to rewrite every single index in that final string concatenation. It's a maintenance nightmare and a bug waiting to happen.
Cleaning and Padding the Right Way
Kotlin gives us extension functions that handle these common tasks in a single line. First, instead of looping through the string to remove spaces, use trim(). It handles leading and trailing whitespace instantly. Then, instead of a while loop, use padStart().
padStart(length, char) says: "If this string is shorter than length, add char to the beginning until it reaches that length. If it's already longer, just leave it alone."
val rawId = " 12345 "
val cleaned = rawId.trim().padStart(10, '0')
// Result: "0000012345"
Breaking Strings Apart with chunked
Now, for the part where we split the string. Instead of calculating indices for substring, we use chunked(). This function splits a string into a list of strings, each with the maximum length you specify.
Here is how I would rewrite that entire formatId function using these extensions. It's cleaner, safer, and much easier to read.
fun formatId(rawId: String): String {
return rawId.trim()
.padStart(10, '0')
.chunked(3)
.joinToString("-")
}
println(formatId(" 12345 ")) // 000-001-234-5
I love chunked because it doesn't care if the total length is perfectly divisible by the chunk size. In the example above, the last chunk is just "5", and Kotlin handles that automatically without throwing an exception. By chaining trim(), padStart(), and chunked(), we've turned a fragile 15-line function into a robust 4-line pipeline.
📋 Practical Task
Build a Credit Card Masking Formatter
You are building a payment processing UI. You receive a raw string of numbers from a user that may contain accidental leading or trailing spaces. You need to process this string to meet the following requirements:
- Remove any leading or trailing whitespace.
- Ensure the string is exactly 16 characters long by padding the start with '0' if the user entered too few digits.
- Split the 16-digit string into chunks of 4.
- Join those chunks together with a space in between.
Write a function formatCardNumber(input: String): String that implements this logic and test it with the input " 54321 ". The expected output should be "0000 0000 0005 4321".
There are no comments for now.