JavaRush /Courses /Kotlin SELF /Regex minimum — Regex(...) and replace for text normaliza...

Regex minimum — Regex(...) and replace for text normalization

Kotlin SELF
Level 27, Lesson 3
Available

1. Why do you need Regex if you already have replace?

If you already know how to do text.replace("old", "new"), a reasonable question may come up: “Why do I need yet another thing called a ‘regular expression’, the one that gives half of programmers a nervous tic?” And yes: for a simple replacement of a specific substring, plain replace is often enough.

The problem starts when you need to replace not a specific piece of text, but a whole class of similar situations: “any number of spaces,” “any digits,” “any of the characters - or _,” “any tab,” and so on.

Imagine you’re writing a console application (our training CLI), and the user enters a command:

add 120 food coffee

If you try to “manually” bring this to a normal form (remove extra spaces, tabs, line breaks), you’ll quickly start writing code that suspiciously resembles a mini parser. But there’s a simpler way: describe the rule once — “replace all consecutive whitespace characters with a single space” — and you get stable input.

2. What is Regex in Kotlin

Regex as an object, and the pattern as a string

In this lecture we’ll think about Regex in the most down-to-earth way possible. Not as a “language for summoning demons,” but as a pattern that can find something in text according to a rule.

A couple of important points “in human terms”:

Regex in Kotlin is an object. You can create it, store it in a val, and reuse it. The pattern inside Regex(...) is a string with special syntax. There, some characters do not mean themselves, but a “rule.”

For example, \s means “a whitespace character,” and + means “one or more times in a row.” So \s+ reads as “one or more consecutive whitespace characters.”

Kotlin provides several ways to get a regex; sometimes you’ll see "...".toRegex() in someone else’s code. For example, Kotlin documentation shows removing spaces via input.replace("\\s".toRegex(), ""). But today we keep the focus on the Regex("...") form, to make it maximally explicit: “here is a regex object, here is its pattern.”

Escaping: a normal string vs. a raw string

With regexes, there’s one everyday (and very common) source of pain: escaping.

In Kotlin, the string "\\s+" contains the characters \ and s and + (because \\ inside a normal string means “a literal backslash”). And "\s+" cannot be written like that, because \s will be an attempt to interpret \ as the start of an escape sequence in the string.

So the rule is simple and friendly:

  • if you write a regex pattern in a normal string, there will almost always be a lot of \\
  • if you write a regex pattern in a raw string """...""", you can write backslashes as-is

Let’s compare on one example:


fun main() {
    val r1 = Regex("\\s+")      // normal string: you need \\s
    val r2 = Regex("""\s+""")   // raw string: write \s as-is

    println(r1.pattern)         // \s+
    println(r2.pattern)         // \s+
}

Usually, for regular expressions, a raw string is basically a “painless development mode.” Not a cure-all, but definitely one less reason to suffer.

3. Regex.replace(...): find everything that matches and replace it

Now the most practical part: the Regex object has a replace method that takes text and a replacement string. It replaces all matches (i.e., it’s “replace all,” not “replace first”). That’s perfect for normalization.

The basic form looks like this:

val regex = Regex("""...""")
val result = regex.replace(text, "replacement")

And yes, you’ll often see a second style as well: text.replace(regex, "replacement"). It works too. But today we stick to the regex.replace(...) form, because it reads better as “here’s the rule; apply it to the text.”

4. Whitespace normalization: Regex("""\s+""")

In real input, whitespace comes in different forms. It’s not only ' ' (a normal space), but also \t (a tab), and a line break \n. For a person it’s “some kind of emptiness,” but for command parsing it’s a potential problem.

The pattern \s means “any whitespace character,” and + means “one or more times in a row.” So \s+ is “any block of whitespace characters.”

Let’s make a function that converts any “dirty” input into the form “words separated by a single space”:

fun normalizeSpaces(text: String): String {
    val spaceRegex = Regex("""\s+""")
    return spaceRegex.replace(text.trim(), " ")
}

fun main() {
    val raw = "  add\t\t120   food \n coffee   "
    val clean = normalizeSpaces(raw)

    println(clean) // add 120 food coffee
}

Notice trim(). The \s+ regex collapses whitespace inside the string perfectly, but if the string has “junk spaces” at the edges, it’s simpler and more readable to remove them with the already familiar trim().

5. Mini cheat sheet for regex elements

To avoid turning the lecture into an “introduction to infinity,” let’s lock in only the regex elements we need today. Important: this is not “everything about regex,” it’s “the minimum you need to do normalization.”

Pattern element Meaning Example pattern What it matches
\s
one whitespace character (space, tab, line break...)
\s
any single “space”
\d
one digit
\d
0…9
+
“one or more times in a row” for the previous element
\s+
a block of spaces/tabs
[ ... ]
a set of characters (“one of the listed”)
[-_]
one
-
or
_

That’s already enough to bring 80% of “human input” to a normal form.

6. Separator normalization: turn spaces, __, and --- into a single -

Now a real-life example. In our CLI project, we often use expense categories. A user might enter:

Fast Food, fast-food, FAST__FOOD, fast food

But we want to store the category in one style, for example: fast-food.

Let’s build a normalizer that:

  1. removes surrounding whitespace
  2. converts to lowercase
  3. collapses any sequences of spaces, hyphens, and underscores into a single hyphen
  4. removes hyphens at the edges (so we don’t get -food-)
