Regex

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(/&amp;/, "&")          # decode HTML entity
    .gsub(/&lt;/, "<")
    .gsub(/&gt;/, ">")
    .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.new untuk dinamis — selalu gunakan Regexp.escape jika membangun pola dari input pengguna agar karakter khusus tidak diinterpretasikan sebagai sintaks regex.
  • \A dan \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 dari match — gunakan match? jika hanya perlu tahu apakah cocok atau tidak, karena tidak mengalokasikan objek MatchData.
  • 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.
  • scan dengan group mengembalikan array of arrays — bisa langsung dikonversi ke Hash dengan .to_h.
  • gsub dengan blok paling fleksibel — setiap match bisa diproses dengan logika Ruby penuh, bukan hanya penggantian string sederhana.
  • Flag x untuk 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.

← Sebelumnya: Date & Time   Berikutnya: RubyGems →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact