Proyek keempat kita mengubah berkas CSV menjadi laporan statistik: alat kecil yang membaca data penjualan, memvalidasi setiap barisnya, menghitung ringkasan per produk, lalu mencetak laporan siap baca. Sebelum menyentuh validasi dan perhitungan, Bagian 1 memfokuskan diri pada keterampilan dasar yang menopang semuanya: membaca teks CSV dan memecahnya menjadi kolom yang rapi. Format CSV (comma-separated values) dipakai hampir semua sistem — spreadsheet, ekspor basis data, log aplikasi — sehingga parser kecil ini adalah investasi yang terus terpakai. Kita juga menetapkan satu keputusan penting sejak awal: dataset ditanam di dalam kode, supaya seluruh contoh proyek ini deterministik dan bisa dicoba tanpa berkas apa pun.
Alur Empat Tahap Alat Analisis #
Sebelum menulis kode, kita petakan dulu alur data dari berkas mentah sampai laporan jadi. Setiap baris CSV melewati empat tahap yang sama: dibaca baris demi baris, dipecah menjadi kolom, divalidasi, lalu diagregasikan per produk — dan hasil agregasi itulah yang dicetak sebagai laporan. Karena setiap tahap ditulis sebagai function terpisah, kita bisa menguji dan membedahnya satu per satu, pola yang sama dengan proyek kalkulator sebelumnya.
Diagram di atas memperlihatkan dua jalur keluaran yang penting kita bedakan sejak Bagian 1. Jalur sukses mengalir dari pembacaan baris menuju agregasi lalu laporan; jalur gagal berhenti di gerbang validasi dan dicatat sebagai daftar galat lengkap dengan nomor barisnya, bukan dibuang diam-diam. Kedua jalur bertemu lagi di laporan: baris valid mengisi angka statistik, sedangkan baris ditolak dilaporkan di bagian atas laporan. Prinsip “lapor, jangan sembunyikan” ini kita pegang sampai akhir proyek.
Dataset Penjualan dan Keputusan Tertanam #
Dataset latihan kita adalah catatan penjualan alat tulis selama empat hari, dengan empat kolom per baris: tanggal, produk, jumlah, dan harga. Isinya persis sama dengan berkas data/penjualan.csv yang tersimpan di bundle proyek:
tanggal,produk,jumlah,harga
2025-11-01,Buku Tulis,10,3500
2025-11-01,Pensil 2B,25,2000
2025-11-02,Pulpen Gel,15,4500
2025-11-03,Buku Tulis,7,3750
2025-11-03,Penghapus,30,1500
2025-11-04,Pensil 2B,20,2000Beberapa produk sengaja muncul lebih dari sekali — Buku Tulis dua kali dengan harga berbeda, begitu pula Pensil 2B — supaya Bagian 3 nanti punya bahan agregasi yang berarti, bukan sekadar satu baris per produk.
Dalam proyek ini dataset kita tanam langsung di dalam kode sebagai constexpr std::string_view, konstanta teks yang sudah tersedia sejak kompilasi. Keputusan ini bukan sekadar gaya: program yang membaca berkas eksternal hanya bisa dijalankan bila berkasnya ikut dibawa, sementara salinan tertanam membuat setiap contoh self-contained dan keluarannya identik di lingkungan mana pun. Berkas data/penjualan.csv tetap kita sertakan di bundle dengan isi yang sama persis, jadi Anda bebas memilih cara menjalankannya tanpa mengubah hasil. Program final di Bagian 4 memakai pola gabungan: berkas eksternal dibaca bila ada, dan salinan tertanam menjadi cadangan bila berkas tidak ditemukan.
constexpr std::string_view DATA_CSV = /* salinan data/penjualan.csv */;
std::istringstream muatData() {
std::string isi;
if (std::ifstream berkas{"data/penjualan.csv"}) {
std::ostringstream penampung;
penampung << berkas.rdbuf(); // sedot seluruh isi berkas
isi = penampung.str();
} else {
isi = DATA_CSV; // berkas tidak ada: pakai salinan tertanam
}
return std::istringstream{isi};
}Tiga hal layak dicatat dari pola di atas. if (std::ifstream berkas{...}) mendeklarasikan berkas di dalam kondisi if, sehingga objeknya hanya hidup di dalam blok itu dan otomatis tertutup saat keluar — pola RAII yang muncul terus di kode C++. penampung << berkas.rdbuf() menyalin seluruh isi berkas lewat stream buffer tanpa loop manual. Dan karena kedua sumber data berisi teks yang sama, fungsi ini selalu mengembalikan stream dengan konten identik — inilah yang membuat laporan kita bisa dijadikan golden output saat pengujian.
Parser Koma Sederhana #
Tahap pemecahan kolom ditulis sebagai function pisah — satu loop kecil yang memotong string pada setiap tanda koma. Mekanismenya: find mencari posisi koma berikutnya mulai dari indeks awal, substr menyalin potongan sebelum koma, lalu awal melompat ke belakang koma yang baru saja ditemukan. Ketika find mengembalikan std::string::npos, konstanta khusus yang berarti “tidak ditemukan”, potongan terakhir ditambahkan dan function selesai.
std::vector<std::string> pisah(const std::string& teks, char pemisah) {
std::vector<std::string> hasil;
std::string::size_type awal = 0;
while (true) {
const std::string::size_type posisi = teks.find(pemisah, awal);
if (posisi == std::string::npos) {
hasil.push_back(teks.substr(awal));
return hasil;
}
hasil.push_back(teks.substr(awal, posisi - awal));
awal = posisi + 1;
}
}Mari kita bedah baris demi baris:
std::string::size_type awal = 0;— indeks awal potongan berikutnya. Tipesize_typeadalah tipe tak bertanda milikstd::string, sama denganstd::size_t; memakainya menghindari peringatan perbandingan bertanda-tak bertanda dari-Wall.teks.find(pemisah, awal)— mencari koma pertama pada atau sesudahawal. Hasilnya posisi koma, ataunposbila tidak ada lagi.hasil.push_back(teks.substr(awal, posisi - awal));— menyalin potongan dariawalsepanjangposisi - awalkarakter, yaitu tepat teks sebelum koma. Bilaawalmenunjuk langsung ke koma (kolom kosong), panjangnya nol dan string kosong pun ikut dimasukkan.return hasil;di dalamif— bila koma tidak ditemukan lagi, sisa teks dariawalsampai akhir adalah kolom terakhir; function selesai di sini tanpa perlubreaktambahan.
Perhatikan konsekuensi pentingnya: kolom kosong tetap terhitung. Baris a,,c menghasilkan tiga kolom, bukan dua — dan justru itu yang kita inginkan, karena kolom yang hilang harus terlihat, bukan runtuh tanpa jejak.
Batasan Kutip dan Kapan Parser Ini Cukup #
Parser koma punya batasan yang harus kita akui sejak awal. Standar de facto CSV (RFC 4180) mengizinkan nilai yang mengandung koma ditulis di dalam tanda kutip, misalnya "Tas Kulit, ukuran Besar",10,3500. Parser kita tidak mengenal tanda kutip, sehingga baris seperti itu terpecah menjadi lima kolom dan nama produknya terpotong di tengah. Batasan ini kita terima dengan dua alasan. Pertama, dataset latihan kita sengaja tidak memakai koma di dalam nama produk. Kedua — dan lebih penting — gerbang validasi di Bagian 2 menolak baris dengan jumlah kolom yang salah, jadi parser yang terbatas menghasilkan kegagalan yang keras dan terlihat, bukan data yang salah tanpa suara. Bila suatu saat Anda perlu dukungan kutip, tambahannya berupa state machine kecil: satu boolean diDalamKutip yang mengubah perilaku pisah saat bertemu karakter " — latihan yang bagus setelah proyek ini selesai.
Demo: Memecah Dataset Menjadi Kolom #
Mari kita buktikan parser ini bekerja. Program demo berikut menanamkan dataset, membacanya baris demi baris dengan std::getline, lalu mencetak hasil pemecahan setiap baris — plus satu baris rusak untuk menunjukkan perlakuan kolom kosong:
// parser-csv.cpp — demo parser CSV sederhana dari Bagian 1.
//
// Dataset ditanam di dalam kode (constexpr std::string_view) supaya
// demo bisa dijalankan di mana pun tanpa berkas eksternal dan output
// yang dicetak selalu sama persis.
//
// g++ -std=c++23 -Wall -Wextra parser-csv.cpp -o parser-csv
// ./parser-csv
#include <iostream>
#include <sstream>
#include <string>
#include <string_view>
#include <vector>
// Salinan data/penjualan.csv yang ditanam di dalam program.
constexpr std::string_view DATA_CSV =
"tanggal,produk,jumlah,harga\n"
"2025-11-01,Buku Tulis,10,3500\n"
"2025-11-01,Pensil 2B,25,2000\n"
"2025-11-02,Pulpen Gel,15,4500\n"
"2025-11-03,Buku Tulis,7,3750\n"
"2025-11-03,Penghapus,30,1500\n"
"2025-11-04,Pensil 2B,20,2000\n";
// Memecah satu baris CSV berdasarkan tanda koma. Kolom kosong tetap
// dihasilkan, sehingga baris yang kehilangan kolom bisa terdeteksi.
//
// Batasan parser ini: tidak ada dukungan tanda kutip, jadi nilai kolom
// tidak boleh mengandung koma (dibahas di akhir bagian ini).
std::vector<std::string> pisah(const std::string& teks, char pemisah) {
std::vector<std::string> hasil;
std::string::size_type awal = 0;
while (true) {
const std::string::size_type posisi = teks.find(pemisah, awal);
if (posisi == std::string::npos) {
hasil.push_back(teks.substr(awal));
return hasil;
}
hasil.push_back(teks.substr(awal, posisi - awal));
awal = posisi + 1;
}
}
int main() {
std::istringstream masuk{std::string(DATA_CSV)};
std::string baris;
int nomor = 0;
while (std::getline(masuk, baris)) {
++nomor;
const std::vector<std::string> kolom = pisah(baris, ',');
std::cout << "baris " << nomor << " -> " << kolom.size() << " kolom:";
for (const std::string& k : kolom) {
std::cout << " [" << k << ']';
}
std::cout << '\n';
}
// Kolom kosong tetap terhitung — bahan penting untuk validasi Bagian 2.
const std::vector<std::string> rusak = pisah("2025-11-05,Penggaris,,", ',');
std::cout << "baris rusak -> " << rusak.size()
<< " kolom, dua di antaranya kosong\n";
}g++ -std=c++23 -Wall -Wextra parser-csv.cpp -o parser-csv
./parser-csvOutput yang Dihasilkan:
baris 1 -> 4 kolom: [tanggal] [produk] [jumlah] [harga]
baris 2 -> 4 kolom: [2025-11-01] [Buku Tulis] [10] [3500]
baris 3 -> 4 kolom: [2025-11-01] [Pensil 2B] [25] [2000]
baris 4 -> 4 kolom: [2025-11-02] [Pulpen Gel] [15] [4500]
baris 5 -> 4 kolom: [2025-11-03] [Buku Tulis] [7] [3750]
baris 6 -> 4 kolom: [2025-11-03] [Penghapus] [30] [1500]
baris 7 -> 4 kolom: [2025-11-04] [Pensil 2B] [20] [2000]
baris rusak -> 4 kolom, dua di antaranya kosongBacaan yang menarik dari keluaran itu: setiap baris dataset menghasilkan tepat empat kolom, dan baris pertama — header tanggal,produk,jumlah,harga — untuk sementara diperlakukan sama seperti data (pemeriksaan header baru kita pasang di Bagian 2). Baris rusak 2025-11-05,Penggaris,, tetap menghasilkan empat kolom dengan dua di antaranya string kosong; di tahap parser itu bukan galat, tetapi nanti gerbang validasi akan menolaknya karena kolom jumlah berisi teks yang bukan angka. Parser dan dataset sudah siap. Masalahnya, pisah menerima apa saja — kolom jumlah berisi "dua" pun lolos di bagian ini. Bagian 2 memasang gerbangnya: std::from_chars yang ketat, tujuh pemeriksaan berurutan, dan pelaporan nomor baris untuk setiap baris yang ditolak.
Rujukan resmi: std::string::find (cppreference) , std::basic_istringstream (cppreference) , std::string_view (cppreference) .