fun normalizeCategory(raw: String): String {
    val sep = Regex("""[-_\s]+""") // hyphen or _ or whitespace
    val lowered = raw.trim().lowercase()
    val dashed = sep.replace(lowered, "-")
    return dashed.trim('-')
}

fun main() {
    println(normalizeCategory("  FAST__FOOD  "))  // fast-food
    println(normalizeCategory("fast   food"))     // fast-food
    println(normalizeCategory("__food__"))        // food
}

Note an important point: we don’t make the regex “too smart.” We don’t try to validate “category correctness” down to the last letter. We just bring the input to a consistent form. That’s what normalization is: first make the data the same, and only then compare and group conveniently.

7. Data masking: replace digits via \d

Another very practical use case is “masking” data, for example a card/phone number in logs or a report. Yes, in our training CLI we usually don’t write banking software, but the skill is useful.

The \d pattern finds each digit, and replace(..., "*") replaces each digit with *.

fun maskDigits(text: String): String {
    val digit = Regex("""\d""")
    return digit.replace(text, "*")
}

fun main() {
    val raw = "Order #2048, phone: +1 555 120-77-33"
    println(maskDigits(raw))
    // Order #****, phone: +* *** ***-**-**
}

This is a simple example, but it shows the mindset well: regex is not necessarily about “parsing everything.” Sometimes it’s just “quickly find a class of characters and replace it.”

8. Integrate into our CLI app: resilient command input

Now let’s tie everything to our console project (let’s call it BudgetCLI). Recall the general style: we read a string, normalize it, parse the command, and update collections.

Let an expense record for now be stored as Triple<String, Int, String>:

  • first — category (already normalized)
  • second — amount
  • third — note (as text)

Let’s make a small but illustrative piece: handling the add command.

Normalize the command line

First we’ll bring input to “one space between tokens.” This greatly simplifies split(" ").

fun normalizeCommandLine(raw: String): String {
    val spaces = Regex("""\s+""")
    return spaces.replace(raw.trim(), " ")
}

fun main() {
    val line = "  add\t  120   fast__food   coffee  "
    println(normalizeCommandLine(line)) // add 120 fast__food coffee
}

Now the command is predictable: you can safely do split(" ") and not worry about empty tokens due to five spaces in a row.

Mini handler for add with category normalization

Let’s add one more step: normalize the category via normalizeCategory(...), and assemble the note back into a string.

fun handleAdd(expenses: MutableList<Triple<String, Int, String>>, rawLine: String) {
    val line = normalizeCommandLine(rawLine)
    val parts = line.split(" ")

    val amount = parts.getOrNull(1)?.toIntOrNull() ?: return
    val category = normalizeCategory(parts.getOrNull(2) ?: return)
    val note = parts.drop(3).joinToString(" ")

    expenses.add(Triple(category, amount, note))
}

fun main() {
    val expenses = mutableListOf<Triple<String, Int, String>>()
    handleAdd(expenses, "add   120  FAST__FOOD   coffee to go")

    println(expenses) // [(fast-food, 120, coffee to go)]
}

Here we’re not trying to do perfect error handling (you already know how to do that via if, when, try/catch, and validation). But what matters is something else: thanks to Regex.replace, normalization is separated from command logic. You’re not smearing “fighting spaces” all over the code.

Pipeline: raw input → normalized input → parsing

To make it easier for your brain, think of it as a small pipeline:

flowchart LR
    A["raw input
user's string"] --> B["normalize
Regex.replace + trim"] B --> C["parse
split + toIntOrNull"] C --> D["domain update
added to collection"]

The more neatly you separate these steps, the less “magic” you have in one place. And the less magic — the fewer late-night conversations with exceptions.

9. Typical mistakes when working with Regex.replace

Mistake #1: mixing up “regular replace” and “regex replace.”
text.replace(".", "-") replaces a dot as a character (when it’s a string), but in regex . means “any character.” If you accidentally think you’re doing a normal string replacement, but in fact you’re using a regex, you can replace “too much.” That’s why in this lecture we keep the style val r = Regex(...) and then r.replace(...): it makes it explicit that this is regex logic.

Mistake #2: forgetting about escaping in a normal string.
Regex("\s+") won’t compile the way you expect, because \s inside a normal string is not “literal characters,” but an escape attempt. Either write Regex("\\s+"), or (often more convenient) use a raw string Regex("""\s+""").

Mistake #3: trying to “validate the world” with regex when you need normalization.
A very common beginner trap: instead of converting whitespace to a single space, a person starts building a regex that “allows only correct input.” As a result, the pattern becomes longer than your program, and the bugs become more interesting. For normalization, simple rules are better: collapse spaces, normalize case, replace separators.

Mistake #4: forgetting trim() after whitespace normalization.
Even if you replaced \s+ with " ", there still can be a single space left at the edges of the string. It’s not fatal, but later it unexpectedly breaks string comparison or output. The combination replace(...).trim() usually gives the most predictable result.

Mistake #5: recreating Regex in every place for no reason.
Technically, you can write Regex("""\s+""") in every function. But if you do it inside a hot path (for example, in a loop processing thousands of lines), you create extra objects. It’s better to move regexes into a val next to normalization functions and reuse them. In our training project it’s not a performance issue yet, but it is a matter of discipline and code cleanliness.

1
Task
Kotlin SELF, level 27, lesson 3
Locked
Clean search
Clean search
1
Task
Kotlin SELF, level 27, lesson 3
Locked
Category for URL
Category for URL
1
Task
Kotlin SELF, level 27, lesson 3
Locked
Splitting into words
Splitting into words
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION