Bab 9 dari Eloquent JavaScript. Sebelumnya: 08 Bug dan Error. Berikutnya: 10 Modul.

Beberapa orang, waktu dihadapkan sama masalah, mikir ‘Aku tahu, aku bakal pakai regular expression.’ Sekarang mereka punya dua masalah.

— Jamie Zawinski

Waktu kamu motong melawan serat kayu, butuh banyak tenaga. Waktu kamu memprogram melawan serat masalahnya, butuh banyak kode.

— Master Yuan-Ma, The Book of Programming

Ilustrasi sistem rel kereta yang mewakili struktur sintaksis regular expression

Alat dan teknik pemrograman bertahan dan menyebar dengan cara yang kacau dan evolusioner. Gak selalu yang terbaik atau paling brilian yang menang, tapi yang berfungsi cukup baik di ceruk yang tepat atau yang kebetulan terintegrasi sama teknologi lain yang sukses.

Di bab ini, saya bakal ngebahas salah satu alat kaya gitu, regular expression. Regular expression adalah cara buat menggambarkan pola di data string. Mereka membentuk bahasa kecil terpisah yang jadi bagian dari JavaScript dan banyak bahasa dan sistem lain.

Regular expression itu sangat canggung sekaligus sangat berguna. Sintaksnya samar dan interface pemrograman yang disediain JavaScript buat mereka kikuk. Tapi mereka alat yang kuat buat meriksa dan ngeproses string. Paham regular expression dengan benar bakal bikin kamu programmer yang lebih efektif.

Bikin Regular Expression

Regular expression adalah jenis objek. Dia bisa dibangun dengan constructor RegExp atau ditulis sebagai nilai literal dengan ngebungkus pola di antara karakter garis miring (/).

let re1 = new RegExp("abc");
let re2 = /abc/;

Kedua objek regular expression itu mewakili pola yang sama: karakter a diikuti b diikuti c.

Waktu pakai constructor RegExp, polanya ditulis sebagai string normal, jadi aturan biasa berlaku buat backslash.

Notasi kedua, di mana polanya muncul di antara karakter garis miring, memperlakukan backslash agak beda. Pertama, karena garis miring ngakhirin polanya, kita perlu naruh backslash sebelum garis miring apa pun yang kita mau jadi bagian dari pola. Selain itu, backslash yang bukan bagian dari kode karakter khusus (kaya \n) bakal dipertahankan, bukannya diabaikan kaya di string, dan ngubah makna polanya. Beberapa karakter, kaya tanda tanya dan tanda plus, punya makna khusus di regular expression dan harus didahului backslash kalau dimaksudkan buat mewakili karakternya sendiri.

let aPlus = /A\+/;

Ngetes Kecocokan

Objek regular expression punya sejumlah method. Yang paling sederhana adalah test. Kalau kamu ngasihnya string, dia bakal ngembaliin Boolean yang ngasih tahu kamu apakah stringnya berisi kecocokan dari pola di ekspresinya.

console.log(/abc/.test("abcde"));
// → true
console.log(/abc/.test("abxde"));
// → false

Regular expression yang cuma terdiri dari karakter non-khusus cukup mewakili deretan karakter itu. Kalau abc muncul di mana pun di string yang kita tes (gak cuma di awal), test bakal ngembaliin true.

Set Karakter

Nyari tahu apakah string berisi abc bisa juga dilakuin dengan pemanggilan indexOf. Regular expression berguna karena mereka ngebolehin kita menggambarkan pola yang lebih rumit.

Misalnya kita mau nyocokin angka apa pun. Di regular expression, naruh sekumpulan karakter di antara kurung siku bikin bagian ekspresi itu cocok dengan salah satu karakter di antara kurungnya.

Kedua ekspresi berikut cocok dengan semua string yang berisi digit:

console.log(/[0123456789]/.test("in 1992"));
// → true
console.log(/[0-9]/.test("in 1992"));
// → true

Di dalam kurung siku, tanda hubung (-) di antara dua karakter bisa dipakai buat nunjukin rentang karakter, di mana urutannya ditentuin sama nomor Unicode karakternya. Karakter 0 sampai 9 duduk tepat bersebelahan di urutan ini (kode 48 sampai 57), jadi [0-9] mencakup semuanya dan cocok dengan digit apa pun.

Sejumlah grup karakter umum punya jalan pintas bawaan sendiri. Digit salah satunya: \d artinya sama dengan [0-9].

KodeArti
\dKarakter digit apa pun
\wKarakter alfanumerik (“karakter kata”)
\sKarakter whitespace apa pun (spasi, tab, newline, dan serupa)
\DKarakter yang bukan digit
\WKarakter non-alfanumerik
\SKarakter non-whitespace
.Karakter apa pun kecuali newline

Kamu bisa nyocokin format tanggal dan waktu kaya 01-30-2003 15:20 dengan ekspresi berikut:

let dateTime = /\d\d-\d\d-\d\d\d\d \d\d:\d\d/;
console.log(dateTime.test("01-30-2003 15:20"));
// → true
console.log(dateTime.test("30-jan-2003 15:20"));
// → false

Regular expression itu keliatan mengerikan banget, kan? Setengahnya backslash, ngehasilin kebisingan latar yang bikin susah nemuin pola sebenarnya yang diekspresikan. Kita bakal lihat versi yang sedikit lebih baik dari ekspresi ini nanti.

Kode backslash ini juga bisa dipakai di dalam kurung siku. Misalnya, [\d.] artinya digit apa pun atau karakter titik. Titiknya sendiri, di antara kurung siku, kehilangan makna khususnya. Sama juga buat karakter khusus lain, kaya tanda plus (+).

Buat ngebalik set karakter, yaitu, nyatain bahwa kamu mau nyocokin karakter apa pun kecuali yang ada di set, kamu bisa nulis karakter caret (^) setelah kurung pembuka.

let nonBinary = /[^01]/;
console.log(nonBinary.test("1100100010100110"));
// → false
console.log(nonBinary.test("0111010112101001"));
// → true

Karakter Internasional

Karena implementasi awal JavaScript yang simplistik dan fakta bahwa pendekatan simplistik ini belakangan dibakukan sebagai perilaku standar, regular expression JavaScript agak bodoh soal karakter yang gak muncul di bahasa Inggris. Misalnya, sejauh yang dipedulikan regular expression JavaScript, “karakter kata” cuma salah satu dari 26 karakter di alfabet Latin (huruf besar atau kecil), digit desimal, dan, entah kenapa, karakter underscore. Hal kaya é atau β, yang jelas-jelas karakter kata, gak bakal cocok dengan \w (dan bakal cocok dengan \W huruf besar, kategori non-kata).

