kodingup.camp

Beranda/Advanced JavaScript: Interaksi Browser

Teknik Pencocokan Pola

Modul 1 dari 57 menit bacaPattern Matching Techniques

Isi modul

    Kenapa ini penting

    Bayangkan kamu kerja di bagian pendaftaran sebuah aplikasi. Setiap hari masuk ribuan data: email, nomor HP, password, kode pos, tanggal lahir. Sebagian diketik asal, misalnya budi@gmail tanpa .com, nomor HP 0812-abc, atau password 123. Kalau data rusak lolos ke database, akibatnya baru terasa belakangan: email konfirmasi tidak terkirim, OTP nyasar, akun gampang dibobol.

    Kamu bisa saja memeriksa semuanya dengan if dan indexOf berlapis, tapi kodenya cepat jadi panjang dan rapuh. Di sinilah Regular Expression (disingkat regex atau RegExp) berguna. Regex adalah bahasa mini untuk mendeskripsikan pola teks. Satu baris regex bisa menggantikan puluhan baris if. Regex dipakai di mana-mana: validasi form, mencari dan mengganti teks di editor kode (VS Code punya tombol .* untuk mode regex), membersihkan data CSV, sampai memecah log server.

    Analogi sehari-hari: regex itu seperti cetakan kue. Kamu tidak mendeskripsikan satu kue tertentu, tapi bentuknya. Adonan apa pun yang pas di cetakan dianggap "cocok". Pola \d{4} artinya "empat digit berturut-turut", jadi 2026, 0812, dan 9999 semuanya pas di cetakan itu.

    Satu peringatan sejak awal: regex mudah ditulis tapi sulit dibaca. Kuncinya adalah mulai dari pola kecil, uji satu per satu, lalu gabungkan. Situs seperti regex101.com (pilih flavor ECMAScript/JavaScript) sangat membantu karena menjelaskan setiap bagian pola dan menyorot hasil cocoknya secara langsung.

    Konsep inti

    Dua cara membuat regex: literal dan RegExp

    Di JavaScript, regex adalah object. Ada dua cara membuatnya:

    1. Literal: ditulis di antara dua garis miring, misalnya /kucing/i. Cara ini paling sering dipakai karena ringkas, dan pola diproses sekali saat kode dimuat.
    2. Constructor RegExp: new RegExp("kucing", "i"). Polanya berupa string, jadi kamu bisa menyusunnya secara dinamis, misalnya dari kata kunci yang diketik pengguna.

    Ada jebakan di constructor: karena polanya string, backslash harus ditulis dua kali. Literal /\d+/ sama dengan new RegExp("\\d+"). Kalau kamu menulis new RegExp("\d+"), JavaScript membaca "\d" sebagai huruf d biasa, dan polamu diam-diam salah.

    Flag: pengubah perilaku

    Flag ditulis setelah garis miring penutup. Yang paling sering kamu pakai:

    Flag Nama Arti
    g global Cari semua kecocokan, bukan hanya yang pertama
    i ignore case Huruf besar dan kecil dianggap sama
    m multiline ^ dan $ berlaku per baris, bukan per seluruh teks
    u unicode Memproses teks sebagai karakter Unicode utuh (emoji, aksara non-Latin) dan menyalakan \p{...}

    Flag bisa digabung, misalnya /halo/gi. Flag u sering dilupakan, padahal penting kalau teksmu mengandung emoji. Tanpa u, emoji seperti 😀 dihitung sebagai dua "potongan" (surrogate pair), sehingga titik . hanya menangkap separuhnya.

    Character class: "satu karakter dari kelompok ini"

    Character class mewakili satu karakter yang memenuhi syarat tertentu.

    Pola Cocok dengan
    [abc] salah satu dari a, b, atau c
    [a-z] satu huruf kecil a sampai z
    [A-Za-z0-9] satu huruf atau angka
    [^0-9] satu karakter yang bukan angka (tanda ^ di dalam kurung siku artinya negasi)
    \d satu digit, sama dengan [0-9]
    \w satu "karakter kata": huruf Latin, angka, atau underscore
    \s satu whitespace: spasi, tab, baris baru
    \D, \W, \S kebalikan dari \d, \w, \s
    . karakter apa saja kecuali baris baru

    Perhatikan bahwa \w tidak mengenali huruf seperti é atau aksara non-Latin. Kalau butuh "huruf dari bahasa apa pun", pakai \p{L} dengan flag u.

    Anchor: posisi, bukan karakter

    ^ artinya awal teks, $ artinya akhir teks. Anchor sangat penting untuk validasi. Pola /\d{4}/ akan cocok dengan "abc1234xyz" karena di dalamnya ada empat digit. Kalau maksudmu "seluruh isi harus empat digit", tulis /^\d{4}$/. Ada juga \b (word boundary), batas antara karakter kata dan bukan-kata, berguna untuk mencari kata utuh.

    Quantifier: berapa kali

    Quantifier ditempel setelah sesuatu untuk menentukan jumlah pengulangannya.

    Quantifier Arti
    * 0 kali atau lebih
    + 1 kali atau lebih
    ? 0 atau 1 kali (opsional)
    {3} tepat 3 kali
    {2,5} 2 sampai 5 kali
    {8,} minimal 8 kali

    Secara default quantifier bersifat greedy (rakus): ia mengambil sebanyak mungkin. Pada teks <b>tebal</b>, pola /<.+>/ akan menangkap seluruh string, bukan hanya <b>. Tambahkan ? setelah quantifier agar jadi lazy (secukupnya): /<.+?>/ hanya menangkap <b>.

    Group dan named group

    Tanda kurung ( ) punya dua fungsi. Pertama, mengelompokkan: (ha)+ cocok dengan ha, haha, hahaha. Kedua, menangkap (capture): bagian yang cocok di dalam kurung disimpan dan bisa kamu ambil nanti.

    Kalau hanya butuh pengelompokan tanpa menyimpan, pakai non-capturing group (?: ). Named group (?<nama>...) memberi nama pada tangkapan, jadi kamu tidak perlu mengingat "ini grup nomor berapa". Hasilnya bisa diakses lewat match.groups.nama, dan kode jadi jauh lebih mudah dibaca.

    Ada juga lookahead (?=...): "pastikan di depan ada ini, tapi jangan ikut ditangkap". Lookahead adalah trik standar untuk validasi password, misalnya "harus ada minimal satu angka di mana pun posisinya".

    Tanda | artinya "atau": /kucing|anjing/ cocok dengan salah satunya.

    Method yang perlu kamu kuasai

    Method Dipanggil pada Mengembalikan
    regex.test(str) regex true / false, cocok untuk validasi
    str.match(regex) string Tanpa g: detail kecocokan pertama (termasuk grup). Dengan g: array semua teks yang cocok. Tidak ada yang cocok: null
    str.matchAll(regex) string Iterator semua kecocokan lengkap dengan grup. Regex wajib memakai g
    str.replace(regex, pengganti) string String baru. Dengan g semua diganti, tanpa g hanya yang pertama
    str.split(regex) string Array potongan teks

    Di dalam string pengganti replace, $1 merujuk grup pertama dan $<nama> merujuk named group. Pengganti juga bisa berupa function, yang dipanggil untuk setiap kecocokan.

    Contoh kode

    Dasar: membuat regex dan flag

    // Literal vs constructor
    const pola1 = /kopi/i;
    const kataKunci = "kopi";
    const pola2 = new RegExp(kataKunci, "i"); // dinamis, dari variabel
    
    console.log(pola1.test("Saya suka KOPI susu")); // → true
    console.log(pola2.test("Teh manis"));           // → false
    
    // Backslash di constructor harus dobel
    const angkaA = /\d+/;
    const angkaB = new RegExp("\\d+");
    console.log(angkaA.test("abc123"), angkaB.test("abc123")); // → true true
    
    // Flag g: semua kecocokan
    const kalimat = "Kopi pagi, kopi siang, KOPI malam";
    console.log(kalimat.match(/kopi/gi)); // → [ 'Kopi', 'kopi', 'KOPI' ]
    console.log(kalimat.match(/kopi/)[0], kalimat.match(/kopi/).index); // → 'kopi' 11 (hanya yang pertama cocok persis)
    
    // Flag m: ^ berlaku di setiap baris
    const daftar = "apel\nbayam\nalpukat";
    console.log(daftar.match(/^a\w+/gm)); // → [ 'apel', 'alpukat' ]
    console.log(daftar.match(/^a\w+/g));  // → [ 'apel' ]  (tanpa m, ^ hanya awal teks)
    
    // Flag u: emoji dihitung sebagai satu karakter
    console.log(/^.$/.test("😀"));  // → false (tanpa u, emoji = 2 potongan)
    console.log(/^.$/u.test("😀")); // → true
    

    Character class, quantifier, greedy vs lazy

    const teks = "Order #A12 dikirim ke kode pos 40115 pada 2026-10-23";
    
    console.log(teks.match(/\d{5}/)[0]);        // → '40115'
    console.log(teks.match(/#[A-Z]\d+/)[0]);    // → '#A12'
    console.log(teks.match(/\d+/g));            // → [ '12', '40115', '2026', '10', '23' ]
    
    // Anchor: validasi seluruh isi
    console.log(/\d{4}/.test("abc1234xyz"));    // → true  (ada 4 digit di dalamnya)
    console.log(/^\d{4}$/.test("abc1234xyz"));  // → false (isinya bukan cuma 4 digit)
    console.log(/^\d{4}$/.test("1234"));        // → true
    
    // Greedy vs lazy
    const html = "<b>tebal</b> dan <i>miring</i>";
    console.log(html.match(/<.+>/)[0]);   // → '<b>tebal</b> dan <i>miring</i>' (rakus)
    console.log(html.match(/<.+?>/g));    // → [ '<b>', '</b>', '<i>', '</i>' ]  (secukupnya)
    

    Group, named group, dan matchAll

    const tanggal = "Rilis: 2026-10-23";
    
    // Capture group biasa: diakses lewat index
    const m1 = tanggal.match(/(\d{4})-(\d{2})-(\d{2})/);
    console.log(m1[0], m1[1], m1[2], m1[3]); // → '2026-10-23' '2026' '10' '23'
    
    // Named group: lebih jelas
    const m2 = tanggal.match(/(?<tahun>\d{4})-(?<bulan>\d{2})-(?<hari>\d{2})/);
    console.log(m2.groups.hari, m2.groups.bulan, m2.groups.tahun); // → '23' '10' '2026'
    
    // matchAll: semua kecocokan + grupnya. Wajib flag g.
    const harga = "Kopi Rp18000, Roti Rp12500, Air Rp5000";
    const polaHarga = /(?<nama>\w+) Rp(?<nominal>\d+)/g;
    
    for (const m of harga.matchAll(polaHarga)) {
      console.log(`${m.groups.nama}: ${Number(m.groups.nominal)}`);
    }
    // → Kopi: 18000
    // → Roti: 12500
    // → Air: 5000
    
    // Mengubah jadi array object, lalu dihitung totalnya
    const items = [...harga.matchAll(polaHarga)].map((m) => ({
      nama: m.groups.nama,
      nominal: Number(m.groups.nominal),
    }));
    const total = items.reduce((jml, it) => jml + it.nominal, 0);
    console.log(total); // → 35500
    

    replace: membersihkan dan memformat teks

    // Ganti format tanggal YYYY-MM-DD menjadi DD/MM/YYYY pakai named group
    const iso = "2026-10-23";
    console.log(iso.replace(/(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/, "$<d>/$<m>/$<y>")); // → '23/10/2026'
    
    // Rapikan spasi berlebih
    const berantakan = "  Halo     dunia   JS  ";
    console.log(berantakan.trim().replace(/\s+/g, " ")); // → 'Halo dunia JS'
    
    // Ambil digit saja dari nomor HP yang diketik dengan spasi/strip
    console.log("0812-3456 7890".replace(/\D/g, "")); // → '081234567890'
    
    // Pengganti berupa function: format angka jadi Rupiah
    const struk = "Total: 1500000 dan diskon 25000";
    const rapi = struk.replace(/\d+/g, (angka) => "Rp" + Number(angka).toLocaleString("id-ID"));
    console.log(rapi); // → 'Total: Rp1.500.000 dan diskon Rp25.000'
    
    // Membuat slug URL dari judul artikel
    function slugify(judul) {
      return judul
        .toLowerCase()
        .trim()
        .replace(/[^a-z0-9\s-]/g, "") // buang tanda baca
        .replace(/\s+/g, "-")         // spasi jadi strip
        .replace(/-+/g, "-");         // strip dobel jadi satu
    }
    console.log(slugify("Belajar JS: DOM & Event, Gampang!")); // → 'belajar-js-dom-event-gampang'
    
    // Sensor kata kasar (kata utuh, tidak peduli huruf besar/kecil)
    const kasar = /\b(bodoh|goblok)\b/gi;
    console.log("Dasar Bodoh!".replace(kasar, (k) => "*".repeat(k.length))); // → 'Dasar *****!'
    

    Validasi dunia nyata: email, nomor HP Indonesia, password

    // 1. Email: cek bentuk umum "sesuatu@domain.tld".
    // Validasi email 100% sempurna dengan regex itu mustahil; cara pasti adalah kirim email verifikasi.
    const polaEmail = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
    
    console.log(polaEmail.test("budi@gmail.com"));      // → true
    console.log(polaEmail.test("budi.s@kantor.co.id")); // → true
    console.log(polaEmail.test("budi@gmail"));          // → false (tidak ada .tld)
    console.log(polaEmail.test("budi @gmail.com"));     // → false (ada spasi)
    
    // 2. Nomor HP Indonesia: diawali 08 / 628 / +628, total 10-13 digit kalau pakai awalan 0.
    const polaHP = /^(?:\+62|62|0)8[1-9]\d{7,10}$/;
    
    function cekHP(input) {
      const bersih = input.replace(/[\s-]/g, ""); // toleransi spasi & strip
      return polaHP.test(bersih);
    }
    console.log(cekHP("081234567890"));      // → true
    console.log(cekHP("+62 812-3456-7890")); // → true
    console.log(cekHP("628123456789"));      // → true
    console.log(cekHP("0212345678"));        // → false (nomor rumah Jakarta, bukan HP)
    console.log(cekHP("08123"));             // → false (terlalu pendek)
    
    // Normalisasi ke format 62xxx sebelum disimpan atau dikirim ke API WhatsApp
    function normalisasiHP(input) {
      return input.replace(/[\s-]/g, "").replace(/^(?:\+62|0)/, "62");
    }
    console.log(normalisasiHP("0812-3456-7890")); // → '6281234567890'
    
    // 3. Password kuat: min 8 karakter, ada huruf kecil, huruf besar, angka, simbol.
    // Tiap (?=...) adalah lookahead: "pastikan ada X di suatu tempat".
    const polaPassword = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z0-9]).{8,}$/;
    
    console.log(polaPassword.test("rahasia"));     // → false
    console.log(polaPassword.test("Rahasia123"));  // → false (tanpa simbol)
    console.log(polaPassword.test("Rahasia123!")); // → true
    
    // Versi ramah pengguna: beri tahu syarat mana yang belum terpenuhi
    function cekPassword(pw) {
      const aturan = [
        { pola: /.{8,}/, pesan: "minimal 8 karakter" },
        { pola: /[a-z]/, pesan: "ada huruf kecil" },
        { pola: /[A-Z]/, pesan: "ada huruf besar" },
        { pola: /\d/, pesan: "ada angka" },
        { pola: /[^A-Za-z0-9]/, pesan: "ada simbol" },
      ];
      return aturan.filter((a) => !a.pola.test(pw)).map((a) => a.pesan);
    }
    console.log(cekPassword("abc"));          // → [ 'minimal 8 karakter', 'ada huruf besar', 'ada angka', 'ada simbol' ]
    console.log(cekPassword("Rahasia123!"));  // → []
    

    Versi cekPassword yang kedua jauh lebih baik untuk pengguna. Satu regex raksasa hanya bisa bilang "salah", sedangkan daftar aturan kecil bisa bilang "kurang huruf besar". Pesan seperti itu yang membuat orang tidak kesal saat mengisi form.

    Mencari dengan kata kunci dari pengguna

    // Karakter spesial di input pengguna harus di-escape sebelum masuk RegExp
    function escapeRegex(teks) {
      return teks.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& = seluruh teks yang cocok
    }
    
    function sorot(kalimat, kata) {
      const pola = new RegExp(escapeRegex(kata), "gi");
      return kalimat.replace(pola, (cocok) => `[${cocok}]`);
    }
    console.log(sorot("Harga C++ dan c++ naik", "c++")); // → 'Harga [C++] dan [c++] naik'
    

    Tanpa escapeRegex, input c++ akan membuat error karena + setelah + tidak valid sebagai pola.

    Kesalahan umum

    1. Lupa anchor saat validasi. /\d{10,13}/.test("abc0812345678901xyz") bernilai true. Untuk validasi isi form, hampir selalu bungkus pola dengan ^...$.
    2. Memakai test() dengan flag g berulang kali. Regex ber-flag g menyimpan posisi terakhir di properti lastIndex. Akibatnya const r = /a/g; r.test("a"); r.test("a"); menghasilkan true lalu false. Untuk validasi, jangan pakai g. Kalau terpaksa, reset dengan r.lastIndex = 0.
    3. Backslash tunggal di new RegExp. new RegExp("\d") sama dengan /d/. Tulis "\\d", atau pakai literal kalau polanya tidak dinamis.
    4. Mengira match() dengan g masih memberi grup. Dengan g, match() hanya mengembalikan array teks yang cocok, tanpa grup. Butuh grup dari semua kecocokan? Pakai matchAll().
    5. Tidak menangani null. "abc".match(/\d+/)[0] langsung error karena hasilnya null. Cek dulu, atau pakai optional chaining: "abc".match(/\d+/)?.[0] ?? "tidak ada".

    Latihan

    1. Mudah, Buat function hanyaAngka(str) yang mengembalikan true jika string hanya berisi digit (minimal satu digit), selain itu false.
    2. Sedang, Diberikan teks "Hubungi kami: 0812-1111-2222 atau +62 857 3333 4444, kantor 021-555-1234". Ambil semua nomor HP (bukan nomor kantor) lalu kembalikan dalam format 62xxx tanpa spasi atau strip.
    3. Menantang, Buat function parseLog(log) yang menerima beberapa baris log seperti "[2026-10-23 08:15:02] ERROR login gagal user=budi" dan mengembalikan array object { tanggal, jam, level, pesan } hanya untuk baris ber-level ERROR atau WARN. Gunakan named group dan matchAll.
    Lihat solusi
    // 1. Mudah
    const hanyaAngka = (str) => /^\d+$/.test(str);
    console.log(hanyaAngka("12345")); // → true
    console.log(hanyaAngka("12a45")); // → false
    console.log(hanyaAngka(""));      // → false
    
    // 2. Sedang
    const teks = "Hubungi kami: 0812-1111-2222 atau +62 857 3333 4444, kantor 021-555-1234";
    // Tangkap kandidat: awalan HP diikuti digit/spasi/strip
    const kandidat = teks.match(/(?:\+62|62|0)\s?8[\d\s-]{8,16}\d/g) ?? [];
    const nomorHP = kandidat.map((n) => n.replace(/[\s-]/g, "").replace(/^(?:\+62|0)/, "62"));
    console.log(nomorHP); // → [ '6281211112222', '6285733334444' ]
    
    // 3. Menantang
    function parseLog(log) {
      const pola =
        /^\[(?<tanggal>\d{4}-\d{2}-\d{2}) (?<jam>\d{2}:\d{2}:\d{2})\] (?<level>ERROR|WARN) (?<pesan>.+)$/gm;
      return [...log.matchAll(pola)].map((m) => ({ ...m.groups }));
    }
    
    const log = `[2026-10-23 08:15:02] ERROR login gagal user=budi
    [2026-10-23 08:16:10] INFO user=sari masuk
    [2026-10-23 08:17:45] WARN disk tersisa 10%`;
    
    console.log(parseLog(log));
    // → [
    //   { tanggal: '2026-10-23', jam: '08:15:02', level: 'ERROR', pesan: 'login gagal user=budi' },
    //   { tanggal: '2026-10-23', jam: '08:17:45', level: 'WARN', pesan: 'disk tersisa 10%' }
    // ]
    

    Pada soal 2, bagian \s? setelah awalan menampung spasi di +62 857. Kunci soal 3 adalah flag m, sehingga ^ dan $ berlaku per baris, dan alternatif (ERROR|WARN) yang otomatis membuang baris INFO.

    Ringkasan

    Kuis pemahaman

    Pilih satu jawaban per soal. Penjelasan muncul setelah kamu menjawab.