Kotlin
Completed
-
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
90: Regex in Kotlin
A few years ago, I was helping a junior dev who had written about forty lines of split() and substringIndexOf() calls just to extract a user ID and a session token from a legacy API response string. It looked like a mess of magic numbers and index offsets. The moment the API team added a single space to the output, the whole parser crashed with a StringIndexOutOfBoundsException. He spent an entire afternoon chasing a bug that could have been solved in two lines of code. That's the danger of manual string slicing; it's fragile. This is exactly why you need to get comfortable with Regular Expressions (Regex).
Matching Patterns with the Regex Class
In Kotlin, the Regex class is your primary tool. While you can use the matches() function for a simple true/false check, you'll more often find yourself using containsMatchIn() or find(). The key is to remember that Kotlin provides a very clean way to instantiate these patterns using the Regex constructor or the toRegex() extension function.
Let's look at a real-world example. Suppose you're validating a specific internal product code that must start with two uppercase letters, followed by a hyphen, and end with four digits (e.g., "TS-1234").
val productCodePattern = "[A-Z]{2}-\\d{4}".toRegex()
val input = "The item is TS-1234"
if (productCodePattern.containsMatchIn(input)) {
println("Found a valid product code!")
}
One thing to keep in mind: because backslashes are escape characters in Kotlin strings, you have to double them (\\d instead of \d). If that bothers you, you can use a raw string—the ones wrapped in triple quotes """—which lets you use single backslashes. I personally always use raw strings for Regex because it makes the pattern much easier to read.
Extracting Data with Groups and Destructuring
Matching is fine, but extracting the actual data is where Regex really earns its keep. Kotlin makes this surprisingly elegant by allowing you to destructure the results of a match. Instead of manually calling groupValues[1], groupValues[2], and so on, you can map the match directly into variables.
Imagine we are parsing a log line like [2023-10-12] ERROR: Connection timed out. We want the date and the error message, but we don't care about the brackets or the "ERROR" label.
val logPattern = """\[(\d{4}-\d{2}-\d{2})] \w+: (.+)""".toRegex()
val logLine = "[2023-10-12] ERROR: Connection timed out"
val matchResult = logPattern.find(logLine)
if (matchResult != null) {
// Destructuring the group values
val (date, message) = matchResult.destructured
println("Date: $date, Message: $message")
}
The destructured property is a lifesaver here. It creates a sequence of the captured groups (the parts in parentheses), allowing you to assign them to variables in one go. Just be careful: if your regex doesn't match, find() returns null, so always wrap this in a null check or use ?.let { ... } to avoid the dreaded NullPointerException.
Cleaning Strings with Replace
Finally, you'll often use Regex not to find things, but to get rid of them. The replace() function in Kotlin can take a Regex object as its first argument. This is incredibly powerful for sanitizing user input or stripping out unwanted characters from a scraped web page.
For instance, if you have a string with inconsistent whitespace—tabs, multiple spaces, and newlines—and you want to collapse it all into single spaces, you can do this:
val messyString = "This is a \t very \n messy string."
val cleanString = messyString.replace("""\s+""".toRegex(), " ")
println(cleanString) // "This is a very messy string."
It's a simple operation, but it replaces what would otherwise be a loop of character checks and conditional logic.
📋 Practical Task
The Log Entry Parser Utility
You are tasked with building a utility that processes a list of system logs. Each log entry follows this format: USER_ID: [LEVEL] MESSAGE (e.g., "user123: [INFO] User logged in" or "admin_01: [WARN] Disk space low").
Create a data class called LogEntry with three properties: userId (String), level (String), and message (String). Then, write a function parseLogs(logs: List<String>): List<LogEntry> that uses a single Regex with capturing groups to extract these three components from each line. If a line doesn't match the pattern, it should be ignored.
Requirements:
- Use a raw string (triple quotes) for your Regex pattern.
- Use the
destructuredproperty to assign values to yourLogEntryobject. - Test your function with at least one valid entry and one malformed entry to ensure your code doesn't crash.
There are no comments for now.