kotlin.text

Searching, splitting, replacing, trimming, padding, buildString, raw strings, and Regex.

Searching split / join replace trim / pad buildString Regex
๐Ÿ”

Searching

Kotlin's String is the same JVM String underneath, but every search operation here is an extension function rather than a Java-style method, and each one has a case-insensitive overload built in โ€” no separate equalsIgnoreCase-style method to remember for every operation.

contains, startsWith, endsWith, indexOf Search
val text = "The Quick Brown Fox"

println(text.contains("Quick"))                    // true
println(text.contains("quick", ignoreCase = true))  // true

println(text.startsWith("The"))   // true
println(text.endsWith("Fox"))     // true

println(text.indexOf("Brown"))  // 10: character index, or -1 if not found
println(text.indexOf("missing")) // -1

// The in operator reads naturally for membership checks
println("Quick" in text)  // true: same as text.contains("Quick")
โœ‚๏ธ

Splitting & Joining

split takes a delimiter (or several) and returns a List<String> directly โ€” no need to convert an array to a list afterward, unlike Java's String.split. joinToString is the reverse operation, and it's genuinely more capable than a simple delimiter-glue function: it accepts a prefix, a suffix, and a per-element transform in the same call.

split: string to list split
val csv = "Ada,Grace,Alan"

println(csv.split(","))            // [Ada, Grace, Alan]
println(csv.split(",", limit = 2)) // [Ada, Grace,Alan]: stop after 2 parts

// Multiple delimiters in one call
println("a, b; c".split(", ", "; "))  // [a, b, c]

// lines(): split on line breaks, handling \n and \r\n uniformly
val multiline = "line1\nline2\nline3"
println(multiline.lines())  // [line1, line2, line3]
joinToString: list to string, with control joinToString
val names = listOf("Ada", "Grace", "Alan")

println(names.joinToString())              // "Ada, Grace, Alan": default separator ", "
println(names.joinToString(" | "))       // "Ada | Grace | Alan"

// prefix, postfix, and a per-element transform, all in one call
val html = names.joinToString(
    separator = ", ",
    prefix = "<ul>",
    postfix = "</ul>"
) { "<li>$it</li>" }
println(html)
๐Ÿ”

Replacing

Plain replace works against a literal string or a Char; a separate Regex overload of the same function name handles pattern-based replacement (covered below). For the common case of stripping a known prefix or suffix, dedicated functions are clearer than a general-purpose replace.

replace, removePrefix, removeSuffix replace
val text = "the cat sat on the mat"

println(text.replace("the", "a"))    // "a cat sat on a mat": replaces ALL occurrences
println(text.replaceFirst("the", "a")) // "a cat sat on the mat": first occurrence only

val fileName = "report_final.pdf"
println(fileName.removeSuffix(".pdf"))    // "report_final"
println(fileName.removePrefix("report_")) // "final.pdf"

// removePrefix/removeSuffix are no-ops if the string doesn't
// actually start/end with the given text โ€” no exception thrown
println("hello".removeSuffix(".pdf"))  // "hello": unchanged
๐Ÿ“

Trimming & Padding

Whitespace handling splits into two families: trimming removes characters from the ends of a string, while trimIndent/trimMargin deal with the shared leading whitespace of a multi-line raw string block (see raw strings below). Padding does the opposite of trimming โ€” growing a string to a target length, most often for aligned console output.

trim, trimStart, trimEnd trim
val padded = "   hello   "

println("[${padded.trim()}]")       // "[hello]"
println("[${padded.trimStart()}]")  // "[hello   ]"
println("[${padded.trimEnd()}]")    // "[   hello]"

// trim accepts specific characters to strip, not just whitespace
println("###hello###".trim('#'))  // "hello"
padStart, padEnd pad
val id = "42"
println(id.padStart(6, '0'))  // "000042"

// Common use: aligning a printed table's columns
val rows = listOf("Ada" to 36, "Grace" to 85)
rows.forEach { (name, age) ->
    println(name.padEnd(10) + age.toString().padStart(3))
}
// Ada        36
// Grace      85
๐Ÿงฑ

buildString

Building a string piece by piece with repeated += allocates a new String on every append, since strings are immutable (see Performance). buildString gives you a scoped StringBuilder receiver โ€” using the trailing-lambda-with-receiver pattern from Functions & Lambdas โ€” so every call inside the block mutates one shared buffer, with a single final String allocated at the end.