Karena kecelakaan sejarah yang aneh, \s (whitespace) gak punya masalah ini dan cocok dengan semua karakter yang dianggap whitespace sama standar Unicode, termasuk hal kaya spasi tak terputus dan pemisah vokal Mongolia.

Bisa aja pakai \p di regular expression buat nyocokin semua karakter yang dikasih properti tertentu sama standar Unicode. Ini ngebolehin kita nyocokin hal kaya huruf dengan cara yang lebih kosmopolitan. Tapi, lagi-lagi karena kompatibilitas dengan standar bahasa asli, ini cuma dikenali waktu kamu naruh karakter u (buat Unicode) setelah regular expression-nya.

KodeArti
\p{L}Huruf apa pun
\p{N}Karakter numerik apa pun
\p{P}Karakter tanda baca apa pun
\P{L}Non-huruf apa pun (P huruf besar ngebalik)
\p{Script=Hangul}Karakter apa pun dari script yang dikasih (lihat [[05 Fungsi Tingkat Tinggi#Dataset Script

Pakai \w buat pemrosesan teks yang mungkin perlu nanganin teks non-Inggris (atau bahkan teks Inggris dengan kata serapan kaya “cliché”) itu beban, karena dia gak bakal memperlakukan karakter kaya “é” sebagai huruf. Walaupun cenderung sedikit lebih panjang, grup properti \p lebih tangguh.

console.log(/\p{L}/u.test("α"));
// → true
console.log(/\p{L}/u.test("!"));
// → false
console.log(/\p{Script=Greek}/u.test("α"));
// → true
console.log(/\p{Script=Arabic}/u.test("α"));
// → false

Di sisi lain, kalau kamu nyocokin angka buat ngelakuin sesuatu dengannya, kamu sering emang mau \d buat digit, karena ngonversi karakter numerik sembarangan jadi angka JavaScript bukan sesuatu yang bisa dilakuin fungsi kaya Number buat kamu.

Ngulang Bagian Pola

Kita sekarang tahu cara nyocokin satu digit. Gimana kalau kita mau nyocokin bilangan utuh, deretan satu digit atau lebih?

Waktu kamu naruh tanda plus (+) setelah sesuatu di regular expression, dia nunjukin bahwa elemennya boleh diulang lebih dari sekali. Jadi, /\d+/ cocok dengan satu karakter digit atau lebih.

console.log(/'\d+'/.test("'123'"));
// → true
console.log(/'\d+'/.test("''"));
// → false
console.log(/'\d*'/.test("'123'"));
// → true
console.log(/'\d*'/.test("''"));
// → true

Bintang (*) punya makna serupa tapi juga ngebolehin polanya cocok nol kali. Sesuatu dengan bintang setelahnya gak pernah nyegah pola buat cocok, dia cuma bakal cocok nol instance kalau gak bisa nemuin teks yang cocok.

Tanda tanya (?) bikin bagian pola opsional, artinya dia boleh muncul nol kali atau satu kali. Di contoh berikut, karakter u boleh muncul, tapi polanya juga cocok waktu dia gak ada:

let neighbor = /neighbou?r/;
console.log(neighbor.test("neighbour"));
// → true
console.log(neighbor.test("neighbor"));
// → true

Buat nunjukin bahwa pola harus muncul sejumlah kali yang presisi, pakai kurung kurawal. Naruh {4} setelah elemen, misalnya, ngeharusin dia muncul persis empat kali. Bisa juga nentuin rentang dengan cara ini: {2,4} artinya elemennya harus muncul minimal dua kali dan maksimal empat kali.

Ini versi lain dari pola tanggal dan waktu yang ngebolehin hari, bulan, dan jam satu maupun dua digit. Dia juga sedikit lebih gampang diuraikan.

let dateTime = /\d{1,2}-\d{1,2}-\d{4} \d{1,2}:\d{2}/;
console.log(dateTime.test("1-30-2003 8:45"));
// → true

Kamu juga bisa nentuin rentang terbuka waktu pakai kurung kurawal dengan ngilangin angka setelah koma. Misalnya, {5,} artinya lima kali atau lebih.

Ngelompokin Subekspresi

Buat pakai operator kaya * atau + di lebih dari satu elemen sekaligus, kamu harus pakai tanda kurung. Bagian regular expression yang dibungkus tanda kurung dihitung sebagai satu elemen sejauh yang dipedulikan operator setelahnya.

let cartoonCrying = /boo+(hoo+)+/i;
console.log(cartoonCrying.test("Boohoooohoohooo"));
// → true

Karakter + pertama dan kedua masing-masing cuma berlaku buat o kedua di boo dan hoo. + ketiga berlaku buat seluruh grup (hoo+), nyocokin satu atau lebih deretan kaya gitu.

i di akhir ekspresi di contoh bikin regular expression ini gak peka huruf besar-kecil (case insensitive), ngebolehinnya nyocokin B huruf besar di string input, walaupun polanya sendiri semua huruf kecil.

Kecocokan dan Grup

Method test adalah cara paling sederhana buat nyocokin regular expression. Dia cuma ngasih tahu kamu apakah cocok dan gak lebih. Regular expression juga punya method exec (execute) yang bakal ngembaliin null kalau gak ada kecocokan ditemuin dan ngembaliin objek dengan informasi soal kecocokannya kalau ada.

let match = /\d+/.exec("one two 100");
console.log(match);
// → ["100"]
console.log(match.index);
// → 8

Objek yang dikembaliin dari exec punya properti index yang ngasih tahu kita di mana di string kecocokan yang berhasil dimulai. Selain itu, objeknya keliatan kaya (dan sebenernya adalah) array string, yang elemen pertamanya adalah string yang cocok. Di contoh sebelumnya, ini deretan digit yang kita cari.

Nilai string punya method match yang berperilaku serupa.

console.log("one two 100".match(/\d+/));
// → ["100"]

Waktu regular expression berisi subekspresi yang dikelompokin dengan tanda kurung, teks yang cocok dengan grup-grup itu juga bakal muncul di array. Seluruh kecocokan selalu jadi elemen pertama. Elemen berikutnya adalah bagian yang cocok dengan grup pertama (yang tanda kurung pembukanya muncul duluan di ekspresi), lalu grup kedua, dan seterusnya.

let quotedText = /'([^']*)'/;
console.log(quotedText.exec("she said 'hello'"));
// → ["'hello'", "hello"]

Waktu grup gak cocok sama sekali (misalnya, waktu diikuti tanda tanya), posisinya di array output bakal berisi undefined. Waktu grup cocok beberapa kali (misalnya, waktu diikuti +), cuma kecocokan terakhir yang masuk ke array.

console.log(/bad(ly)?/.exec("bad"));
// → ["bad", undefined]
console.log(/(\d)+/.exec("123"));
// → ["123", "3"]

Kalau kamu mau pakai tanda kurung murni buat ngelompokin, tanpa bikin mereka muncul di array kecocokan, kamu bisa naruh ?: setelah kurung pembuka.

console.log(/(?:na)+/.exec("banana"));
// → ["nana"]

Grup bisa berguna buat ngekstrak bagian dari string. Kalau kita gak cuma mau memverifikasi apakah string berisi tanggal tapi juga ngekstraknya dan ngebangun objek yang mewakilinya, kita bisa ngebungkus pola digitnya dengan tanda kurung dan langsung ngambil tanggalnya dari hasil exec.

Tapi pertama kita bakal belok sebentar buat ngebahas cara bawaan mewakili nilai tanggal dan waktu di JavaScript.

Class Date

JavaScript punya class Date standar buat mewakili tanggal, atau lebih tepatnya, titik waktu. Kalau kamu cuma bikin objek date pakai new, kamu dapet tanggal dan waktu saat ini.

console.log(new Date());
// → Fri Feb 02 2024 18:03:06 GMT+0100 (CET)

Kamu juga bisa bikin objek buat waktu tertentu.

console.log(new Date(2009, 11, 9));
// → Wed Dec 09 2009 00:00:00 GMT+0100 (CET)
console.log(new Date(2009, 11, 9, 12, 59, 59, 999));
// → Wed Dec 09 2009 12:59:59 GMT+0100 (CET)

JavaScript pakai konvensi di mana nomor bulan dimulai dari nol (jadi Desember adalah 11), tapi nomor hari dimulai dari satu. Ini membingungkan dan konyol. Hati-hati.

Empat argumen terakhir (jam, menit, detik, dan milidetik) opsional dan dianggap nol kalau gak dikasih.

Timestamp disimpan sebagai jumlah milidetik sejak awal 1970, di zona waktu UTC. Ini ngikutin konvensi yang ditetapin “Unix time”, yang diciptain sekitar waktu itu. Kamu bisa pakai angka negatif buat waktu sebelum 1970. Method getTime di objek date ngembaliin angka ini. Dia besar, kaya yang bisa kamu bayangin.

console.log(new Date(2013, 11, 19).getTime());
// → 1387407600000
console.log(new Date(1387407600000));
// → Thu Dec 19 2013 00:00:00 GMT+0100 (CET)

Kalau kamu ngasih constructor Date satu argumen, argumen itu diperlakukan sebagai hitungan milidetik kaya gitu. Kamu bisa dapet hitungan milidetik saat ini dengan bikin objek Date baru dan manggil getTime di dia atau dengan manggil fungsi Date.now.

Objek date nyediain method kaya getFullYear, getMonth, getDate, getHours, getMinutes, dan getSeconds buat ngekstrak komponennya. Selain getFullYear ada juga getYear, yang ngasih kamu tahun dikurangi 1900 (kaya 98 atau 125) dan hampir gak berguna.

Naruh tanda kurung di sekitar bagian ekspresi yang kita minati, kita sekarang bisa bikin objek date dari string.

function getDate(string) {
  let [_, month, day, year] =
    /(\d{1,2})-(\d{1,2})-(\d{4})/.exec(string);
  return new Date(year, month - 1, day);
}
console.log(getDate("1-30-2003"));
// → Thu Jan 30 2003 00:00:00 GMT+0100 (CET)

Binding underscore (_) diabaikan dan cuma dipakai buat ngelewatin elemen kecocokan penuh di array yang dikembaliin exec.

Boundary dan Look-Ahead

Sayangnya, getDate juga bakal dengan senang hati ngekstrak tanggal dari string "100-1-30000". Kecocokan bisa terjadi di mana pun di string, jadi di kasus ini, dia cuma bakal mulai di karakter kedua dan berakhir di karakter kedua dari terakhir.

Kalau kita mau memaksa bahwa kecocokannya harus membentang seluruh string, kita bisa nambahin penanda ^ dan $. Caret cocok dengan awal string input, sedangkan tanda dolar cocok dengan akhirnya. Jadi /^\d+$/ cocok dengan string yang sepenuhnya terdiri dari satu digit atau lebih, /^!/ cocok dengan string apa pun yang dimulai dengan tanda seru, dan /x^/ gak cocok dengan string apa pun (gak mungkin ada x sebelum awal string).

Ada juga penanda \b yang cocok dengan batas kata (word boundary), posisi yang punya karakter kata di satu sisi, dan karakter non-kata di sisi lain. Sayangnya, ini pakai konsep karakter kata yang sama simplistiknya dengan \w dan karena itu gak terlalu bisa diandalkan.

Perhatiin bahwa penanda batas ini gak cocok dengan karakter apa pun yang sebenarnya. Mereka cuma memaksa bahwa kondisi tertentu terpenuhi di tempat mereka muncul di pola.

Tes look-ahead ngelakuin sesuatu yang serupa. Mereka nyediain pola dan bakal bikin kecocokan gagal kalau inputnya gak cocok dengan pola itu, tapi gak beneran mindahin posisi kecocokan maju. Mereka ditulis di antara (?= dan ).

console.log(/a(?=e)/.exec("braeburn"));
// → ["a"]
console.log(/a(?! )/.exec("a b"));
// → null

e di contoh pertama perlu buat cocok, tapi bukan bagian dari string yang cocok. Notasi (?! ) nyatain look-ahead negatif. Ini cocok cuma kalau pola di dalam tanda kurung gak cocok, bikin contoh kedua cuma cocok dengan karakter a yang gak punya spasi setelahnya.

Pola Pilihan

Misalnya kita mau tahu apakah sepotong teks berisi bukan cuma angka tapi angka diikuti salah satu kata pig, cow, atau chicken, atau bentuk jamaknya.

Kita bisa nulis tiga regular expression dan ngetesnya bergantian, tapi ada cara yang lebih bagus. Karakter pipa (|) nunjukin pilihan antara pola di kirinya dan pola di kanannya. Kita bisa pakainya di ekspresi kaya gini:

let animalCount = /\d+ (pig|cow|chicken)s?/;
console.log(animalCount.test("15 pigs"));
// → true
console.log(animalCount.test("15 pugs"));
// → false

Tanda kurung bisa dipakai buat ngebatasin bagian pola tempat operator pipa berlaku, dan kamu bisa naruh beberapa operator kaya gitu bersebelahan buat nyatain pilihan antara lebih dari dua alternatif.

Mekanika Pencocokan

Secara konseptual, waktu kamu pakai exec atau test, mesin regular expression nyari kecocokan di stringmu dengan nyoba nyocokin ekspresinya pertama dari awal string, lalu dari karakter kedua, dan seterusnya sampai dia nemuin kecocokan atau nyampe akhir string. Dia bakal ngembaliin kecocokan pertama yang bisa ditemuin atau gagal nemuin kecocokan sama sekali.

Buat ngelakuin pencocokan yang sebenarnya, mesinnya memperlakukan regular expression kaya diagram alur. Ini diagram buat ekspresi ternak di contoh sebelumnya:

Diagram rel kereta yang pertama ngelewatin kotak berlabel 'digit', yang punya loop balik dari setelahnya ke sebelumnya, lalu kotak buat karakter spasi. Setelah itu, relnya bercabang tiga, ngelewatin kotak buat 'pig', 'cow', dan 'chicken'. Setelah itu mereka bergabung lagi, dan ngelewatin kotak berlabel 's', yang, karena opsional, juga punya rel yang ngelewatinnya. Terakhir, garisnya nyampe keadaan diterima.

Kalau kita bisa nemuin jalur dari sisi kiri diagram ke sisi kanan, ekspresi kita cocok. Kita nyimpen posisi saat ini di string, dan setiap kali kita ngelewatin kotak, kita memverifikasi bahwa bagian string setelah posisi kita saat ini cocok dengan kotak itu.

Backtracking

Regular expression /^([01]+b|[\da-f]+h|\d+)$/ cocok dengan angka biner diikuti b, angka heksadesimal (yaitu, basis 16, dengan huruf a sampai f mewakili digit 10 sampai 15) diikuti h, atau angka desimal biasa tanpa karakter akhiran. Ini diagram yang sesuai:

Diagram rel kereta buat regular expression '^([01]+b|\d+|[\da-f]+h)$'

Waktu nyocokin ekspresi ini, cabang atas (biner) sering bakal dimasuki walaupun inputnya sebenernya gak berisi angka biner. Waktu nyocokin string "103", misalnya, baru jelas di 3 bahwa kita ada di cabang yang salah. Stringnya emang cocok dengan ekspresinya, cuma bukan cabang yang lagi kita masuki.

Jadi pencocoknya backtrack (mundur). Waktu masuk cabang, dia ngingat posisinya saat ini (di kasus ini, di awal string, tepat setelah kotak batas pertama di diagram) biar dia bisa balik dan nyoba cabang lain kalau yang sekarang gak berhasil. Buat string "103", setelah nemuin karakter 3, pencocoknya mulai nyoba cabang buat angka heksadesimal, yang gagal lagi karena gak ada h setelah angkanya. Dia lalu nyoba cabang angka desimal. Yang ini pas, dan kecocokan dilaporin pada akhirnya.

Pencocoknya berhenti begitu nemuin kecocokan penuh. Ini berarti kalau beberapa cabang berpotensi cocok dengan sebuah string, cuma yang pertama (diurutkan berdasarkan tempat cabangnya muncul di regular expression) yang dipakai.

Backtracking juga terjadi buat operator pengulangan kaya + dan *. Kalau kamu nyocokin /^.*x/ dengan "abcxe", bagian .* bakal pertama nyoba ngonsumsi seluruh string. Mesinnya lalu bakal nyadar dia butuh x buat nyocokin polanya. Karena gak ada x setelah akhir string, operator bintangnya nyoba nyocokin satu karakter lebih sedikit. Tapi pencocoknya gak nemuin x setelah abcx juga, jadi dia backtrack lagi, nyocokin operator bintangnya cuma ke abc. Sekarang dia nemuin x di tempat dia butuhinnya dan ngelaporin kecocokan berhasil dari posisi 0 sampai 4.

Bisa aja nulis regular expression yang bakal ngelakuin banyak backtracking. Masalah ini terjadi waktu sebuah pola bisa cocok dengan sepotong input dengan banyak cara berbeda. Misalnya, kalau kita bingung waktu nulis regular expression angka biner, kita mungkin gak sengaja nulis sesuatu kaya /([01]+)+b/.

Diagram rel kereta buat regular expression '([01]+)+b'

Kalau itu nyoba nyocokin deretan panjang nol dan satu tanpa karakter b di akhir, pencocoknya pertama ngelewatin loop dalam sampai kehabisan digit. Lalu dia nyadar gak ada b, jadi dia backtrack satu posisi, ngelewatin loop luar sekali, dan menyerah lagi, nyoba backtrack keluar dari loop dalam sekali lagi. Dia bakal terus nyoba setiap rute yang mungkin lewat dua loop ini. Ini berarti jumlah kerjaannya berlipat dua dengan setiap karakter tambahan. Bahkan buat cuma beberapa lusin karakter, kecocokan hasilnya bakal makan waktu praktis selamanya.

Method replace

Nilai string punya method replace yang bisa dipakai buat ngegantiin bagian string dengan string lain.

console.log("papa".replace("p", "m"));
// → mapa

Argumen pertamanya juga bisa regular expression, di kasus mana kecocokan pertama dari regular expression-nya diganti. Waktu opsi g (buat global) ditambahin setelah regular expression-nya, semua kecocokan di string bakal diganti, gak cuma yang pertama.

console.log("Borobudur".replace(/[ou]/, "a"));
// → Barobudur
console.log("Borobudur".replace(/[ou]/g, "a"));
// → Barabadar

Kekuatan sebenarnya dari pakai regular expression dengan replace datang dari fakta bahwa kita bisa ngerujuk grup yang cocok di string pengganti. Misalnya, kita punya string besar yang berisi nama orang, satu nama per baris, dalam format Lastname, Firstname. Kalau kita mau nuker nama-nama ini dan ngehapus komanya buat dapet format Firstname Lastname, kita bisa pakai kode berikut:

console.log(
  "Liskov, Barbara\nMcCarthy, John\nMilner, Robin"
    .replace(/(\p{L}+), (\p{L}+)/gu, "$2 $1"));
// → Barbara Liskov
//   John McCarthy
//   Robin Milner

$1 dan $2 di string pengganti ngerujuk ke grup dalam tanda kurung di pola. $1 diganti dengan teks yang cocok dengan grup pertama, $2 dengan yang kedua, dan seterusnya, sampai $9. Seluruh kecocokan bisa dirujuk dengan $&.

Bisa aja ngasih fungsi, bukannya string, sebagai argumen kedua ke replace. Buat setiap penggantian, fungsinya bakal dipanggil dengan grup yang cocok (juga seluruh kecocokan) sebagai argumen, dan nilai kembaliannya bakal disisipin ke string baru.

Ini contohnya:

let stock = "1 lemon, 2 cabbages, and 101 eggs";
function minusOne(match, amount, unit) {
  amount = Number(amount) - 1;
  if (amount == 1) { // cuma tersisa satu, hapus 's'-nya
    unit = unit.slice(0, unit.length - 1);
  } else if (amount == 0) {
    amount = "no";
  }
  return amount + " " + unit;
}
console.log(stock.replace(/(\d+) (\p{L}+)/gu, minusOne));
// → no lemon, 1 cabbage, and 100 eggs

Kode ini ngambil string, nemuin semua kemunculan angka diikuti kata alfanumerik, dan ngembaliin string yang punya satu lebih sedikit dari setiap kuantitas kaya gitu.

Grup (\d+) berakhir sebagai argumen amount ke fungsinya, dan grup (\p{L}+) terikat ke unit. Fungsinya ngonversi amount ke angka, yang selalu jalan, karena dia cocok dengan \d+ sebelumnya, dan bikin beberapa penyesuaian kalau cuma tersisa satu atau nol.

Greed (Kerakusan)

Kita bisa pakai replace buat nulis fungsi yang ngehapus semua komentar dari sepotong kode JavaScript. Ini percobaan pertama:

function stripComments(code) {
  return code.replace(/\/\/.*|\/\*[^]*\*\//g, "");
}
console.log(stripComments("1 + /* 2 */3"));
// → 1 + 3
console.log(stripComments("x = 10;// ten!"));
// → x = 10;
console.log(stripComments("1 /* a */+/* b */ 1"));
// → 1  1

Bagian sebelum operator | cocok dengan dua karakter garis miring diikuti berapa pun karakter non-newline. Bagian buat komentar multi-baris lebih rumit. Kita pakai [^] (karakter apa pun yang gak ada di set karakter kosong) sebagai cara buat nyocokin karakter apa pun. Kita gak bisa cuma pakai titik di sini karena komentar blok bisa berlanjut di baris baru, dan karakter titik gak cocok dengan karakter newline.

Tapi output buat baris terakhir keliatannya salah. Kenapa?

Bagian [^]* dari ekspresinya, kaya yang saya jelasin di bagian backtracking, bakal pertama nyocokin sebanyak mungkin. Kalau itu bikin bagian berikutnya dari pola gagal, pencocoknya mundur satu karakter dan nyoba lagi dari sana. Di contoh, pencocoknya pertama nyoba nyocokin seluruh sisa string lalu mundur dari sana. Dia bakal nemuin kemunculan */ setelah mundur empat karakter dan nyocokin itu. Ini bukan yang kita mau, niatnya adalah nyocokin satu komentar, bukan pergi sampai akhir kode dan nemuin akhir dari komentar blok terakhir.

Karena perilaku ini, kita bilang operator pengulangan (+, *, ?, dan {}) itu greedy (rakus), artinya mereka nyocokin sebanyak mungkin dan backtrack dari sana. Kalau kamu naruh tanda tanya setelahnya (+?, *?, ??, {}?), mereka jadi nongreedy dan mulai dengan nyocokin sesedikit mungkin, nyocokin lebih banyak cuma waktu sisa polanya gak pas dengan kecocokan yang lebih kecil.

Dan itu persis yang kita mau di kasus ini. Dengan bikin bintangnya nyocokin rentang karakter terkecil yang bawa kita ke */, kita ngonsumsi satu komentar blok dan gak lebih.

function stripComments(code) {
  return code.replace(/\/\/.*|\/\*[^]*?\*\//g, "");
}
console.log(stripComments("1 /* a */+/* b */ 1"));
// → 1 + 1

Banyak bug di program regular expression bisa ditelusuri ke penggunaan operator greedy yang gak disengaja di mana operator nongreedy bakal lebih baik. Waktu pakai operator pengulangan, lebih pilih varian nongreedy.

❌ greedy, [^]* nelan sampai */ terakhir

"1 /* a */+/* b */ 1".replace(/\/\*[^]*\*\//g, "")
// → "1  1"

✅ nongreedy, [^]*? berhenti di */ pertama

"1 /* a */+/* b */ 1".replace(/\/\*[^]*?\*\//g, "")
// → "1 + 1"

Bikin Objek RegExp Secara Dinamis

Di beberapa kasus kamu mungkin gak tahu pola persis yang perlu kamu cocokin waktu nulis kodemu. Misalnya kamu mau ngetes nama pengguna di sepotong teks. Kamu bisa ngebangun string dan pakai constructor RegExp dengan itu.

let name = "harry";
let regexp = new RegExp("(^|\\s)" + name + "($|\\s)", "gi");
console.log(regexp.test("Harry is a dodgy character."));
// → true

Waktu bikin bagian \s dari string, kita harus pakai dua backslash karena kita nulisnya di string normal, bukan regular expression yang dibungkus garis miring. Argumen kedua ke constructor RegExp berisi opsi buat regular expression-nya, di kasus ini, "gi" buat global dan case insensitive.

Tapi gimana kalau namanya "dea+hl[]rd" karena pengguna kita remaja kutu buku? Itu bakal ngehasilin regular expression yang gak masuk akal yang sebenernya gak bakal cocok dengan nama penggunanya.

Buat ngatasi ini, kita bisa nambahin backslash sebelum karakter apa pun yang punya makna khusus.

let name = "dea+hl[]rd";
let escaped = name.replace(/[\\[.+*?(){|^$]/g, "\\$&");
let regexp = new RegExp("(^|\\s)" + escaped + "($|\\s)",
                        "gi");
let text = "This dea+hl[]rd guy is super annoying.";
console.log(regexp.test(text));
// → true

Sementara method indexOf di string gak bisa dipanggil dengan regular expression, ada method lain, search, yang emang ngarepin regular expression. Kaya indexOf, dia ngembaliin indeks pertama tempat ekspresinya ditemuin, atau -1 waktu gak ditemuin.

console.log("  word".search(/\S/));
// → 2
console.log("    ".search(/\S/));
// → -1

Sayangnya, gak ada cara buat nunjukin bahwa kecocokannya harus mulai di offset tertentu (kaya yang bisa kita lakuin dengan argumen kedua ke indexOf), yang sering bakal berguna.

Properti lastIndex

Method exec juga gak nyediain cara praktis buat mulai nyari dari posisi tertentu di string. Tapi dia emang nyediain cara yang gak praktis.

Objek regular expression punya properti. Salah satunya adalah source, yang berisi string tempat ekspresinya dibikin. Properti lain adalah lastIndex, yang ngendaliin, di beberapa keadaan terbatas, di mana kecocokan berikutnya bakal mulai.

Keadaan itu adalah regular expression-nya harus punya opsi global (g) atau sticky (y) yang diaktifin, dan kecocokannya harus terjadi lewat method exec. Lagi-lagi, solusi yang kurang membingungkan adalah cukup ngebolehin argumen ekstra dikasih ke exec, tapi kebingungan adalah fitur penting dari interface regular expression JavaScript.

let pattern = /y/g;
pattern.lastIndex = 3;
let match = pattern.exec("xyzzy");
console.log(match.index);
// → 4
console.log(pattern.lastIndex);
// → 5

Kalau kecocokannya berhasil, pemanggilan exec otomatis ngupdate properti lastIndex buat nunjuk setelah kecocokannya. Kalau gak ada kecocokan ditemuin, lastIndex disetel balik ke 0, yang juga nilainya di objek regular expression yang baru dibangun.

Perbedaan antara opsi global dan sticky adalah waktu sticky diaktifin, kecocokannya bakal berhasil cuma kalau dia mulai langsung di lastIndex, sedangkan dengan global, dia bakal nyari ke depan buat posisi di mana kecocokan bisa mulai.

let global = /abc/g;
console.log(global.exec("xyz abc"));
// → ["abc"]
let sticky = /abc/y;
console.log(sticky.exec("xyz abc"));
// → null

Waktu pakai nilai regular expression bersama buat beberapa pemanggilan exec, update otomatis ke properti lastIndex ini bisa bikin masalah. Regular expression-mu mungkin gak sengaja mulai di indeks sisa dari pemanggilan sebelumnya.

let digit = /\d/g;
console.log(digit.exec("here it is: 1"));
// → ["1"]
console.log(digit.exec("and now: 1"));
// → null

Efek menarik lain dari opsi global adalah dia ngubah cara method match di string bekerja. Waktu dipanggil dengan ekspresi global, bukannya ngembaliin array yang mirip yang dikembaliin exec, match bakal nemuin semua kecocokan pola di string dan ngembaliin array yang berisi string-string yang cocok.

console.log("Banana".match(/an/g));
// → ["an", "an"]

Jadi hati-hati dengan regular expression global. Kasus di mana mereka dibutuhin, pemanggilan replace dan tempat di mana kamu mau eksplisit pakai lastIndex, biasanya situasi di mana kamu mau pakainya.

Hal umum yang dilakuin adalah nemuin semua kecocokan regular expression di string. Kita bisa ngelakuin ini pakai method matchAll.

let input = "A string with 3 numbers in it... 42 and 88.";
let matches = input.matchAll(/\d+/g);
for (let match of matches) {
  console.log("Found", match[0], "at", match.index);
}
// → Found 3 at 14
//   Found 42 at 33
//   Found 88 at 40

Method ini ngembaliin array dari array kecocokan. Regular expression yang dikasih ke matchAll harus punya g diaktifin.

Parsing File INI

Buat nutup bab ini, kita bakal ngelihat masalah yang butuh regular expression. Bayangin kita nulis program buat otomatis ngumpulin informasi soal musuh-musuh kita dari internet. (Kita gak bakal beneran nulis program itu di sini, cuma bagian yang baca file konfigurasinya. Maaf.) File konfigurasinya keliatan kaya gini:

searchengine=https://duckduckgo.com/?q=$1
spitefulness=9.7
 
; comments are preceded by a semicolon...
; each section concerns an individual enemy
[larry]
fullname=Larry Doe
type=kindergarten bully
website=http://www.geocities.com/CapeCanaveral/11451
 
[davaeorn]
fullname=Davaeorn
type=evil wizard
outputdir=/home/marijn/enemies/davaeorn

Aturan persis buat format ini, yang merupakan format file yang dipakai luas, biasanya disebut file INI, adalah sebagai berikut:

  • Baris kosong dan baris yang dimulai dengan titik koma diabaikan.
  • Baris yang dibungkus [ dan ] memulai section baru.
  • Baris yang berisi identifier alfanumerik diikuti karakter = nambahin setting ke section saat ini.
  • Yang lain gak valid.

Tugas kita adalah ngonversi string kaya gini jadi objek yang propertinya megang string buat setting yang ditulis sebelum header section pertama dan subobjek buat section, dengan subobjek itu megang setting section-nya.

Karena formatnya harus diproses baris per baris, mecah filenya jadi baris-baris terpisah adalah awal yang bagus. Kita lihat method split di Bab 4. Tapi beberapa sistem operasi gak cuma pakai karakter newline buat misahin baris tapi karakter carriage return diikuti newline ("\r\n"). Mengingat method split juga ngebolehin regular expression sebagai argumennya, kita bisa pakai regular expression kaya /\r?\n/ buat mecah dengan cara yang ngebolehin "\n" maupun "\r\n" di antara baris.

function parseINI(string) {
  // Mulai dengan objek buat megang field level atas
  let result = {};
  let section = result;
  for (let line of string.split(/\r?\n/)) {
    let match;
    if (match = line.match(/^(\w+)=(.*)$/)) {
      section[match[1]] = match[2];
    } else if (match = line.match(/^\[(.*)\]$/)) {
      section = result[match[1]] = {};
    } else if (!/^\s*(;|$)/.test(line)) {
      throw new Error("Line '" + line + "' is not valid.");
    }
  };
  return result;
}
 
console.log(parseINI(`
name=Vasilis
[address]
city=Tessaloniki`));
// → {name: "Vasilis", address: {city: "Tessaloniki"}}

Kodenya ngelewatin baris-baris file dan ngebangun objek. Properti di atas disimpan langsung ke objek itu, sedangkan properti yang ditemuin di section disimpan di objek section terpisah. Binding section nunjuk ke objek buat section saat ini.

Ada dua jenis baris signifikan, header section atau baris properti. Waktu baris adalah properti biasa, dia disimpan di section saat ini. Waktu dia header section, objek section baru dibikin, dan section disetel buat nunjuk ke sana.

Perhatiin penggunaan berulang ^ dan $ buat mastiin ekspresinya cocok dengan seluruh baris, bukan cuma sebagian. Ngilangin ini ngehasilin kode yang kebanyakan jalan tapi berperilaku aneh buat beberapa input, yang bisa jadi bug yang susah dilacak.

Pola if (match = string.match(...)) memanfaatkan fakta bahwa nilai dari ekspresi assignment (=) adalah nilai yang di-assign. Kamu sering gak yakin pemanggilan match-mu bakal berhasil, jadi kamu cuma bisa ngakses objek hasilnya di dalam statement if yang ngetes ini. Biar gak ngerusak rantai bentuk else if yang enak, kita assign hasil match-nya ke binding dan langsung pakai assignment itu sebagai tes buat statement if.

Kalau baris bukan header section atau properti, fungsinya ngecek apakah dia komentar atau baris kosong pakai ekspresi /^\s*(;|$)/ buat nyocokin baris yang cuma berisi whitespace, atau whitespace diikuti titik koma (bikin sisa barisnya komentar). Waktu baris gak cocok dengan bentuk yang diharapkan mana pun, fungsinya ngelempar exception.

Code Unit dan Karakter

Kesalahan desain lain yang udah dibakukan di regular expression JavaScript adalah secara default, operator kaya . atau ? bekerja pada code unit (kaya yang dibahas di Bab 5), bukan karakter sebenarnya. Ini berarti karakter yang tersusun dari dua code unit berperilaku aneh.

console.log(/🍎{3}/.test("🍎🍎🍎"));
// → false
console.log(/<.>/.test("<🌹>"));
// → false
console.log(/<.>/u.test("<🌹>"));
// → true

Masalahnya adalah 🍎 di baris pertama diperlakukan sebagai dua code unit, dan {3} cuma diterapin ke unit kedua. Sama juga, titiknya cocok dengan satu code unit, bukan dua yang nyusun emoji mawar.

Kamu harus nambahin opsi u (Unicode) ke regular expression-mu biar dia memperlakukan karakter kaya gitu dengan benar.

console.log(/🍎{3}/u.test("🍎🍎🍎"));
// → true

Ringkasan

Regular expression adalah objek yang mewakili pola di string. Mereka pakai bahasa mereka sendiri buat nyatain pola-pola ini.

PolaArti
/abc/Deretan karakter
/[abc]/Karakter apa pun dari sekumpulan karakter
/[^abc]/Karakter apa pun yang gak ada di sekumpulan karakter
/[0-9]/Karakter apa pun di rentang karakter
/x+/Satu atau lebih kemunculan pola x
/x+?/Satu atau lebih kemunculan, nongreedy
/x*/Nol atau lebih kemunculan
/x?/Nol atau satu kemunculan
/x{2,4}/Dua sampai empat kemunculan
/(abc)/Sebuah grup
/a|b|c/Salah satu dari beberapa pola
/\d/Karakter digit apa pun
/\w/Karakter alfanumerik (“karakter kata”)
/\s/Karakter whitespace apa pun
/./Karakter apa pun kecuali newline
/\p{L}/uKarakter huruf apa pun
/^/Awal input
/$/Akhir input
/(?=a)/Tes look-ahead

Regular expression punya method test buat ngetes apakah string tertentu cocok dengannya. Dia juga punya method exec yang, waktu kecocokan ditemuin, ngembaliin array yang berisi semua grup yang cocok. Array kaya gitu punya properti index yang nunjukin di mana kecocokannya mulai.

String punya method match buat nyocokinnya dengan regular expression dan method search buat nyari satu, cuma ngembaliin posisi awal kecocokannya. Method replace mereka bisa ngegantiin kecocokan pola dengan string atau fungsi pengganti.

Regular expression bisa punya opsi, yang ditulis setelah garis miring penutup. Opsi i bikin kecocokannya case insensitive. Opsi g bikin ekspresinya global, yang, antara lain, bikin method replace ngegantiin semua instance bukannya cuma yang pertama. Opsi y bikin ekspresinya sticky, yang berarti dia gak bakal nyari ke depan dan ngelewatin bagian string waktu nyari kecocokan. Opsi u nyalain mode Unicode, yang ngaktifin sintaks \p dan memperbaiki sejumlah masalah seputar penanganan karakter yang makan dua code unit.

Regular expression adalah alat tajam dengan pegangan yang canggung. Mereka menyederhanakan beberapa tugas dengan luar biasa tapi bisa cepat jadi gak terkendali waktu diterapin ke masalah yang rumit. Bagian dari tahu cara pakainya adalah nahan dorongan buat nyoba maksain hal-hal yang gak bisa mereka ekspresikan dengan bersih.

Poin-poin pentingnya:

  1. Regex ditulis /pola/opsi atau new RegExp("pola", "opsi"). Method utamanya test (Boolean) dan exec (array kecocokan + index).
  2. Set karakter [abc], [^abc], [0-9], jalan pintas \d \w \s (dan kebalikannya \D \W \S), . buat apa pun kecuali newline. Buat huruf non-Inggris pakai \p{L} dengan opsi u.
  3. Pengulangan: + (1+), * (0+), ? (0/1), {n}, {n,m}, {n,}. Tambahin ? di belakang biar nongreedy.
  4. Grup ( ) buat ngelompokin dan nangkep; (?: ) buat ngelompokin tanpa nangkep; | buat pilihan.
  5. ^ dan $ buat awal/akhir input, \b batas kata, (?= ) look-ahead, (?! ) look-ahead negatif.
  6. Mesin regex kerja pakai backtracking; pola kaya ([01]+)+ bisa jadi sangat lambat.
  7. replace bisa pakai $1, $2, $&, atau fungsi. Opsi g bikin semua kecocokan diganti.
  8. Date: bulan mulai dari 0, hari mulai dari 1. getTime() = milidetik sejak 1970.
  9. lastIndex cuma berlaku dengan g/y lewat exec. match dengan g ngembaliin semua string yang cocok; matchAll ngembaliin semua array kecocokan.

Latihan

Hampir gak terhindarkan bahwa, selama ngerjain latihan ini, kamu bakal bingung dan frustrasi sama perilaku yang gak bisa dijelasin dari suatu regular expression. Kadang membantu buat masukin ekspresimu ke alat online kaya debuggex.com buat lihat apakah visualisasinya sesuai dengan yang kamu maksud dan buat bereksperimen dengan cara dia merespons berbagai string input.

Regexp Golf

Code golf adalah istilah yang dipakai buat permainan nyoba nyatain program tertentu dengan karakter sesedikit mungkin. Sama juga, regexp golf adalah praktik nulis regular expression sekecil mungkin buat nyocokin pola tertentu dan cuma pola itu.

Buat setiap item berikut, tulis regular expression buat ngetes apakah pola yang dikasih muncul di string. Regular expression-nya harus cocok cuma dengan string yang berisi polanya. Waktu ekspresimu jalan, lihat apakah kamu bisa bikinnya lebih kecil.

  1. car dan cat
  2. pop dan prop
  3. ferret, ferry, dan ferrari
  4. Kata apa pun yang berakhiran ious
  5. Karakter whitespace diikuti titik, koma, titik dua, atau titik koma
  6. Kata yang lebih panjang dari enam huruf
  7. Kata tanpa huruf e (atau E)

Lihat tabel di ringkasan bab buat bantuan. Tes setiap solusi dengan beberapa string tes.

// Isi regular expression-nya
 
verify(/.../,
       ["my car", "bad cats"],
       ["camper", "high art"]);
 
verify(/.../,
       ["pop culture", "mad props"],
       ["plop", "prrrop"]);
 
verify(/.../,
       ["ferret", "ferry", "ferrari"],
       ["ferrum", "transfer A"]);
 
verify(/.../,
       ["how delicious", "spacious room"],
       ["ruinous", "consciousness"]);
 
verify(/.../,
       ["bad punctuation ."],
       ["escape the period"]);
 
verify(/.../,
       ["Siebentausenddreihundertzweiundzwanzig"],
       ["no", "three small words"]);
 
verify(/.../,
       ["red platypus", "wobbling nest"],
       ["earth bed", "bedrøvet abe", "BEET"]);
 
function verify(regexp, yes, no) {
  // Abaikan latihan yang belum selesai
  if (regexp.source == "...") return;
  for (let str of yes) if (!regexp.test(str)) {
    console.log(`Failure to match '${str}'`);
  }
  for (let str of no) if (regexp.test(str)) {
    console.log(`Unexpected match for '${str}'`);
  }
}

Solusi:

// Isi regular expression-nya
 
verify(/ca[rt]/,
       ["my car", "bad cats"],
       ["camper", "high art"]);
 
verify(/pr?op/,
       ["pop culture", "mad props"],
       ["plop", "prrrop"]);
 
verify(/ferr(et|y|ari)/,
       ["ferret", "ferry", "ferrari"],
       ["ferrum", "transfer A"]);
 
verify(/ious($|\P{L})/u,
       ["how delicious", "spacious room"],
       ["ruinous", "consciousness"]);
 
verify(/\s[.,:;]/,
       ["bad punctuation ."],
       ["escape the dot"]);
 
verify(/\p{L}{7}/u,
       ["Siebentausenddreihundertzweiundzwanzig"],
       ["no", "three small words"]);
 
verify(/(^|\P{L})[^\P{L}e]+($|\P{L})/ui,
       ["red platypus", "wobbling nest"],
       ["earth bed", "bedrøvet abe", "BEET"]);
 
 
function verify(regexp, yes, no) {
  // Abaikan latihan yang belum selesai
  if (regexp.source == "...") return;
  for (let str of yes) if (!regexp.test(str)) {
    console.log(`Failure to match '${str}'`);
  }
  for (let str of no) if (regexp.test(str)) {
    console.log(`Unexpected match for '${str}'`);
  }
}

Gaya Kutipan

Bayangin kamu udah nulis cerita dan pakai tanda kutip tunggal di seluruhnya buat nandain potongan dialog. Sekarang kamu mau ngegantiin semua kutip dialog dengan kutip ganda, sambil ngejaga kutip tunggal yang dipakai di kontraksi kaya aren’t.

Pikirin pola yang ngebedain dua jenis penggunaan kutip ini dan buat pemanggilan method replace yang ngelakuin penggantian yang tepat.

let text = "'I'm the cook,' he said, 'it's my job.'";
// Ubah pemanggilan ini.
console.log(text.replace(/A/g, "B"));
// → "I'm the cook," he said, "it's my job."

Petunjuk:

Solusi paling jelas adalah ngegantiin cuma kutip dengan karakter non-huruf di setidaknya satu sisi, sesuatu kaya /\P{L}'|'\P{L}/u. Tapi kamu juga harus memperhitungkan awal dan akhir baris.

Selain itu, kamu harus mastiin penggantiannya juga nyertain karakter yang cocok dengan pola \P{L} biar mereka gak dibuang. Ini bisa dilakuin dengan ngebungkusnya dalam tanda kurung dan nyertain grupnya di string pengganti ($1, $2). Grup yang gak cocok bakal diganti dengan kosong.

Solusi:

let text = "'I'm the cook,' he said, 'it's my job.'";
 
console.log(text.replace(/(^|\P{L})'|'(\P{L}|$)/gu, '$1"$2'));
// → "I'm the cook," he said, "it's my job."

Angka Lagi

Tulis ekspresi yang cuma cocok dengan angka gaya JavaScript. Dia harus mendukung tanda minus atau plus opsional di depan angka, titik desimal, dan notasi eksponen, 5e-3 atau 1E10, lagi-lagi dengan tanda opsional di depan eksponennya. Juga catat bahwa gak perlu ada digit di depan atau setelah titik, tapi angkanya gak boleh cuma titik aja. Yaitu, .5 dan 5. adalah angka JavaScript yang valid, tapi titik sendirian bukan.

// Isi regular expression ini.
let number = /^...$/;
 
// Tes:
for (let str of ["1", "-1", "+15", "1.55", ".5", "5.",
                 "1.3e2", "1E-4", "1e+12"]) {
  if (!number.test(str)) {
    console.log(`Failed to match '${str}'`);
  }
}
for (let str of ["1a", "+-1", "1.2.3", "1+1", "1e4.5",
                 ".5.", "1f5", "."]) {
  if (number.test(str)) {
    console.log(`Incorrectly accepted '${str}'`);
  }
}

Petunjuk:

Pertama, jangan lupa backslash di depan titik.

Nyocokin tanda opsional di depan angka, juga di depan eksponen, bisa dilakuin dengan [+\-]? atau (\+|-|) (plus, minus, atau kosong).

Bagian yang lebih rumit dari latihan ini adalah masalah nyocokin "5." maupun ".5" tanpa juga nyocokin ".". Buat ini, solusi yang bagus adalah pakai operator | buat misahin dua kasusnya, entah satu digit atau lebih yang opsional diikuti titik dan nol digit atau lebih atau titik diikuti satu digit atau lebih.

Terakhir, buat bikin e-nya case insensitive, entah tambahin opsi i ke regular expression-nya atau pakai [eE].

Solusi:

// Isi regular expression ini.
let number = /^[+\-]?(\d+(\.\d*)?|\.\d+)([eE][+\-]?\d+)?$/;
 
// Tes:
for (let str of ["1", "-1", "+15", "1.55", ".5", "5.",
                 "1.3e2", "1E-4", "1e+12"]) {
  if (!number.test(str)) {
    console.log(`Failed to match '${str}'`);
  }
}
for (let str of ["1a", "+-1", "1.2.3", "1+1", "1e4.5",
                 ".5.", "1f5", "."]) {
  if (number.test(str)) {
    console.log(`Incorrectly accepted '${str}'`);
  }
}