Regex #
Regular expression adalah bahasa mini untuk mendeskripsikan pola teks — dan Ruby mengintegrasikannya secara mendalam ke dalam bahasa, bukan hanya sebagai library eksternal. Regex bisa digunakan langsung dengan operator =~, dalam case/when, sebagai argumen gsub, scan, split, dan puluhan method String lainnya. Yang membuat Ruby menonjol adalah kemudahan mengekstrak data dari teks terstruktur: named capture group (?<nama>pola) langsung menjadi variabel lokal dengan match, dan variabel global $1, $2, $~ menyimpan hasil match untuk diakses kapan saja. Artikel ini membahas regex dari fondasi hingga pola siap pakai untuk validasi data nyata di aplikasi Indonesia.
Sintaks Dasar dan Cara Membuat Regex #
Ada dua cara membuat regex di Ruby:
# 1. Literal — cara paling umum
pola = /halo/
email_pola = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
# 2. Regexp.new — untuk pola dinamis dari variabel
kata = "ruby"
pola_dinamis = Regexp.new(kata)
pola_dinamis = Regexp.new(kata, Regexp::IGNORECASE) # dengan flag
# Kapan pakai Regexp.new:
kata_cari = gets.chomp # input dari user
pola = Regexp.new(Regexp.escape(kata_cari)) # escape karakter khusus!
# Regexp.escape("a.b*c") => "a\\.b\\*c" (titik dan bintang di-escape)
Flag / Modifier #
Flag diletakkan setelah delimiter / penutup dan mengubah perilaku pencocokan:
teks = "Ruby adalah BAHASA yang Menyenangkan"
# i — case insensitive (abaikan huruf besar/kecil)
puts teks.match?(/ruby/i) # => true (tanpa i: false)
puts teks.match?(/bahasa/i) # => true
# m — multiline — titik (.) cocok dengan newline
multi = "baris pertama\nbaris kedua"
puts multi.match?(/pertama.kedua/) # => false (. tidak cocok \n)
puts multi.match?(/pertama.kedua/m) # => true (dengan m: cocok)
# x — extended — izinkan whitespace dan komentar dalam pola
pola_email = /
\A # awal string
[\w+\-.]+ # nama pengguna (huruf, angka, +, -, .)
@ # simbol @
[a-z\d\-.]+ # domain
\. # titik
[a-z]+ # TLD
\z # akhir string
/ix # i=case insensitive, x=extended
puts "[email protected]".match?(pola_email) # => true
# Kombinasi flag
puts "HALO\nDUNIA".match?(/halo.dunia/im) # => true (i + m)
Karakter Khusus dan Shorthand #
Regex menggunakan karakter khusus untuk mendeskripsikan kelas karakter:
# Karakter literal vs karakter khusus
# . ^ $ * + ? { } [ ] \ | ( ) ← karakter khusus, perlu di-escape dengan \
# Shorthand karakter
# \d — digit (0-9), ekuivalen dengan [0-9]
# \D — bukan digit, ekuivalen dengan [^0-9]
# \w — word character (huruf, digit, underscore), ekuivalen [a-zA-Z0-9_]
# \W — bukan word character
# \s — whitespace (spasi, tab, newline)
# \S — bukan whitespace
# . — karakter apapun kecuali newline (dengan flag m: termasuk newline)
teks = "Harga: Rp 15.000 (diskon 10%)"
puts teks.scan(/\d+/) # => ["15", "000", "10"]
puts teks.scan(/\d[\d.]+/) # => ["15.000", "10"] (angka dengan titik)
puts teks.scan(/\w+/) # => ["Harga", "Rp", "15", "000", "diskon", "10"]
puts teks.scan(/\s+/).length # => jumlah kelompok whitespace
# Character class — definisi kustom
/[aeiou]/ # hanya vokal kecil
/[a-z]/ # huruf kecil a sampai z
/[A-Za-z]/ # semua huruf
/[0-9a-f]/ # heksadesimal
/[^aeiou]/ # bukan vokal (^ di dalam [] berarti negasi)
/[.,;:!?]/ # tanda baca tertentu
# Escape karakter khusus untuk dicocokkan secara literal
/Rp\s\d+/ # "Rp " diikuti satu atau lebih digit
/www\.ruby-lang\.org/ # titik sebagai titik literal
Quantifier — Jumlah Pengulangan #
Quantifier menentukan berapa kali pola sebelumnya harus muncul:
# * — 0 atau lebih
# + — 1 atau lebih
# ? — 0 atau 1 (opsional)
# {n} — persis n kali
# {n,} — minimal n kali
# {n,m} — antara n dan m kali
teks = "aaabbc"
puts teks.match(/a+/)[0] # => "aaa"
puts teks.match(/b*/)[0] # => "" (0 atau lebih b di awal)
puts teks.match(/c?/)[0] # => ""
# Contoh praktis
/\d{4}/ # persis 4 digit — tahun
/\d{2,4}/ # 2 sampai 4 digit
/\+?62\d{9,12}/ # nomor telepon Indonesia (dengan atau tanpa +62)
/[A-Z]{2,3}/ # kode negara 2-3 huruf kapital
# Greedy vs Lazy quantifier
teks = "<b>tebal</b> dan <i>miring</i>"
puts teks.match(/<.+>/)[0] # => "<b>tebal</b> dan <i>miring</i>" (greedy!)
puts teks.match(/<.+?>/)[0] # => "<b>" (lazy — sesingkat mungkin)
puts teks.scan(/<.+?>/) # => ["<b>", "</b>", "<i>", "</i>"]
Quantifier secara default bersifat greedy — mencoba mencocokkan sebanyak mungkin karakter. Tambahkan?setelah quantifier (+?,*?,??) untuk membuatnya lazy — mencocokkan sesedikit mungkin. Pilihan yang salah antara greedy dan lazy adalah sumber bug regex yang umum.
Anchor — Posisi dalam String #
Anchor tidak mencocokkan karakter, tapi posisi di mana karakter berada:
# \A — awal string (seluruh string, bukan hanya baris)
# \z — akhir string
# ^ — awal baris (setiap baris dalam multiline)
# $ — akhir baris
# \b — word boundary (batas kata)
# \B — bukan word boundary
teks = "ruby on rails\nruby is great"
# \A dan \z — seluruh string
puts teks.match?(/\Aruby/) # => true (string dimulai "ruby")
puts teks.match?(/\Agreat/) # => false
puts teks.match?(/great\z/) # => true (string diakhiri "great")
# ^ dan $ — per baris (dengan flag multiline)
puts teks.scan(/^ruby/) # => ["ruby", "ruby"] (dua baris dimulai "ruby")
puts teks.scan(/rails$/) # => ["rails"]
# \b — word boundary
teks2 = "cat concatenate category"
puts teks2.scan(/\bcat\b/) # => ["cat"] (hanya kata "cat" persis)
puts teks2.scan(/cat/) # => ["cat", "cat", "cat"] (semua yang ada "cat")
# \A...\z — validasi format penuh (WAJIB untuk validasi input!)
"abc123".match?(/\A[a-z\d]+\z/) # => true (hanya huruf kecil dan digit)
"abc 123".match?(/\A[a-z\d]+\z/) # => false (ada spasi)
Perbedaan \A/\z vs ^/$:
\A → awal seluruh string (tidak terpengaruh newline)
\z → akhir seluruh string
^ → awal setiap baris (dalam string multiline)
$ → akhir setiap baris
Untuk validasi input: SELALU gunakan \A dan \z, bukan ^ dan $
Regex /^jahat$/ bisa di-bypass dengan "baik\njahat" karena ^ cocok
dengan awal baris, bukan awal string!
Capturing Group #
Group memungkinkan pengambilan bagian tertentu dari string yang cocok:
# Numbered capture groups — diakses dengan [1], [2], dst atau $1, $2
pola = /(\d{4})-(\d{2})-(\d{2})/
tanggal = "Lahir: 1990-03-15"
if m = pola.match(tanggal)
puts m[0] # => "1990-03-15" (seluruh match)
puts m[1] # => "1990" (group 1)
puts m[2] # => "03" (group 2)
puts m[3] # => "15" (group 3)
puts "Tahun: #{m[1]}, Bulan: #{m[2]}, Hari: #{m[3]}"
end
# Variabel global $1, $2 — tersedia setelah match berhasil
"harga: 15000" =~ /(\d+)/
puts $1 # => "15000"
puts $~[0] # => "15000" ($~ adalah MatchData terakhir)
puts $& # => "15000" (string yang cocok)
puts $` # => "harga: " (string sebelum match)
puts $' # => "" (string setelah match)
Named Capture Group #
Named capture group adalah cara yang jauh lebih ekspresif dan mudah dibaca:
# (?<nama>pola) — named capture group
pola_tanggal = /(?<tahun>\d{4})-(?<bulan>\d{2})-(?<hari>\d{2})/
if m = pola_tanggal.match("1990-03-15")
puts m[:tahun] # => "1990"
puts m[:bulan] # => "03"
puts m[:hari] # => "15"
end
# Named capture otomatis jadi variabel lokal saat digunakan dengan =~
pola_log = /\[(?<level>\w+)\] (?<pesan>.+)/
"[ERROR] Koneksi database gagal" =~ pola_log
puts level # => "ERROR" (langsung jadi variabel lokal!)
puts pesan # => "Koneksi database gagal"
# Contoh nyata: parsing log dengan named capture
pola_akses = /
(?<ip>[\d.]+) # IP address
\s-\s-\s
\[(?<waktu>[^\]]+)\] # timestamp dalam [ ]
\s"(?<method>\w+) # HTTP method
\s(?<path>[^\s"]+) # URL path
/x
log = '192.168.1.1 - - [15/Aug/2024:14:30:05] "GET /api/users'
if m = pola_akses.match(log)
puts "IP: #{m[:ip]}, Method: #{m[:method]}, Path: #{m[:path]}"
end
Non-Capturing Group #
(?:pola) membuat group untuk pengurutan atau alternasi tanpa menangkap hasilnya:
# (?:pola) — group tanpa capture
pola = /(?:Mr|Mrs|Dr)\. (\w+)/
if m = pola.match("Dr. Santoso")
puts m[1] # => "Santoso" (bukan "Dr." karena (?:) tidak di-capture)
puts m[0] # => "Dr. Santoso"
end
# Alternasi dalam group
/(?:png|jpg|jpeg|gif|webp)/ # format gambar
/(?:https?|ftp):\/\// # protokol URL
Lookahead dan Lookbehind #
Lookahead dan lookbehind adalah zero-width assertion — mereka memeriksa konteks tanpa mengonsumsi karakter:
# Positive lookahead (?=pola) — cocok jika DIIKUTI oleh pola
# Negative lookahead (?!pola) — cocok jika TIDAK diikuti oleh pola
# Positive lookbehind (?<=pola) — cocok jika DIDAHULUI oleh pola
# Negative lookbehind (?<!pola) — cocok jika TIDAK didahului oleh pola
teks = "harga: Rp 15000, diskon: Rp 2000"
# Ambil angka yang didahului "Rp "
angka = teks.scan(/(?<=Rp )\d+/)
puts angka.inspect # => ["15000", "2000"]
# Ambil kata yang diikuti tanda titik dua
label = teks.scan(/\w+(?=:)/)
puts label.inspect # => ["harga", "diskon"]
# Validasi password yang mengandung huruf besar DAN angka
def password_kuat?(pwd)
pwd.match?(/\A(?=.*[A-Z])(?=.*\d).{8,}\z/)
end
puts password_kuat?("rahasia") # => false (tidak ada huruf besar dan angka)
puts password_kuat?("Rahasia1") # => true
puts password_kuat?("rahasia1") # => false (tidak ada huruf besar)
puts password_kuat?("RAHASIA") # => false (tidak ada angka)
puts password_kuat?("R1") # => false (kurang dari 8 karakter)
Method Utama yang Menggunakan Regex #
match dan match? #
teks = "Nomor: 08123456789"
pola = /0\d{9,11}/
# match — kembalikan MatchData atau nil
hasil = teks.match(pola)
puts hasil&.[](0) # => "08123456789"
puts hasil.nil? # => false
# match? — kembalikan boolean saja (lebih cepat, tidak alokasi MatchData)
puts teks.match?(pola) # => true
# Lebih idiomatik dari =~
if teks.match?(pola)
puts "Nomor telepon ditemukan"
end
scan — Cari Semua Match #
teks = "Email: [email protected], [email protected], dan [email protected]"
pola_email = /[\w.+-]+@[\w-]+\.[a-z.]+/i
semua_email = teks.scan(pola_email)
puts semua_email.inspect
# => ["[email protected]", "[email protected]", "[email protected]"]
# scan dengan group — kembalikan array of arrays
teks2 = "Rina:85, Budi:92, Citra:78"
data = teks2.scan(/(\w+):(\d+)/)
puts data.inspect
# => [["Rina", "85"], ["Budi", "92"], ["Citra", "78"]]
# Konversi langsung ke Hash
nilai = teks2.scan(/(\w+):(\d+)/).to_h { |nama, skor| [nama, skor.to_i] }
puts nilai.inspect # => {"Rina"=>85, "Budi"=>92, "Citra"=>78}
gsub dan sub — Ganti dengan Regex #
teks = "harga: 15000, stok: 200, berat: 1500"
# sub — ganti PERTAMA saja
puts teks.sub(/\d+/, "XXX")
# => "harga: XXX, stok: 200, berat: 1500"
# gsub — ganti SEMUA
puts teks.gsub(/\d+/, "XXX")
# => "harga: XXX, stok: XXX, berat: XXX"
# gsub dengan referensi back-reference \1
puts "2024-08-15".gsub(/(\d{4})-(\d{2})-(\d{2})/, '\3/\2/\1')
# => "15/08/2024" (ubah format tanggal)
# gsub dengan named capture
puts "John Doe".gsub(/(?<depan>\w+) (?<belakang>\w+)/, '\k<belakang>, \k<depan>')
# => "Doe, John"
# gsub dengan blok — paling fleksibel
harga_str = "laptop: 15000000, mouse: 350000"
hasil = harga_str.gsub(/\d+/) do |angka|
"Rp #{angka.to_i.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\1.').reverse}"
end
puts hasil
# => "laptop: Rp 15.000.000, mouse: Rp 350.000"
# gsub untuk sanitasi input
def bersihkan_html(teks)
teks
.gsub(/<[^>]+>/, "") # hapus tag HTML
.gsub(/&/, "&") # decode HTML entity
.gsub(/</, "<")
.gsub(/>/, ">")
.gsub(/\s+/, " ") # normalkan whitespace
.strip
end
split — Pecah String dengan Regex #
# split dengan string biasa
"a,b,c".split(",") # => ["a", "b", "c"]
# split dengan regex — lebih fleksibel
"a , b , c".split(/\s*,\s*/) # => ["a", "b", "c"] (abaikan spasi di sekitar koma)
"satu dua tiga".split(/\s+/) # => ["satu", "dua", "tiga"] (satu atau lebih spasi)
# Parsing CSV sederhana
baris_csv = "Rina,28,Bandung,aktif"
kolom = baris_csv.split(",")
puts kolom.inspect # => ["Rina", "28", "Bandung", "aktif"]
# Pisahkan kalimat
kalimat = "Ini kalimat pertama. Ini kedua! Dan ketiga?"
kalimat.split(/(?<=[.!?])\s+/)
# => ["Ini kalimat pertama.", "Ini kedua!", "Dan ketiga?"]
Pola Regex Umum untuk Validasi Indonesia #
Berikut kumpulan pola regex siap pakai yang relevan untuk aplikasi berbahasa Indonesia:
# Email
POLA_EMAIL = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i.freeze
# Nomor telepon Indonesia — mendukung 08xx, +628xx, 628xx
POLA_TELEPON_ID = /\A(\+62|62|0)[0-9]{8,12}\z/.freeze
# NIK (Nomor Induk Kependudukan) — 16 digit
POLA_NIK = /\A\d{16}\z/.freeze
# NPWP — format: XX.XXX.XXX.X-XXX.XXX
POLA_NPWP = /\A\d{2}\.\d{3}\.\d{3}\.\d-\d{3}\.\d{3}\z/.freeze
# Kode pos Indonesia — 5 digit
POLA_KODE_POS = /\A[1-9]\d{4}\z/.freeze
# Plat nomor kendaraan Indonesia
POLA_PLAT = /\A[A-Z]{1,2}\s?\d{1,4}\s?[A-Z]{1,3}\z/i.freeze
# Tanggal format Indonesia DD/MM/YYYY atau DD-MM-YYYY
POLA_TANGGAL_ID = /\A(0?[1-9]|[12]\d|3[01])[\/\-](0?[1-9]|1[0-2])[\/\-]\d{4}\z/.freeze
# URL
POLA_URL = /\Ahttps?:\/\/[\w\-]+(\.[\w\-]+)+([\/\?#]\S*)?\z/i.freeze
# Slug (URL-friendly string)
POLA_SLUG = /\A[a-z0-9]+(?:-[a-z0-9]+)*\z/.freeze
# Harga dalam Rupiah (opsional Rp dan titik sebagai pemisah ribuan)
POLA_HARGA = /\A(?:Rp\.?\s?)?\d{1,3}(?:\.\d{3})*(?:,\d{2})?\z/.freeze
# Penggunaan
module Validator
def self.email_valid?(email)
email.to_s.match?(POLA_EMAIL)
end
def self.telepon_valid?(nomor)
nomor.to_s.gsub(/[\s\-()]/, "").match?(POLA_TELEPON_ID)
end
def self.nik_valid?(nik)
nik.to_s.match?(POLA_NIK)
end
def self.url_valid?(url)
url.to_s.match?(POLA_URL)
end
end
puts Validator.email_valid?("[email protected]") # => true
puts Validator.email_valid?("bukan-email") # => false
puts Validator.telepon_valid?("08123456789") # => true
puts Validator.telepon_valid?("+6281234567890") # => true
puts Validator.telepon_valid?("123") # => false
puts Validator.nik_valid?("3201234501010001") # => true
puts Validator.url_valid?("https://ruby-lang.org") # => true
Debugging dan Performa Regex #
# Uji regex di IRB atau dengan puts debug
pola = /(?<tahun>\d{4})-(?<bulan>\d{2})-(?<hari>\d{2})/
teks = "Tanggal: 2024-08-15"
m = pola.match(teks)
puts m.inspect # tampilkan seluruh MatchData
puts m.named_captures.inspect # => {"tahun"=>"2024", "bulan"=>"08", "hari"=>"15"}
puts m.pre_match # => "Tanggal: " (sebelum match)
puts m.post_match # => "" (setelah match)
# Performa — match? lebih cepat dari match untuk pengecekan boolean
require 'benchmark'
teks = "[email protected]"
pola = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
Benchmark.bm(15) do |x|
x.report("match:") { 100_000.times { teks.match(pola) } }
x.report("match?:") { 100_000.times { teks.match?(pola) } }
x.report("=~:") { 100_000.times { teks =~ pola } }
end
# match? biasanya 2-3x lebih cepat dari match karena tidak alokasi MatchData
# Hindari katastrofik backtracking
# BERBAHAYA: pola seperti /(\w+)+\z/ pada input panjang bisa sangat lambat
# AMAN: tulis pola yang lebih spesifik dan tidak ambigu
Panduan membuat regex yang baik:
✓ Mulai dengan \A dan akhiri dengan \z untuk validasi penuh
✓ Gunakan named capture (?<nama>pola) untuk keterbacaan
✓ Gunakan flag x untuk regex panjang — izinkan komentar
✓ Gunakan match? bukan match jika hanya perlu boolean
✓ Escape karakter khusus dengan Regexp.escape untuk input user
✓ Test regex dengan beragam input valid DAN invalid
✗ Hindari pola greedy yang bisa menyebabkan backtracking berlebihan
✗ Jangan gunakan ^ dan $ untuk validasi — gunakan \A dan \z
✗ Jangan buat regex yang terlalu panjang tanpa flag x dan komentar
Ringkasan #
/pola/untuk literal,Regexp.newuntuk dinamis — selalu gunakanRegexp.escapejika membangun pola dari input pengguna agar karakter khusus tidak diinterpretasikan sebagai sintaks regex.\Adan\z, bukan^dan$— untuk validasi input penuh, selalu gunakan anchor\A(awal string) dan\z(akhir string). Pola dengan^/$bisa di-bypass dengan input multiline.- Named capture
(?<nama>pola)jauh lebih ekspresif dari numbered group(\d+)— hasil capture bisa diakses dengan nama yang bermakna, bukan angka indeks.match?lebih cepat darimatch— gunakanmatch?jika hanya perlu tahu apakah cocok atau tidak, karena tidak mengalokasikan objekMatchData.- Greedy vs Lazy quantifier —
+dan*adalah greedy (cocok sebanyak mungkin); tambahkan?(+?,*?) untuk membuatnya lazy (sesedikit mungkin).- Lookahead
(?=)dan lookbehind(?<=)untuk konteks tanpa mengonsumsi karakter — berguna untuk mengambil angka yang didahului simbol tertentu.scandengan group mengembalikan array of arrays — bisa langsung dikonversi ke Hash dengan.to_h.gsubdengan blok paling fleksibel — setiap match bisa diproses dengan logika Ruby penuh, bukan hanya penggantian string sederhana.- Flag
xuntuk regex kompleks — izinkan whitespace dan komentar inline sehingga pola panjang tetap mudah dibaca dan dipelihara.- Simpan pola validasi sebagai konstanta
freeze— hindari mengompilasi ulang pola yang sama berulang kali dalam hot path.