Building incrementally buildString
val report = buildString {
    append("Report\n")
    append("------\n")
    for (i in 1..3) {
        append("Line $i\n")
    }
    appendLine("Done.")   // append + a trailing newline
}
println(report)
๐Ÿ“œ

Raw Strings

A triple-quoted raw string treats backslashes literally โ€” no escaping needed for backslashes, quotes, or newlines โ€” which is why Basics introduces it for JSON literals and regex patterns. The remaining piece is indentation control: trimIndent strips the smallest common leading whitespace across every line, while trimMargin strips everything up to an explicit marker character you place at the start of each line, giving you more control when the block's natural indentation doesn't line up cleanly.

trimIndent vs trimMargin Indentation control
// trimIndent: strips the common minimum leading whitespace
val code = """
    fun main() {
        println("hi")
    }
""".trimIndent()
// "fun main() {\n    println(\"hi\")\n}" โ€” the outer 4-space indent is gone,
// but the inner 4-space indent (relative) is preserved

// trimMargin: an explicit marker (default "|") anchors each line
val message = """
    |Dear Ada,
    |Thanks for your order.
    |Regards, Support
""".trimMargin()
println(message)
// Dear Ada,
// Thanks for your order.
// Regards, Support
๐Ÿ’ก
Reach for trimIndent by default โ€” it needs no marker characters cluttering the literal. Switch to trimMargin specifically when the text itself might start a line with meaningful leading whitespace that trimIndent's "smallest common indent" heuristic would otherwise misjudge.
๐ŸŽฏ

Regex

Kotlin's Regex wraps java.util.regex.Pattern with a friendlier API. Construct one with the Regex(...) constructor or the .toRegex() extension on a string โ€” a raw string literal is the natural companion here, since regex patterns are already full of backslashes that would otherwise all need escaping.

matches, find, and pattern-based replace Regex
val phonePattern = """\d{3}-\d{4}""".toRegex()

println(phonePattern.matches("555-1234"))  // true: entire string must match
println(phonePattern.containsMatchIn("call 555-1234 now"))  // true: match anywhere

// find(): returns the first MatchResult, or null
val match = phonePattern.find("call 555-1234 or 555-9999")
println(match?.value)  // "555-1234": the first match only

// findAll(): every match in the string
val allMatches = phonePattern.findAll("call 555-1234 or 555-9999")
println(allMatches.map { it.value }.toList())  // [555-1234, 555-9999]

// Capture groups
val datePattern = """(\d{4})-(\d{2})-(\d{2})""".toRegex()
val (year, month, day) = datePattern.find("2024-03-15")!!.destructured
println("$year/$month/$day")  // "2024/03/15"

// Regex-based replace, using the string's own replace overload
val masked = "call 555-1234 now".replace(phonePattern, "XXX-XXXX")
println(masked)  // "call XXX-XXXX now"
๐Ÿ’ก
The .destructured property on a MatchResult plugs directly into destructuring declarations (see Classes & Objects), turning capture groups into named variables in one line instead of indexing into match.groupValues[1], [2], [3] by hand.
๐Ÿ“‹

Quick Reference

Function Signature Shape Notes
Substring checks.contains(x, ignoreCase = true)Every search function has an ignoreCase overload
Position lookups.indexOf(x)Returns -1 if not found, not an exception
Split to lists.split(delim)Returns List<String> directly
Join with controllist.joinToString(sep, prefix, postfix) { }Per-element transform in the same call
Replace alls.replace(old, new)Overloaded for String, Char, or Regex
Strip known prefix/suffixs.removePrefix(p) / removeSuffix(p)No-op (not an error) if it doesn't match
Trim whitespaces.trim() / trimStart() / trimEnd()Accepts specific chars to strip instead
Pad to widths.padStart(n, char) / padEnd(n, char)Common for aligned console output
Efficient concatenationbuildString { append(...) }One StringBuilder, one final allocation
Multi-line, no escaping"""text"""Backslashes and quotes are literal
Strip common indent"""...""".trimIndent()Uses the smallest leading whitespace across lines
Strip via marker"""...""".trimMargin()Anchors on a leading "|" (or custom) per line
Build a Regex"""pattern""".toRegex()Raw string avoids double-escaping backslashes
Full-string matchregex.matches(s)The whole string must match, not a substring
First matchregex.find(s)Returns MatchResult? โ€” null if no match
Every matchregex.findAll(s)Sequence<MatchResult>
Capture groups as variablesmatch.destructuredDestructure directly instead of groupValues[i]