Penanganan string

Mengolah nilai String, slice per byte dan per code point, serta konversi antar bentuk.

Daftar isi

String pada Mojo disimpan sebagai deret byte UTF-8, sehingga panjang sebuah teks dapat berarti tiga hal yang berbeda, yaitu jumlah byte, jumlah code point, dan jumlah grapheme. Topik ini membahas pengukuran ketiganya, pemotongan teks (slice) per byte dan per code point, serta konversi antar bentuk seperti bilangan menjadi String dan penyisipan nilai lewat TString. Setelah topik ini dibaca, teks dapat diolah dengan memperhatikan pengodean UTF-8 tanpa mengandalkan asumsi panjang tunggal.

Tiga cara mengukur panjang#

Metode byte_length() menghitung jumlah byte UTF-8, metode count_codepoints() menghitung jumlah code point, dan metode count_graphemes() menghitung jumlah grapheme. Code point adalah satu satuan karakter Unicode, sedangkan grapheme adalah satu karakter yang dilihat pengguna dan dapat tersusun dari lebih dari satu code point. Pada teks ASCII murni ketiganya bernilai sama, karena setiap karakternya memakan satu byte dan tidak ada karakter gabungan.

Fungsi len yang berlaku pada koleksi tidak berlaku untuk String. Compiler menolak pemanggilannya dengan pesan yang menjelaskan ambiguitas panjang tersebut, dan bagian Kesalahan umum memperlihatkan pesannya secara utuh. Penghitungan panjang selalu melalui salah satu dari tiga metode di atas, sehingga pilihan metodenya menyatakan makna yang dimaksud.

mojo hitung.mojo
def main():
    var s = "Halo Mojo 🎉"
    print(s.byte_length())
    print(s.count_codepoints())
    print(s.count_graphemes())
bash
# jalankan program penghitungan byte dan code point
mojo run hitung.mojo
text Keluaran
14
11
11

Keluaran 14 adalah jumlah byte, sedangkan dua baris 11 berikutnya adalah jumlah code point dan jumlah grapheme. Emoji perayaan pada contoh menyimpan empat byte UTF-8 tetapi dihitung satu code point sekaligus satu grapheme. Selisih tiga byte itulah yang membedakan penghitungan byte dari penghitungan code point pada String yang memuat karakter non-ASCII.

Slice per byte dan per code point#

Pemotongan String menyebutkan makna posisinya secara eksplisit, yaitu s[byte=a:b] untuk posisi byte dan s[codepoint=a:b] untuk posisi code point. Rentang a:b mengikuti konvensi rentang Mojo, yaitu batas awal tercakup dan batas akhir tidak. Bentuk polos s[a:b] tanpa label tidak tersedia karena rentang yang sama dapat berarti teks yang berbeda tergantung makna posisinya.

Berkas slice-string.mojo memotong satu String dengan kedua makna posisi, termasuk mengambil emoji empat byte di ujungnya.

mojo slice-string.mojo
def main():
    var s = "Mojo Bahasa 🎉"
    print(s[byte=0:4])
    print(s[codepoint=5:11])
    print(s[byte=12:16])
    print(s[codepoint=12:13])
bash
# jalankan program potongan per byte dan per code point
mojo run slice-string.mojo
text Keluaran
Mojo
Bahasa
🎉
🎉

Keluaran Mojo berasal dari potongan byte 0:4, sedangkan Bahasa berasal dari potongan code point 5:11 yang menjangkau enam karakter tersebut. Dua baris berikutnya mengambil emoji dengan potongan byte 12:16 dan potongan code point 12:13, dan keduanya menghasilkan karakter yang sama karena emoji itu adalah satu code point empat byte. Contoh ini menunjukkan bahwa posisi byte dan posisi code point membutuhkan jangkauan yang berbeda untuk sampai pada teks yang sama.

Posisi byte wajib jatuh pada batas code point. Potongan byte yang membelah satu code point di tengah diperiksa saat program berjalan, dan program berhenti dengan pesan error batas code point. Pemilihan makna posisi menyesuaikan tujuan, yaitu posisi byte cocok untuk pekerjaan tata letak memori dan format biner, sedangkan posisi code point cocok untuk teks yang dilihat pengguna.

Konversi dan format#

Konversi antar bentuk dilakukan melalui konstruktor String. Konstruktor String(5) mengubah bilangan bulat menjadi teksnya, sedangkan konstruktor variadic String("a", 5) menyambung argumennya menjadi satu String. Konversi sebaliknya dari teks ke bilangan tidak disediakan prelude, dan bagian Kesalahan umum menunjukkan apa yang terjadi apabila int("123") dipanggil.

Penyisipan nilai ke dalam teks menggunakan TString, yaitu literal t"x = {x}" yang menyimpan pola teks beserta nilai yang disisipkan, dan print menampilkan hasil penyisipannya. Metode format() tetap tersedia untuk mengisi tanda kurung kurawal {} secara berurutan, sedangkan raw string r"..." menampilkan urutan karakternya apa adanya tanpa menafsirkan escape.

mojo konversi.mojo
def main():
    print(String(5))
    print(String("a", 5))
    var x = 5
    var s = t"x = {x}"
    print(s)
    print("{} {}".format(True, "halo"))
    print(r"baris\nbaru")
bash
# jalankan program konversi dan format teks
mojo run konversi.mojo
text Keluaran
5
a5
x = 5
True halo
baris\nbaru

