Regex #
Regular expression (regex) adalah bahasa mini untuk mendeskripsikan pola dalam teks. Dengan satu baris pola, kamu bisa memvalidasi format email, mengekstrak semua nomor telepon dari sebuah dokumen, atau mengganti semua tanggal dalam format lama ke format baru. Kotlin menyediakan kelas Regex yang bersifat immutable dan thread-safe, dengan API yang lebih bersih dari Java. Satu keunggulan besar di Kotlin: raw string (triple-quote) memungkinkan penulisan pola regex tanpa perlu double-escaping yang menyiksa. Artikel ini membahas seluruh kapabilitas regex di Kotlin — dari sintaks dasar hingga named capture group, mode matching, dan pola-pola validasi yang umum digunakan di aplikasi nyata.
Membuat Regex #
Ada dua cara membuat objek Regex:
// Cara 1: konstruktor Regex
val polaNomor = Regex("\\d+")
// Cara 2: extension function toRegex() pada String
val polaNomor2 = "\\d+".toRegex()
// Cara 3 (direkomendasikan): raw string — tidak perlu escape backslash
val polaNomor3 = """\d+""".toRegex()
Raw String untuk Regex #
Di Kotlin, \\d dan \d adalah hal yang berbeda. Dalam string biasa, \d adalah escape sequence yang tidak valid, sehingga kamu harus menulis \\d agar karakter \d sampai ke mesin regex. Dalam raw string (triple-quote), tidak ada escape — \d langsung dikirim ke regex apa adanya.
// String biasa: perlu escape ganda — mudah salah dan susah dibaca
val emailBiasa = "^[a-zA-Z0-9._%+\\-]+@[a-zA-Z0-9.\\-]+\\.[a-zA-Z]{2,}$".toRegex()
// Raw string: perlu tulis apa adanya — jauh lebih bersih
val emailRaw = """^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$""".toRegex()
// Raw string juga mendukung multiline untuk dokumentasi
val nomorTelepon = """
^(\+62|0) # awalan: +62 atau 0
[0-9]{2,3} # kode area
[-\s]? # pemisah opsional
[0-9]{3,4} # blok tengah
[-\s]? # pemisah opsional
[0-9]{4}$ # blok akhir
""".trimIndent().toRegex(RegexOption.COMMENTS)
Referensi Pola Regex #
Sebelum membahas metode, penting memahami elemen pola yang paling sering digunakan:
Karakter dan Kelas #
| Pola | Cocok dengan |
|---|---|
\d | Digit: [0-9] |
\D | Non-digit |
\w | Word char: [a-zA-Z0-9_] |
\W | Non-word char |
\s | Whitespace: spasi, tab, newline |
\S | Non-whitespace |
. | Karakter apapun (kecuali newline) |
[abc] | Salah satu dari a, b, atau c |
[^abc] | Bukan a, b, atau c |
[a-z] | Huruf kecil a sampai z |
[A-Za-z0-9] | Alfanumerik |
Kuantifier #
| Pola | Artinya |
|---|---|
* | 0 atau lebih kali |
+ | 1 atau lebih kali |
? | 0 atau 1 kali (opsional) |
{n} | Tepat n kali |
{n,} | Minimal n kali |
{n,m} | Antara n dan m kali |
*? | 0 atau lebih, non-greedy |
+? | 1 atau lebih, non-greedy |
Anchor dan Grup #
| Pola | Artinya |
|---|---|
^ | Awal string (atau awal baris dengan MULTILINE) |
$ | Akhir string (atau akhir baris dengan MULTILINE) |
\b | Word boundary |
(abc) | Capture group |
(?:abc) | Non-capturing group |
(?<nama>abc) | Named capture group |
a|b | a atau b |
(?=abc) | Lookahead positif |
(?!abc) | Lookahead negatif |
Metode Utama #
matches — Cocokkan Seluruh String
#
matches memeriksa apakah seluruh string sesuai dengan pola (bukan hanya sebagian):
val angka = """\d+""".toRegex()
println(angka.matches("12345")) // true — seluruh string adalah digit
println(angka.matches("123abc")) // false — ada karakter non-digit
println(angka.matches("")) // false — harus minimal 1 digit
// Bandingkan dengan containsMatchIn
println(angka.containsMatchIn("ada 123 di sini")) // true — ada bagian yang cocok
println(angka.matches("ada 123 di sini")) // false — seluruh string tidak cocok
// Penggunaan umum: validasi format
val kodePos = """\d{5}""".toRegex()
println(kodePos.matches("12345")) // true
println(kodePos.matches("1234")) // false — kurang dari 5 digit
println(kodePos.matches("123456")) // false — lebih dari 5 digit
find dan findAll — Temukan Kecocokan
#
find mencari kecocokan pertama, findAll mencari semua kecocokan:
val angka = """\d+""".toRegex()
val teks = "Pesanan #123 berisi 5 item senilai Rp450000"
// find — kecocokan pertama
val pertama = angka.find(teks)
println(pertama?.value) // 123
println(pertama?.range) // 10..12
// find dengan posisi mulai
val dariPosisi = angka.find(teks, startIndex = 15)
println(dariPosisi?.value) // 5
// findAll — semua kecocokan
val semua = angka.findAll(teks)
semua.forEach { println(it.value) }
// 123
// 5
// 450000
// Konversi ke list
val daftarAngka = angka.findAll(teks).map { it.value.toInt() }.toList()
println(daftarAngka) // [123, 5, 450000]
println(daftarAngka.sum()) // 450128
Capture Group dan groupValues
#
Capture group () memungkinkan mengekstrak bagian spesifik dari kecocokan:
// Ekstrak komponen tanggal dari format dd/MM/yyyy
val formatTanggal = """(\d{2})/(\d{2})/(\d{4})""".toRegex()
val teks = "Tanggal lahir: 17/08/1945"
val kecocokan = formatTanggal.find(teks)
if (kecocokan != null) {
println(kecocokan.value) // 17/08/1945 (kecocokan penuh)
println(kecocokan.groupValues[0]) // 17/08/1945 (grup 0 = seluruh kecocokan)
println(kecocokan.groupValues[1]) // 17 (grup 1 = hari)
println(kecocokan.groupValues[2]) // 08 (grup 2 = bulan)
println(kecocokan.groupValues[3]) // 1945 (grup 3 = tahun)
}
Named Capture Group — Lebih Ekspresif #
Named capture group (?<nama>pola) membuat kode jauh lebih mudah dibaca:
// Named group — lebih jelas dari indeks angka
val formatTanggalBernama = """(?<hari>\d{2})/(?<bulan>\d{2})/(?<tahun>\d{4})""".toRegex()
val kecocokan = formatTanggalBernama.find("Proklamasi: 17/08/1945")
if (kecocokan != null) {
val hari = kecocokan.groups["hari"]?.value
val bulan = kecocokan.groups["bulan"]?.value
val tahun = kecocokan.groups["tahun"]?.value
println("$hari $bulan $tahun") // 17 08 1945
}
// Contoh: parse log entry
val formatLog = """(?<waktu>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) \[(?<level>\w+)\] (?<pesan>.+)""".toRegex()
val log = "2024-08-17 10:30:45 [ERROR] Koneksi database gagal"
formatLog.find(log)?.let { m ->
println("Waktu: ${m.groups["waktu"]?.value}") // 2024-08-17 10:30:45
println("Level: ${m.groups["level"]?.value}") // ERROR
println("Pesan: ${m.groups["pesan"]?.value}") // Koneksi database gagal
}
replace — Ganti Kecocokan
#
val spasiBerlebih = """\s+""".toRegex()
val teks = "Kotlin adalah bahasa yang hebat"
// Ganti semua spasi berlebih dengan satu spasi
println(spasiBerlebih.replace(teks, " "))
// Kotlin adalah bahasa yang hebat
// replace dengan transformasi — ubah setiap kecocokan dengan lambda
val angka = """\d+""".toRegex()
val harga = "Harga: 50000 dan diskon: 10000"
val denganFormat = angka.replace(harga) { match ->
"Rp${"%,d".format(match.value.toInt())}"
}
println(denganFormat) // Harga: Rp50,000 dan diskon: Rp10,000
// replaceFirst — hanya ganti kecocokan pertama
val hasilPertama = angka.replaceFirst(harga, "###")
println(hasilPertama) // Harga: ### dan diskon: 10000
split — Pecah String
#
// Split berdasarkan satu atau lebih spasi/koma/titik koma
val pemisah = """[\s,;]+""".toRegex()
val input = "kotlin, java; python go"
println(pemisah.split(input))
// [kotlin, java, python, go]
// Split dengan limit
println(pemisah.split(input, limit = 2))
// [kotlin, java; python go]
RegexOption — Opsi Matching
#
RegexOption mengubah perilaku pencocokan:
// IGNORE_CASE — abaikan huruf besar/kecil
val pola = "kotlin".toRegex(RegexOption.IGNORE_CASE)
println(pola.containsMatchIn("Saya belajar KOTLIN")) // true
println(pola.containsMatchIn("Saya belajar Kotlin")) // true
// Beberapa opsi sekaligus
val multiOpsi = """^\d+$""".toRegex(setOf(
RegexOption.MULTILINE, // ^ dan $ berlaku per baris
RegexOption.IGNORE_CASE
))
// MULTILINE — ^ dan $ cocok dengan awal/akhir setiap baris
val polaBaris = """^\w+""".toRegex(RegexOption.MULTILINE)
val multiBaris = """
baris pertama
baris kedua
baris ketiga
""".trimIndent()
polaBaris.findAll(multiBaris).forEach { println(it.value) }
// baris
// baris
// baris
// COMMENTS — izinkan whitespace dan komentar dalam pola
val polaKomentar = """
\d{4} # tahun
-
\d{2} # bulan
-
\d{2} # hari
""".trimIndent().toRegex(RegexOption.COMMENTS)
println(polaKomentar.matches("2024-08-17")) // true
Validasi Input — Contoh Nyata #
Ini adalah penggunaan regex paling umum di aplikasi. Selalu simpan pattern sebagai konstanta agar tidak dikompilasi ulang setiap pemanggilan.
object ValidasiPola {
// Email standar — tidak mendukung quoted string atau IP literal
val EMAIL = """^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$""".toRegex()
// Nomor telepon Indonesia: +62xxx atau 08xx, dengan atau tanpa pemisah
val NOMOR_TELEPON_ID = """^(\+62|0)[0-9]{2,3}[-\s]?[0-9]{3,4}[-\s]?[0-9]{4}$""".toRegex()
// URL sederhana
val URL = """^https?://[^\s/$.?#].[^\s]*$""".toRegex()
// Kode pos Indonesia (5 digit)
val KODE_POS = """\d{5}""".toRegex()
// NIK Indonesia (16 digit)
val NIK = """\d{16}""".toRegex()
// Username: huruf, angka, underscore, 3-20 karakter
val USERNAME = """^[a-zA-Z0-9_]{3,20}$""".toRegex()
// Password kuat: min 8 karakter, huruf besar, kecil, angka, dan simbol
val HURUF_BESAR = """[A-Z]""".toRegex()
val HURUF_KECIL = """[a-z]""".toRegex()
val ANGKA = """[0-9]""".toRegex()
val SIMBOL = """[!@#${'$'}%^&*()_+\-=\[\]{};':",.<>?/]""".toRegex()
}
fun validasiEmail(email: String): Boolean =
ValidasiPola.EMAIL.matches(email)
fun validasiPassword(sandi: String): List<String> {
val masalah = mutableListOf<String>()
if (sandi.length < 8) masalah.add("Minimal 8 karakter")
if (!ValidasiPola.HURUF_BESAR.containsMatchIn(sandi)) masalah.add("Butuh minimal 1 huruf besar")
if (!ValidasiPola.HURUF_KECIL.containsMatchIn(sandi)) masalah.add("Butuh minimal 1 huruf kecil")
if (!ValidasiPola.ANGKA.containsMatchIn(sandi)) masalah.add("Butuh minimal 1 angka")
if (!ValidasiPola.SIMBOL.containsMatchIn(sandi)) masalah.add("Butuh minimal 1 simbol")
return masalah
}
// Pengujian
listOf(
"[email protected]",
"bukan-email",
"user@",
"[email protected]"
).forEach { email ->
println("$email → ${if (validasiEmail(email)) "✓ Valid" else "✗ Tidak valid"}")
}
// [email protected] → ✓ Valid
// bukan-email → ✗ Tidak valid
// user@ → ✗ Tidak valid
// [email protected] → ✓ Valid
val masalahSandi = validasiPassword("abc")
if (masalahSandi.isEmpty()) println("Password kuat!")
else masalahSandi.forEach { println("• $it") }
// • Minimal 8 karakter
// • Butuh minimal 1 huruf besar
// • Butuh minimal 1 angka
// • Butuh minimal 1 simbol
Ekstraksi Data dari Teks #
Regex juga sangat berguna untuk mengekstrak data terstruktur dari teks tidak terstruktur:
// Ekstrak semua URL dari HTML
val urlRegex = """https?://[^\s"'<>]+""".toRegex()
val html = """
<a href="https://kotlin.unisbadri.com">Kotlin</a>
<img src="https://example.com/gambar.png">
Kunjungi http://docs.kotlin.org untuk dokumentasi
""".trimIndent()
val url = urlRegex.findAll(html).map { it.value }.toList()
url.forEach { println(it) }
// https://kotlin.unisbadri.com
// https://example.com/gambar.png
// http://docs.kotlin.org
// Ekstrak harga dari teks
val hargaRegex = """Rp\s*[\d.,]+""".toRegex(RegexOption.IGNORE_CASE)
val katalog = "Laptop Rp15.000.000, Mouse rp 250.000, Keyboard Rp500.000"
hargaRegex.findAll(katalog).forEach { println(it.value) }
// Rp15.000.000
// rp 250.000
// Rp500.000
// Parse CSV sederhana
val csvBaris = """"Budi Santoso","25","Jakarta","[email protected]""""
val kolomRegex = """"([^"]*)"""".toRegex()
val kolom = kolomRegex.findAll(csvBaris).map { it.groupValues[1] }.toList()
println(kolom) // [Budi Santoso, 25, Jakarta, [email protected]]
Pembersihan dan Normalisasi Teks #
// Hapus karakter non-alphanumeric untuk slug URL
fun buatSlug(judul: String): String {
val nonAlpha = """[^a-zA-Z0-9\s]""".toRegex()
val spasiGanda = """\s+""".toRegex()
return judul
.lowercase()
.let { nonAlpha.replace(it, "") }
.let { spasiGanda.replace(it, "-") }
.trim('-')
}
println(buatSlug("Belajar Kotlin — Panduan Lengkap!"))
// belajar-kotlin-panduan-lengkap
// Hapus tag HTML
fun hapusHtml(html: String): String {
val tagHtml = """<[^>]+>""".toRegex()
return tagHtml.replace(html, "")
}
println(hapusHtml("<h1>Judul</h1><p>Ini <b>teks</b> dengan HTML</p>"))
// JudulIni teks dengan HTML
// Normalisasi nomor telepon Indonesia ke format standar
fun normalisasiTelepon(nomor: String): String? {
val bersih = """[\s\-()]""".toRegex().replace(nomor, "")
return when {
bersih.matches("""^08\d{8,11}$""".toRegex()) ->
"+62" + bersih.substring(1)
bersih.matches("""^\+628\d{8,11}$""".toRegex()) -> bersih
else -> null
}
}
println(normalisasiTelepon("0812-3456-7890")) // +6281234567890
println(normalisasiTelepon("+62 812 3456 7890")) // +6281234567890
println(normalisasiTelepon("bukan nomor")) // null
Tips Performa dan Praktik Terbaik #
Kompilasi Regex Hanya Sekali #
// ANTI-PATTERN: regex dikompilasi ulang setiap pemanggilan fungsi
fun validasiEmailBuruk(email: String): Boolean {
return """^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$""".toRegex().matches(email)
}
// BENAR: kompilasi sekali sebagai konstanta atau properti
object Validator {
private val EMAIL = """^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$""".toRegex()
fun validasiEmail(email: String) = EMAIL.matches(email)
}
// Atau sebagai top-level val
private val REGEX_EMAIL = """^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$""".toRegex()
fun validasiEmail(email: String) = REGEX_EMAIL.matches(email)
Kapan Tidak Menggunakan Regex #
GUNAKAN regex jika:
✓ Pola kompleks yang tidak bisa ditangani String methods biasa
✓ Perlu ekstrak bagian spesifik dengan capture group
✓ Validasi format yang tidak trivial (email, URL, nomor telepon)
✓ Mencari semua kecocokan pola dalam teks panjang
JANGAN gunakan regex jika:
✗ Cek apakah string mengandung substring → gunakan contains()
✗ Cek awalan/akhiran → gunakan startsWith()/endsWith()
✗ Ganti string literal → gunakan replace() biasa
✗ Pecah berdasarkan delimiter tetap → gunakan split() biasa
✗ Pola sangat sederhana → String methods lebih cepat dan lebih jelas
Ringkasan #
- Gunakan raw string untuk pola regex —
"""\d+"""jauh lebih bersih dari"\\d+". Hindari double-escaping yang membuat pola susah dibaca dan rawan salah.matchesvscontainsMatchIn—matchesmemeriksa seluruh string,containsMatchInmencari di manapun dalam string. Pilih sesuai kebutuhan validasi.- Named capture group untuk keterbacaan —
(?<tahun>\d{4})lebih jelas dari\d{4}tanpa nama. Akses viagroups["tahun"]?.value.- Simpan Regex sebagai konstanta — kompilasi regex mahal. Deklarasikan sebagai
valdi level kelas atau object agar dikompilasi hanya sekali.replacedengan lambda untuk transformasi — ketika penggantian perlu dihitung dari nilai kecocokan (misalnya memformat angka), gunakan versireplaceyang menerima lambda transformasi.RegexOption.COMMENTS— gunakan opsi ini bersama raw string multiline untuk mendokumentasikan pola regex yang kompleks langsung di dalam kode.RegexOption.IGNORE_CASE— untuk pencocokan case-insensitive tanpa perlu[a-zA-Z]eksplisit dalam pola.- Jangan pakai regex untuk yang bisa diselesaikan String methods —
contains(),startsWith(),split()jauh lebih cepat dan lebih mudah dibaca untuk kasus sederhana. Regex adalah alat kuat untuk masalah yang memang membutuhkannya.