Kerangka REPL dari Bagian 1 sudah membaca baris dan menandainya; sekarang kita isi titik kosong itu dengan mesin perhitungan sungguhan. Bagian ini menghasilkan versi pertama kalkulator yang benar-benar berhitung: ia memotong baris menjadi token, mengurai angka, lalu menerapkan operator +, -, *, /, dan % satu per satu dari kiri ke kanan. Versi ini sengaja kita buat polos — ia menganggap semua masukan sah — supaya mekanisme inti terlihat jernih tanpa tercampur penanganan galat, yang menjadi tugas Bagian 3 dan 4.
Keputusan Desain: Kiri ke Kanan, Tanpa Presedensi #
Pertanyaan desain pertama: apa yang dilakukan kalkulator dengan 2 + 3 * 4? Matematika sekolah menjawab 14 karena perkalian didahulukan (presedensi). Versi kita menjawab 20 karena evaluasi dilakukan kiri ke kanan tanpa keistimewaan:
| Pendekatan | Hasil 2 + 3 * 4 | Kelebihan | Kekurangan |
|---|---|---|---|
| Kiri ke kanan | 20 | satu loop sederhana, mudah dilacak dan diuji | hasil membeda dari konvensi matematika |
| Dengan presedensi | 14 | sesuai konvensi matematika | butuh parser bertingkat (recursive descent) |
Kita memilih kiri ke kanan untuk versi ini dengan alasan konstruksi, bukan kemalasan. Evaluasi kiri ke kanan cukup dijalan oleh satu variable accumulator: mulai dari angka pertama, lalu untuk setiap pasangan operator-angka berikutnya, akumulator diperbarui. Tidak ada tumpukan, tidak ada pohon ekspresi, tidak ada rekursi. Mesin presedensi sendiri — umumnya parser recursive descent yang mengganti loop dengan pemanggilan function berjenjang — adalah topik yang layak mendapat ruangnya sendiri; setelah proyek ini tuntas, menambahkannya berarti mengganti isi evaluasi tanpa menyentuh tokenisasi maupun antarmuka. Keputusan menunda itu murah justru karena pemisahan function murni kita dari Bagian 1.
Versi Lengkap yang Kita Bangun #
Simpan program berikut sebagai kalkulator-v1.cpp — inilah kerangka Bagian 1 yang sudah terisi penuh:
// kalkulator-v1.cpp — versi awal: alur REPL lengkap, input diasumsikan selalu sah.
// Catatan: versi ini sengaja belum memvalidasi apa pun (diperbaiki di Bagian 3 dan 4).
#include <cmath> // std::fmod
#include <cstddef> // std::size_t
#include <iostream> // std::getline, std::cin
#include <print> // std::println
#include <string>
#include <string_view>
#include <vector>
// tahap 1: tokenisasi — potong baris menjadi token berdasarkan spasi
std::vector<std::string_view> tokenisasi(std::string_view baris) {
std::vector<std::string_view> hasil;
std::size_t i = 0;
while (i < baris.size()) {
while (i < baris.size() && (baris[i] == ' ' || baris[i] == '\t')) {
++i;
}
std::size_t mulai = i;
while (i < baris.size() && baris[i] != ' ' && baris[i] != '\t') {
++i;
}
if (i > mulai) {
hasil.push_back(baris.substr(mulai, i - mulai));
}
}
return hasil;
}
// tahap 2: parsing angka — DIASUMSIKAN token selalu angka sah
double parseAngka(std::string_view token) {
return std::stod(std::string(token));
}
// tahap 3: operasi aritmetika
bool adalahOperator(std::string_view token) {
return token == "+" || token == "-" || token == "*"
|| token == "/" || token == "%";
}
double terapkan(std::string_view op, double kiri, double kanan) {
if (op == "+") return kiri + kanan;
if (op == "-") return kiri - kanan;
if (op == "*") return kiri * kanan;
if (op == "/") return kiri / kanan; // belum ada pemeriksaan pembagi nol
return std::fmod(kiri, kanan); // op == "%"
}
// tahap 4: evaluasi kiri ke kanan tanpa presedensi
double evaluasi(std::string_view baris) {
const auto token = tokenisasi(baris);
double akumulator = parseAngka(token[0]);
for (std::size_t i = 1; i + 1 < token.size(); i += 2) {
akumulator = terapkan(token[i], akumulator, parseAngka(token[i + 1]));
}
return akumulator;
}
// antarmuka: loop REPL
int main() {
std::string baris;
while (std::getline(std::cin, baris)) {
std::println("{}", evaluasi(baris));
}
}Mari kita bedah bagian yang baru:
tokenisasi— dua pointer indeks berjalan di atasbaris: yang lompat melewati spasi, yang maju sampai menemukan spasi berikutnya. Hasil pemotongan berupastring_viewlewatsubstr, jadi tidak ada satu karakter pun yang disalin — token hanyalah jendela ke baris asli. Spasi ganda tidak menjadi masalah karena loop pertama di dalamwhileterus melompati spasi sebelum mengukur token baru.parseAngkadenganstd::stod— satu baris yang kelihatannya memadai, tetapi menyimpan dua utang. Pertama,stodmelemparstd::invalid_argumentbila teks bukan angka; kita tidak menangkapnya, jadi input jahil akan melumpuhkan program. Kedua, lebih licik:stod("12abc")mengembalikan12tanpa protes — sisanyaabcdibuang diam-diam. Kedua celah ini dibayar di Bagian 3 denganstd::from_chars.adalahOperator— whitelist lima karakter yang dikenal. Bentuk sederhana ini kelak langsung terbaca ketika kita menolak operator asing di Bagian 3.terapkan— satu titik keputusan untuk semua operasi. Perhatikanstd::fmoduntuk%: pada tipedouble, sisa pembagian dihitung versi riil (12.5 % 5adalah2.5), dan tandanya mengikuti pembilang. Pembagi nol belum diperiksa —5 / 0padadoubletidak melempar, ia menghasilkaninf; kita benahi di Bagian 4.evaluasi— pola akumulator: angka pertama menjadi nilai awal, lalu loopi += 2melangkah dari pasangan operator-angka ke pasangan berikutnya. Susunan token yang sah selalu ganjil: angka, operator, angka, operator, angka, dan seterusnya.main— tetap tipis seperti kerangka Bagian 1;std::println("{}", ...)yang mengurus tampilan angka, termasuk membuang.0dari42.0.
Mencoba Versi Pertama #
Kompilasi dan jalankan dengan masukan yang seluruhnya sah:
g++ -std=c++23 -Wall -Wextra kalkulator-v1.cpp -o kalkulator-v1
printf '12 + 30\n100 / 4\n2 + 3 * 4\n10 - 2 - 3\n12 % 5\n7 / 2\n0.1 + 0.2\n' | ./kalkulator-v1Output yang Dihasilkan:
42
25
20
5
2
3.5
0.30000000000000004Empat hal patut dicermati dari keluaran tersebut. 2 + 3 * 4 menghasilkan 20 — bukti nyata keputusan kiri ke kanan kita, bukan teori. 10 - 2 - 3 menghasilkan 5, karena pengurangan dihitung berurutan dari kiri; kalau dihitung dari kanan, hasilnya 11. 7 / 2 menghasilkan 3.5 — pembagian tetap riil karena kedua operan double, tidak ada pembagian bulat. Dan 0.1 + 0.2 menghasilkan 0.30000000000000004: bukan bug kalkulator, melainkan sifat representasi biner untuk pecahan desimal — angka seperti 0.1 tidak punya representasi biner berhingga, sehingga jumlahnya nyaris namun tidak persis 0.3.
Versi pertama ini berhitung dengan benar selama pengguna berperilaku baik. Bagian berikutnya membuang asumsi itu: kita perketat penguraian angka dengan std::from_chars, menolak operator asing, dan menolak susunan token yang tidak utuh — semua dengan pesan galat yang menyebutkan token bermasalahnya.
Rujukan resmi: std::fmod (cppreference) , std::string_view::substr (cppreference) .