Regex #
Regular expression (regex) adalah mini-language untuk mendeskripsikan pola dalam teks — alat yang tidak tergantikan untuk validasi, ekstraksi, dan transformasi string. Dart mengimplementasikan regex melalui kelas RegExp yang mengikuti standar ECMAScript (sama dengan JavaScript), sehingga pola yang kamu tulis di alat uji regex online akan berjalan identik di Dart. Meski powerful, regex juga punya reputasi sebagai kode yang “tulis sekali, tidak pernah dimengerti lagi” — artikel ini membangun pemahaman dari dasar hingga pola yang cukup canggih, disertai kapan sebaiknya tidak menggunakan regex.
RegExp dan Raw String
#
RegExp adalah kelas yang merepresentasikan ekspresi reguler. Karena pola regex banyak menggunakan backslash (\) yang juga merupakan karakter escape di Dart, hampir selalu gunakan raw string (r'...') untuk menulis pola — ini menghilangkan ambiguitas:
// Tanpa raw string — backslash harus di-escape dua kali
RegExp tanpaRaw = RegExp('\\d+'); // \\ agar Dart menghasilkan literal \
// Dengan raw string — backslash langsung diteruskan ke regex engine
RegExp denganRaw = RegExp(r'\d+'); // ✓ lebih bersih dan tidak ambigu
// Untuk pola kompleks, perbedaannya sangat mencolok
RegExp emailBuruk = RegExp('^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}\$');
RegExp emailBaik = RegExp(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$');
Flag RegExp
#
RegExp mendukung empat flag opsional yang mengubah cara pola diinterpretasikan:
// caseSensitive — default: true (huruf besar/kecil dibedakan)
RegExp sensitif = RegExp(r'dart');
RegExp tidakSensitif = RegExp(r'dart', caseSensitive: false);
print(sensitif.hasMatch('Dart')); // false
print(tidakSensitif.hasMatch('DART')); // true
print(tidakSensitif.hasMatch('dart')); // true
// multiLine — ^ dan $ cocok dengan awal/akhir SETIAP baris
String teks = 'baris pertama\nbaris kedua\nbaris ketiga';
RegExp single = RegExp(r'^baris');
RegExp multi = RegExp(r'^baris', multiLine: true);
print(single.allMatches(teks).length); // 1 — hanya awal string
print(multi.allMatches(teks).length); // 3 — awal setiap baris
// dotAll — . cocok dengan karakter APAPUN termasuk newline
String multiline = 'awal\nakhir';
RegExp dotNormal = RegExp(r'awal.akhir');
RegExp dotAll = RegExp(r'awal.akhir', dotAll: true);
print(dotNormal.hasMatch(multiline)); // false — . tidak cocok \n
print(dotAll.hasMatch(multiline)); // true — . cocok \n
// unicode — aktifkan Unicode mode untuk karakter di luar BMP
RegExp unicode = RegExp(r'\p{L}+', unicode: true); // cocok semua huruf Unicode
Method RegExp — Cara Bekerja dengan Pola
#
RegExp pola = RegExp(r'\d{3,}');
String teks = 'Ada 123 apel, 45 jeruk, dan 6789 mangga';
// hasMatch — cek apakah ada kecocokan (paling efisien untuk cek ada/tidak)
bool ada = pola.hasMatch(teks); // true
// firstMatch — dapatkan detail kecocokan pertama
RegExpMatch? pertama = pola.firstMatch(teks);
print(pertama?.group(0)); // '123'
print(pertama?.start); // 4 (posisi awal dalam string)
print(pertama?.end); // 7 (posisi akhir, eksklusif)
// stringMatch — string dari kecocokan pertama (shortcut)
String? str = pola.stringMatch(teks); // '123'
// allMatches — semua kecocokan sebagai Iterable
Iterable<RegExpMatch> semua = pola.allMatches(teks);
for (final m in semua) {
print('Cocok: "${m.group(0)}" di posisi ${m.start}-${m.end}');
}
// Cocok: "123" di posisi 4-7
// Cocok: "6789" di posisi 30-34
// allMatches dengan start — mulai pencarian dari posisi tertentu
Iterable<RegExpMatch> dariPosisi = pola.allMatches(teks, 10);
// pencarian dimulai dari indeks 10
Sintaks Pola Regex Lengkap #
Karakter dan Kelas Karakter #
. sembarang karakter kecuali newline (kecuali dotAll aktif)
\d digit [0-9]
\D bukan digit
\w word character [a-zA-Z0-9_]
\W bukan word character
\s whitespace (spasi, tab, newline, carriage return)
\S bukan whitespace
[abc] salah satu dari: a, b, atau c
[^abc] bukan a, b, atau c
[a-z] huruf kecil a sampai z
[A-Z] huruf kapital A sampai Z
[0-9] digit 0 sampai 9
[a-zA-Z0-9] semua huruf dan digit
Karakter khusus yang perlu di-escape: . * + ? ^ $ { } [ ] | ( ) \
// Contoh kelas karakter
RegExp huruf = RegExp(r'[a-zA-Z]+');
RegExp bukanHuruf = RegExp(r'[^a-zA-Z]+');
RegExp hexColor = RegExp(r'#[0-9a-fA-F]{6}');
print(huruf.stringMatch('abc123')); // 'abc'
print(hexColor.hasMatch('#FF5733')); // true
print(hexColor.hasMatch('#GG0000')); // false — G bukan hex
Anchors — Posisi dalam String #
^ awal string (atau awal baris jika multiLine aktif)
$ akhir string (atau akhir baris jika multiLine aktif)
\b batas kata (word boundary) — antara \w dan \W
\B bukan batas kata
// ^ dan $ memastikan pola cocok dengan SELURUH string
RegExp hanyaAngka = RegExp(r'^\d+$');
print(hanyaAngka.hasMatch('12345')); // true
print(hanyaAngka.hasMatch('123abc')); // false — ada huruf
// \b untuk word boundary — pastikan cocok kata utuh
RegExp kataDart = RegExp(r'\bDart\b');
print(kataDart.hasMatch('Dart adalah bahasa')); // true
print(kataDart.hasMatch('Dartmouth')); // false — 'Dart' bukan kata utuh
print(kataDart.hasMatch('DartPad')); // false
Quantifier — Jumlah Pengulangan #
* 0 atau lebih (greedy)
+ 1 atau lebih (greedy)
? 0 atau 1 (greedy)
{n} tepat n kali
{n,} n atau lebih kali
{n,m} antara n dan m kali (inklusif)
*? 0 atau lebih (lazy/non-greedy)
+? 1 atau lebih (lazy/non-greedy)
?? 0 atau 1 (lazy/non-greedy)
{n,m}? antara n dan m kali (lazy)
Greedy vs Lazy #
Quantifier greedy mengambil sebanyak mungkin karakter yang cocok. Quantifier lazy (tambahkan ?) mengambil sesedikit mungkin:
String html = '<b>Teks tebal</b> dan <i>Italic</i>';
// Greedy — mengambil dari <b> sampai </i> (terlalu banyak!)
RegExp greedyTag = RegExp(r'<.+>');
print(greedyTag.stringMatch(html));
// '<b>Teks tebal</b> dan <i>Italic</i>' — seluruh string!
// Lazy — mengambil tag terpendek yang cocok
RegExp lazyTag = RegExp(r'<.+?>');
for (final m in lazyTag.allMatches(html)) {
print(m.group(0)); // '<b>', '</b>', '<i>', '</i>' — masing-masing tag
}
// ANTI-PATTERN: greedy yang tidak disengaja menghasilkan kecocokan terlalu luas
String json = '{"nama": "Budi", "kota": "Jakarta"}';
RegExp kunciGreedy = RegExp(r'".*"');
print(kunciGreedy.stringMatch(json));
// '"nama": "Budi", "kota": "Jakarta"' — mengambil semuanya!
// BENAR: lazy untuk mencocokkan string JSON dengan benar
RegExp kunciLazy = RegExp(r'".*?"');
for (final m in kunciLazy.allMatches(json)) {
print(m.group(0)); // '"nama"', '"Budi"', '"kota"', '"Jakarta"'
}
Capturing Group #
Group () menangkap bagian dari kecocokan dan membuatnya bisa diakses secara terpisah dari seluruh kecocokan:
// Group diberi indeks mulai dari 1
// group(0) = seluruh kecocokan
// group(1) = group pertama, dst
RegExp tanggal = RegExp(r'(\d{4})-(\d{2})-(\d{2})');
String teks = 'Tanggal lahir: 1998-05-20';
RegExpMatch? match = tanggal.firstMatch(teks);
if (match != null) {
print(match.group(0)); // '1998-05-20' — seluruh kecocokan
print(match.group(1)); // '1998' — tahun
print(match.group(2)); // '05' — bulan
print(match.group(3)); // '20' — hari
}
// Named groups — lebih ekspresif untuk pola kompleks
RegExp tanggalNamed = RegExp(r'(?<tahun>\d{4})-(?<bulan>\d{2})-(?<hari>\d{2})');
RegExpMatch? namedMatch = tanggalNamed.firstMatch('2024-11-15');
if (namedMatch != null) {
print(namedMatch.namedGroup('tahun')); // '2024'
print(namedMatch.namedGroup('bulan')); // '11'
print(namedMatch.namedGroup('hari')); // '15'
}
Non-Capturing Group (?:...)
#
Gunakan (?:...) jika butuh grup untuk logika tapi tidak perlu menangkap hasilnya:
// Alternation tanpa menangkap grup
RegExp protokol = RegExp(r'(?:https?|ftp)://[\w./-]+');
print(protokol.hasMatch('https://dart.dev')); // true
print(protokol.hasMatch('ftp://files.dart')); // true
// Grup 1 tidak akan berisi protokol karena non-capturing
RegExpMatch? m = protokol.firstMatch('https://dart.dev');
print(m?.group(1)); // null — tidak ada capturing group
Lookahead dan Lookbehind #
Lookahead dan lookbehind cocokkan posisi berdasarkan konteks tanpa mengonsumsi karakter:
// Positive lookahead (?=...) — cocok jika DIIKUTI oleh pola
RegExp hargaRupiah = RegExp(r'\d+(?=\s?Rb)');
String teks = '500 Rb dan 1500 Rb dan 200';
for (final m in hargaRupiah.allMatches(teks)) {
print(m.group(0)); // '500', '1500' — hanya angka sebelum 'Rb'
}
// Negative lookahead (?!...) — cocok jika TIDAK diikuti oleh pola
RegExp angkaTanpaRb = RegExp(r'\d+(?!\s?Rb|\d)');
for (final m in angkaTanpaRb.allMatches(teks)) {
print(m.group(0)); // '200' — angka yang tidak diikuti 'Rb'
}
// Positive lookbehind (?<=...) — cocok jika DIDAHULUI oleh pola
RegExp setelahRp = RegExp(r'(?<=Rp\s?)\d+');
print(setelahRp.stringMatch('Harga: Rp 50000')); // '50000'
// Negative lookbehind (?<!...) — cocok jika TIDAK didahului oleh pola
RegExp bukanSetelahRp = RegExp(r'(?<!Rp\s?)\d+');
// angka yang bukan harga Rupiah
Mengganti Teks #
replaceAll dan replaceFirst
#
String teks = 'Saya suka jeruk, jeruk sangat manis';
// Ganti semua kecocokan dengan string tetap
String diganti = teks.replaceAll(RegExp(r'jeruk'), 'mangga');
print(diganti); // 'Saya suka mangga, mangga sangat manis'
// Ganti hanya kecocokan pertama
String pertama = teks.replaceFirst(RegExp(r'jeruk'), 'mangga');
print(pertama); // 'Saya suka mangga, jeruk sangat manis'
// Gunakan backreference \1 dalam replacement string
String camel = 'namaDepan belakangNama';
String kebab = camel.replaceAllMapped(
RegExp(r'([A-Z])'),
(m) => '-${m.group(0)!.toLowerCase()}',
);
print(kebab); // 'nama-depan belakang-nama' — camelCase ke kebab-case
replaceAllMapped — Transformasi Dinamis
#
replaceAllMapped memungkinkan penggantian berdasarkan konten kecocokan — jauh lebih powerful dari string pengganti statis:
// Format angka dengan pemisah ribuan
String formatRibuan(String angka) {
return angka.replaceAllMapped(
RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
(m) => '${m.group(1)}.',
);
}
print(formatRibuan('1234567')); // '1.234.567'
print(formatRibuan('9999')); // '9.999'
// Ubah semua URL menjadi link HTML
String tambahLink(String teks) {
return teks.replaceAllMapped(
RegExp(r'https?://[^\s]+'),
(m) => '<a href="${m.group(0)}">${m.group(0)}</a>',
);
}
print(tambahLink('Kunjungi https://dart.dev untuk info lebih lanjut'));
// 'Kunjungi <a href="https://dart.dev">https://dart.dev</a> untuk info lebih lanjut'
// Ubah template variabel {nama} dengan nilai dari Map
String isiTemplate(String template, Map<String, String> data) {
return template.replaceAllMapped(
RegExp(r'\{(\w+)\}'),
(m) => data[m.group(1)] ?? m.group(0)!,
);
}
String pesan = isiTemplate(
'Halo {nama}, pesananmu #{id} sudah dikirim ke {alamat}.',
{'nama': 'Budi', 'id': 'ORD-001', 'alamat': 'Jakarta'},
);
print(pesan); // 'Halo Budi, pesananmu #ORD-001 sudah dikirim ke Jakarta.'
Membuat RegExp sebagai Konstanta
#
Jika regex digunakan berulang kali, deklarasikan sebagai konstanta — menghindari kompilasi ulang pola setiap kali digunakan:
// ANTI-PATTERN: membuat RegExp baru di setiap pemanggilan fungsi
bool isEmail(String input) {
return RegExp(r'^[\w\.-]+@[\w\.-]+\.\w{2,}$').hasMatch(input); // dikompilasi setiap panggil
}
// BENAR: deklarasikan sekali sebagai const atau static
class Validator {
static final RegExp _email = RegExp(r'^[\w\.-]+@[\w\.-]+\.\w{2,}$');
static final RegExp _phone = RegExp(r'^\+?[\d\s\-\(\)]{10,}$');
static final RegExp _url = RegExp(r'^https?://[\w\.-]+(?:/[^\s]*)?$');
static bool isEmail(String s) => _email.hasMatch(s);
static bool isPhone(String s) => _phone.hasMatch(s);
static bool isUrl(String s) => _url.hasMatch(s);
}
Kasus Penggunaan Nyata #
Validasi Format #
abstract class Validator {
// Email — sederhana tapi mencakup kasus umum
static final RegExp email = RegExp(
r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',
);
// Nomor telepon Indonesia (08xx, +628xx, 628xx)
static final RegExp phoneId = RegExp(
r'^(\+62|62|0)8[1-9][0-9]{6,9}$',
);
// Password minimal 8 karakter, ada huruf besar, kecil, dan angka
static final RegExp passwordKuat = RegExp(
r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$',
);
// Kode pos Indonesia (5 digit)
static final RegExp kodePos = RegExp(r'^\d{5}$');
// NIK (16 digit)
static final RegExp nik = RegExp(r'^\d{16}$');
// Slug URL-friendly
static final RegExp slug = RegExp(r'^[a-z0-9]+(?:-[a-z0-9]+)*$');
// Warna hex
static final RegExp hexColor = RegExp(r'^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$');
}
// Penggunaan
void validasiForm(String email, String phone, String password) {
if (!Validator.email.hasMatch(email)) {
throw ArgumentError('Email tidak valid: $email');
}
if (!Validator.phoneId.hasMatch(phone)) {
throw ArgumentError('Nomor telepon tidak valid: $phone');
}
if (!Validator.passwordKuat.hasMatch(password)) {
throw ArgumentError('Password terlalu lemah');
}
}
Ekstraksi Data #
// Ekstrak semua URL dari teks
List<String> ekstrakUrl(String teks) {
final pola = RegExp(r'https?://[^\s<>"{}|\\^`\[\]]+');
return pola.allMatches(teks).map((m) => m.group(0)!).toList();
}
// Ekstrak semua hashtag dari teks
List<String> ekstrakHashtag(String teks) {
final pola = RegExp(r'#\w+');
return pola.allMatches(teks).map((m) => m.group(0)!).toList();
}
print(ekstrakHashtag('Belajar #dart dan #flutter hari ini!'));
// ['#dart', '#flutter']
// Parse log format: [LEVEL] timestamp - pesan
void parseLog(String baris) {
final pola = RegExp(
r'^\[(\w+)\]\s(\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2})\s-\s(.+)$',
);
final m = pola.firstMatch(baris);
if (m != null) {
print('Level: ${m.group(1)}');
print('Waktu: ${m.group(2)}');
print('Pesan: ${m.group(3)}');
}
}
parseLog('[ERROR] 2024-11-15 14:30:45 - Koneksi database gagal');
// Ekstrak angka dari string campuran
List<double> ekstrakAngka(String teks) {
final pola = RegExp(r'-?\d+(?:\.\d+)?');
return pola.allMatches(teks)
.map((m) => double.parse(m.group(0)!))
.toList();
}
print(ekstrakAngka('Suhu: -5.5 derajat, kelembaban: 80%'));
// [-5.5, 80.0]
Transformasi String #
// camelCase ke snake_case
String camelToSnake(String s) {
return s
.replaceAllMapped(
RegExp(r'[A-Z]'),
(m) => '_${m.group(0)!.toLowerCase()}',
)
.replaceFirst(RegExp(r'^_'), ''); // hapus underscore di awal
}
print(camelToSnake('namaLengkapPengguna')); // 'nama_lengkap_pengguna'
// Normalisasi whitespace — ganti multiple spasi/newline dengan satu spasi
String normalWhitespace(String s) {
return s.trim().replaceAll(RegExp(r'\s+'), ' ');
}
print(normalWhitespace(' teks dengan banyak spasi '));
// 'teks dengan banyak spasi'
// Hapus karakter non-alphanumeric
String hanyaAlphanumeric(String s) {
return s.replaceAll(RegExp(r'[^\w\s]'), '');
}
print(hanyaAlphanumeric('Halo, Dunia! #Dart2024'));
// 'Halo Dunia Dart2024'
Kapan Tidak Menggunakan Regex #
Regex adalah alat yang tepat untuk banyak kasus, tapi ada situasi di mana ia bukan pilihan terbaik:
GUNAKAN regex ketika:
✓ Memvalidasi format (email, telepon, kode pos, slug)
✓ Mengekstrak bagian dari teks dengan pola yang terdefinisi baik
✓ Melakukan find-and-replace dengan logika kondisional
✓ Memproses log atau format teks terstruktur yang sederhana
JANGAN gunakan regex ketika:
✗ Parsing HTML atau XML — gunakan parser yang tepat (html package)
✗ Parsing JSON — gunakan jsonDecode
✗ Memvalidasi URL yang kompleks — Uri.tryParse lebih andal
✗ Operasi string sederhana yang bisa dilakukan dengan contains/split/substring
✗ Pola yang sangat panjang dan kompleks — pertimbangkan parser khusus
// ANTI-PATTERN: regex untuk parsing HTML
RegExp htmlTag = RegExp(r'<a[^>]*href=["\']([^"\']+)["\'][^>]*>([^<]+)</a>');
// ✗ Tidak menangani nested tag, entitas HTML, attribute order yang berbeda, dll.
// BENAR: gunakan package html parser
import 'package:html/parser.dart' as html;
final document = html.parse(htmlString);
final links = document.querySelectorAll('a');
// ANTI-PATTERN: regex untuk validasi URL lengkap
RegExp urlKompleks = RegExp(r'^(https?://)?(www\.)?...');
// ✗ URL sangat kompleks — terlalu banyak edge case
// BENAR: gunakan Uri.tryParse
bool isValidUrl(String s) {
final uri = Uri.tryParse(s);
return uri != null && (uri.isScheme('http') || uri.isScheme('https'));
}
Anti-Pattern Regex #
Catastrophic Backtracking #
Pola dengan nested quantifier dan alternation bisa menyebabkan regex engine mengambil waktu eksponensial untuk input tertentu:
// ANTI-PATTERN: nested quantifier yang rentan catastrophic backtracking
RegExp berbahaya = RegExp(r'^(a+)+$');
// Untuk input 'aaaaaaaaaaaab', regex engine mencoba semua kombinasi
// pengelompokan a+ sebelum menyerah — bisa hang detik/menit!
// BENAR: hindari nested quantifier dengan pola yang ekuivalen
RegExp aman = RegExp(r'^a+$');
// Efek yang sama tanpa risiko catastrophic backtracking
Terlalu Bergantung pada Regex untuk Validasi Bisnis #
// ANTI-PATTERN: validasi bisnis yang kompleks dengan satu regex besar
// Email "valid" menurut RFC 5321 memiliki regex sepanjang ratusan karakter
// dan masih tidak sempurna
// BENAR: kombinasikan regex untuk format dasar dengan validasi bisnis terpisah
bool isValidEmail(String email) {
// Regex hanya untuk format dasar
if (!RegExp(r'^[\w\.-]+@[\w\.-]+\.\w{2,}$').hasMatch(email)) return false;
// Validasi bisnis tambahan (tidak bisa dilakukan dengan regex)
final parts = email.split('@');
if (parts[0].length > 64) return false; // lokal part max 64 karakter
if (parts[1].length > 255) return false; // domain max 255 karakter
if (email.length > 320) return false; // total max 320 karakter
return true;
}
Ringkasan #
- Selalu gunakan raw string (
r'...') untuk pola regex — menghindari konflik backslash antara escape Dart dan escape regex.- Flag penting:
caseSensitive: falseuntuk pencarian case-insensitive,multiLine: trueagar^dan$cocok dengan setiap baris,dotAll: trueagar.cocok termasuk newline.hasMatchuntuk cek ada/tidak (paling efisien),firstMatchuntuk detail kecocokan pertama,allMatchesuntuk semua kecocokan.- Greedy vs lazy — quantifier greedy (
*,+) mengambil sebanyak mungkin; tambahkan?(*?,+?) untuk lazy yang mengambil sesedikit mungkin. Gunakan lazy untuk HTML-like parsing.- Capturing group
()untuk mengekstrak bagian tertentu dari kecocokan. Gunakan(?:...)untuk group non-capturing jika tidak perlu mengakses isinya.- Named group
(?<nama>...)membuat pola lebih terbaca dan akses dengannamedGroup('nama')lebih ekspresif dari indeks numerik.replaceAllMappeduntuk transformasi dinamis di mana string pengganti bergantung pada konten kecocokan — jauh lebih powerful darireplaceAlldengan string statis.- Deklarasikan
RegExpsebagaistatic finaldi kelas atau sebagai konstanta top-level — hindari membuat objek baru setiap kali fungsi dipanggil.- Jangan gunakan regex untuk HTML/XML — struktur nested tidak bisa dideskripsikan dengan bahasa reguler. Gunakan parser yang tepat.
- Hindari nested quantifier seperti
(a+)+— rentan catastrophic backtracking yang bisa membuat program hang untuk input tertentu.