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:

PendekatanHasil 2 + 3 * 4KelebihanKekurangan
Kiri ke kanan20satu loop sederhana, mudah dilacak dan diujihasil membeda dari konvensi matematika
Dengan presedensi14sesuai konvensi matematikabutuh 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:

cpp kalkulator-v1.cpp (versi awal, tanpa validasi)
// 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:

  1. tokenisasi — dua pointer indeks berjalan di atas baris: yang lompat melewati spasi, yang maju sampai menemukan spasi berikutnya. Hasil pemotongan berupa string_view lewat substr, jadi tidak ada satu karakter pun yang disalin — token hanyalah jendela ke baris asli. Spasi ganda tidak menjadi masalah karena loop pertama di dalam while terus melompati spasi sebelum mengukur token baru.
  2. parseAngka dengan std::stod — satu baris yang kelihatannya memadai, tetapi menyimpan dua utang. Pertama, stod melempar std::invalid_argument bila teks bukan angka; kita tidak menangkapnya, jadi input jahil akan melumpuhkan program. Kedua, lebih licik: stod("12abc") mengembalikan 12 tanpa protes — sisanya abc dibuang diam-diam. Kedua celah ini dibayar di Bagian 3 dengan std::from_chars.
  3. adalahOperator — whitelist lima karakter yang dikenal. Bentuk sederhana ini kelak langsung terbaca ketika kita menolak operator asing di Bagian 3.
  4. terapkan — satu titik keputusan untuk semua operasi. Perhatikan std::fmod untuk %: pada tipe double, sisa pembagian dihitung versi riil (12.5 % 5 adalah 2.5), dan tandanya mengikuti pembilang. Pembagi nol belum diperiksa — 5 / 0 pada double tidak melempar, ia menghasilkan inf; kita benahi di Bagian 4.
  5. evaluasi — pola akumulator: angka pertama menjadi nilai awal, lalu loop i += 2 melangkah dari pasangan operator-angka ke pasangan berikutnya. Susunan token yang sah selalu ganjil: angka, operator, angka, operator, angka, dan seterusnya.
  6. main — tetap tipis seperti kerangka Bagian 1; std::println("{}", ...) yang mengurus tampilan angka, termasuk membuang .0 dari 42.0.

Mencoba Versi Pertama #

Kompilasi dan jalankan dengan masukan yang seluruhnya sah:

bash
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-v1

Output yang Dihasilkan:

text
42
25
20
5
2
3.5
0.30000000000000004

Empat 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) .