Keluaran 5 dan a5 berasal dari kedua bentuk konstruktor String. Baris x = 5 adalah hasil TString yang mengganti {x} dengan nilai variabelnya, sedangkan True halo berasal dari format() yang mengisi dua tanda {} berurutan. Baris terakhir menampilkan backslash dan huruf n apa adanya karena raw string tidak menafsirkan escape \n.

Kesalahan umum#

Kesalahan pertama adalah menghitung panjang String dengan len. Ambiguitas tiga makna panjang String membuat compiler menolak pemanggilan itu agar maknanya selalu eksplisit. Berkas len-string.mojo memuat pemanggilan semacam itu.

mojo len-string.mojo
def main():
    var s = "Halo Mojo"
    print(len(s))
bash
# len pada String ditolak compiler
mojo run len-string.mojo
text Keluaran
/home/kal/my-project/content/mojo/koleksi-dan-string/penanganan-string/len-string.mojo:3:11: error: `len(String/StringSlice)` is not supported because Mojo strings are UTF-8 encoded, so a single length is ambiguous: it could mean the number of UTF-8 bytes, the number of Unicode code points, or the number of user-visible characters (grapheme clusters). Use `s.byte_length()` or `len(s.bytes())` for the number of UTF-8 bytes, `len(s.codepoints())` for Unicode code points, or `len(s.graphemes())` for grapheme clusters.
    print(len(s))
          ^~~
<unknown>:0: note: 'len' declared here
/home/kal/.pixi/envs/mojo/bin/mojo: error: failed to parse the provided Mojo source module

Pesan error menyebutkan tiga kemungkinan makna panjang beserta penggantinya, yaitu s.byte_length() untuk byte, len(s.codepoints()) untuk code point, dan len(s.graphemes()) untuk grapheme. Perbaikan yang paling langsung adalah memilih salah satu metode count_* sesuai makna yang dimaksud. Dengan begitu, pilihan makna panjang tersimpan dalam kode dan tidak bergantung pada penafsiran pembaca.

Kesalahan kedua adalah memotong String dengan rentang polos tanpa label makna posisi. Rentang polos dipandang ambigu oleh compiler karena UTF-8 membuat satu rentang dapat dibaca sebagai byte maupun code point. Berkas slice-polos.mojo memuat potongan semacam itu.

mojo slice-polos.mojo
def main():
    var s = "Mojo Bahasa"
    print(s[0:4])
bash
# potongan polos tanpa label ditolak compiler
mojo run slice-polos.mojo
text Keluaran
/home/kal/my-project/content/mojo/koleksi-dan-string/penanganan-string/slice-polos.mojo:3:12: error: String does not support direct positional slicing like `s[a:b]` because Mojo strings are UTF-8 encoded, and the same range can mean different things. Use `s[byte=a:b]` to slice by raw UTF-8 byte positions, or `s[codepoint=a:b]` to slice by Unicode code points.
    print(s[0:4])
          ~^~~~~
<unknown>:0: note: '__getitem__' declared here
/home/kal/.pixi/envs/mojo/bin/mojo: error: failed to parse the provided Mojo source module

Pesan error menawarkan kedua bentuk yang sah, yaitu s[byte=a:b] untuk posisi byte mentah dan s[codepoint=a:b] untuk posisi code point. Perbaikannya adalah memilih salah satunya sebagaimana diperagakan pada berkas slice-string.mojo. Rentang yang sama kemudian memotong teks yang sama pula pada setiap eksekusi program.

Kesalahan ketiga adalah mengubah teks menjadi bilangan lewat int sebagaimana kebiasaan pada bahasa lain. Deklarasi int tidak ada pada prelude Mojo, sehingga pemanggilannya ditolak compiler. Berkas int-string.mojo memuat pemanggilan semacam itu.

mojo int-string.mojo
def main() raises:
    var n = int("123")
    print(n)
bash
# int dari teks tidak ada pada prelude
mojo run int-string.mojo
text Keluaran
/home/kal/my-project/content/mojo/koleksi-dan-string/penanganan-string/int-string.mojo:2:13: error: use of unknown declaration 'int'
    var n = int("123")
            ^~~
/home/kal/.pixi/envs/mojo/bin/mojo: error: failed to parse the provided Mojo source module

Pesan use of unknown declaration 'int' menyatakan tidak adanya deklarasi tersebut pada lingkup program. Cara yang konsisten dengan bahasa ini adalah menjaga nilai numerik tetap dalam tipe angka sejak awal, sehingga kebutuhan konversi dari teks tidak muncul. Apabila teks berisi angka dari luar program, lakukan penguraian melalui mekanisme yang memang menyediakannya, misalnya jembatan Python.

Ringkasan#

  • Panjang String dihitung lewat byte_length(), count_codepoints(), atau count_graphemes(), dan fungsi len ditolak compiler karena panjang tunggal tidak bermakna pada UTF-8.
  • Pemotongan String wajib menyebut makna posisinya lewat s[byte=a:b] atau s[codepoint=a:b], dan posisi byte harus jatuh pada batas code point.
  • Konstruktor String mengubah bilangan menjadi teks dan menyambung argumen, sedangkan konversi teks ke bilangan tidak tersedia pada prelude.
  • TString menyisipkan nilai ke dalam pola teks, format() mengisi tanda {} berurutan, dan raw string menampilkan escape apa adanya.

Referensi#