Bagian 1 menghasilkan kolom yang rapi, tetapi semua baris kita masih diperlakukan sama — seolah dataset selalu sempurna. Kenyataannya berkas CSV hasil ekspor sering bernoda: kolom hilang karena koma terlewat, angka terketik sebagai teks "dua", harga menyisakan karakter asing dari proses salin-tempel. Bagian 2 memasang gerbang validasi di antara parser dan perhitungan: setiap baris diperiksa lewat tujuh gerbang berurutan, baris yang lolos dikemas menjadi struct siap hitung, dan baris yang gagal dilaporkan lengkap dengan nomor baris serta alasannya. Prinsip yang kita pegang sejak awal: data yang meragukan tidak pernah boleh masuk perhitungan — lebih baik laporan menolak lima dari delapan baris daripada total pendapatan yang bergeser tanpa tanda.
Prinsip Fail-Closed #
Ketika sebuah baris meragukan, ada dua kebijakan yang mungkin dipilih. Fail-open membiarkan perhitungan tetap berjalan: baris yang bermasalah diabaikan diam-diam atau diisi nilai default seperti nol. Fail-closed menolak baris itu secara eksplisit dan melaporkannya. Perbandingan akibatnya di laporan bisa kita petakan begini:
| Kebijakan | Perlakuan baris rusak | Akibatnya di laporan |
|---|---|---|
| Fail-open | diabaikan atau diisi 0 tanpa catatan | total bergeser tanpa jejak; siapa pun yang memeriksa sulit merekonstruksi sebabnya |
| Fail-closed | baris ditolak, dicatat sebagai baris N: alasan | jumlah baris ditolak transparan; angka yang tersisa bisa dipertanggungjawabkan |
Kebijakan fail-closed terasa di tiga tempat dalam kode bagian ini. keAngka mengembalikan std::optional<long long> sehingga kegagalan konversi menjadi bagian dari tipe kembalian, bukan exception yang bisa luput ditangkap. parseBaris mengemas hasilnya dalam std::expected<BarisPenjualan, std::string> yang membawa pesan galat spesifik. Dan loop pembaca menampung semua galat ke dalam vector — tidak ada satu pun kegagalan yang ditelan tanpa suara.
keAngka: from_chars yang Ketat #
Alat konversi angka yang paling sering dipakai pemula, std::stoi, menyimpan dua utang yang sudah kita bayar di proyek kalkulator: ia melempar exception untuk input jahil, dan ia diam-diam menerima 12ab sebagai 12. Untuk data CSV kita butuh kebalikannya — tanpa exception dan menolak parsial — dan std::from_chars dari header <charconv> memberikan tepat itu. Perilaku ketatnya bisa kita petakan (semua baris di tabel ini diverifikasi oleh harness uji di Bagian 4):
| Masukan | Hasil keAngka | Alasan |
|---|---|---|
"42" | 42 | seluruh teks terurai menjadi angka |
"-7" | -7 | tanda minus sah untuk bilangan bulat |
"12ab" | gagal | parser berhenti di a; ada sisa teks |
"+7" | gagal | mode desimal from_chars tidak menerima tanda plus |
" 7" | gagal | spasi tidak dilewati otomatis |
"" | gagal | tidak ada satu karakter pun yang terurai |
"dua" | gagal | bukan digit |
std::optional<long long> keAngka(const std::string& teks) {
long long nilai = 0;
const char* awal = teks.data();
const char* akhir = awal + teks.size();
auto [ptr, ec] = std::from_chars(awal, akhir, nilai);
if (ec != std::errc{} || ptr != akhir) {
return std::nullopt;
}
return nilai;
}Bedah baris demi baris:
const char* awal = teks.data(); const char* akhir = awal + teks.size();—from_charsberbicara dalam pasangan pointer awal-akhir, bukanstd::string. Pointerakhirmenunjuk tepat satu posisi setelah karakter terakhir, menjadi garis finis parser.auto [ptr, ec] = std::from_chars(awal, akhir, nilai);— structured binding membuka dua informasi sekaligus:ptradalah posisi parser berhenti,ecadalah kode galat. Function ini tidak pernah melempar apa pun.if (ec != std::errc{} || ptr != akhir)— dua pemeriksaan dalam satu gerbang.ecberisiinvalid_argumentbila tidak ada angka yang terurai, atauresult_out_of_rangebila angkanya melampaui rentanglong long. Sementaraptr != akhirmenangkap penguraian parsial seperti12ab.return std::nullopt;— kegagalan dikembalikan sebagai optional kosong. Pemanggil tidak mungkin membaca nilai tanpa memeriksa dulu, karena membukanya paksa justru terlihat jelas di kode.
parseBaris: Tujuh Gerbang Berurutan #
Konversi angka hanyalah dua dari tujuh pemeriksaan yang harus dilalui satu baris. Semua gerbangnya berada dalam satu function parseBaris yang mengembalikan std::expected<BarisPenjualan, std::string> — struct BarisPenjualan bila lolos, string pesan bila ditolak:
struct BarisPenjualan {
std::string tanggal;
std::string produk;
long long jumlah = 0;
long long harga = 0;
long long nilai() const { return jumlah * harga; } // nilai transaksi
};
std::expected<BarisPenjualan, std::string> parseBaris(
const std::vector<std::string>& kolom) {
if (kolom.size() != 4) {
return std::unexpected("harus 4 kolom, ditemukan "
+ std::to_string(kolom.size()));
}
if (kolom[0].empty()) {
return std::unexpected("tanggal tidak boleh kosong");
}
if (kolom[1].empty()) {
return std::unexpected("produk tidak boleh kosong");
}
const auto jumlah = keAngka(kolom[2]);
if (!jumlah) {
return std::unexpected("jumlah bukan bilangan bulat: \"" + kolom[2] + "\"");
}
if (*jumlah <= 0) {
return std::unexpected("jumlah harus lebih dari 0");
}
const auto harga = keAngka(kolom[3]);
if (!harga) {
return std::unexpected("harga bukan bilangan bulat: \"" + kolom[3] + "\"");
}
if (*harga < 0) {
return std::unexpected("harga tidak boleh negatif");
}
return BarisPenjualan{kolom[0], kolom[1], *jumlah, *harga};
}Urutan gerbangnya bukan acakan. Gerbang pertama memeriksa kolom.size() != 4 dan wajib paling depan: tanpa itu, akses kolom[3] pada baris dua kolom akan membaca di luar batas vector. Enam gerbang berikutnya memeriksa isi: tanggal dan produk tidak boleh kosong (string kosong adalah bentuk paling umum dari kolom hilang), jumlah harus bilangan bulat dan harus lebih dari 0 (pengembalian barang 0 unit tidak masuk akal), harga harus bilangan bulat dan tidak boleh negatif. Setiap penolakan membawa pesan yang menyebut penyebabnya, termasuk teks asli kolom bermasalah — pesan seperti jumlah bukan bilangan bulat: "dua" jauh lebih mudah ditelusuri daripada sekadar “baris invalid”.
bacaCsv: Nomor Baris dan CRLF #
Gerbang per baris sudah jadi; sekarang kita pasang gerbang itu ke dalam pembacaan stream. Loop bacaCsv menghitung nomor baris fisik, menangani sisa karakter \r dari berkas berformat CRLF, melewati baris kosong dan header, lalu memilah hasil:
struct HasilBaca {
std::vector<BarisPenjualan> valid; // baris yang lolos validasi
std::vector<std::string> galat; // "baris N: alasan" untuk yang ditolak
};
HasilBaca bacaCsv(std::istream& masuk) {
HasilBaca hasil;
std::string baris;
int nomor = 0;
while (std::getline(masuk, baris)) {
++nomor;
if (!baris.empty() && baris.back() == '\r') {
baris.pop_back(); // buang '\r' sisa berkas berformat CRLF
}
if (baris.empty()) {
continue;
}
const std::vector<std::string> kolom = pisah(baris, ',');
if (nomor == 1 && !kolom.empty() && kolom[0] == "tanggal") {
continue; // baris header
}
auto data = parseBaris(kolom);
if (data) {
hasil.valid.push_back(*data);
} else {
hasil.galat.push_back("baris " + std::to_string(nomor) + ": "
+ data.error());
}
}
return hasil;
}Tiga detail patut digarisbawahi. Pertama, ++nomor dijalankan sebelum pemrosesan apa pun, jadi nomor yang dilaporkan adalah nomor baris fisik di berkas — header dihitung sebagai baris 1, dan baris rusak di baris 3 tetap dilaporkan sebagai baris 3. Kedua, baris.pop_back() membuang karakter \r yang tertinggal bila berkas disimpan dengan akhir baris CRLF dari editor Windows; tanpa itu, harga 3500\r akan ditolak from_chars padahal matanya terlihat normal. Ketiga, hasilnya berupa dua vector terpisah: valid berisi data siap hitung, galat berisi pesan yang sudah menyebut nomor baris — persis format fail-closed yang kita rancang di atas.
Demo: Melaporkan Baris yang Ditolak #
Program demo berikut menyusun skenario lengkap: satu header, dua baris valid, dan lima baris rusak — masing-masing rusak karena gerbang yang berbeda:
// validasi-demo.cpp — demo validasi baris CSV dari Bagian 2.
//
// Setiap baris yang ditolak dilaporkan lengkap dengan nomor barisnya.
// Prinsip fail-closed: baris yang meragukan tidak pernah masuk ke
// daftar data valid yang dipakai perhitungan.
//
// g++ -std=c++23 -Wall -Wextra validasi-demo.cpp -o validasi-demo
// ./validasi-demo
#include <charconv>
#include <expected>
#include <iostream>
#include <optional>
#include <sstream>
#include <string>
#include <vector>
// Memecah satu baris CSV berdasarkan tanda koma (dari Bagian 1).
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;
}
}
// Satu baris penjualan yang sudah lolos validasi.
struct BarisPenjualan {
std::string tanggal;
std::string produk;
long long jumlah = 0;
long long harga = 0;
long long nilai() const { return jumlah * harga; } // nilai transaksi
};
// Konversi teks ke bilangan bulat yang ketat memakai std::from_chars:
// seluruh teks harus terpakai, tanpa spasi, tanpa tanda '+'.
// Mengembalikan std::nullopt bila gagal — tanpa exception.
std::optional<long long> keAngka(const std::string& teks) {
long long nilai = 0;
const char* awal = teks.data();
const char* akhir = awal + teks.size();
auto [ptr, ec] = std::from_chars(awal, akhir, nilai);
if (ec != std::errc{} || ptr != akhir) {
return std::nullopt;
}
return nilai;
}
// Enam gerbang validasi, diperiksa berurutan. Gerbang pertama wajib
// di depan agar pengaksesan kolom[0]..kolom[3] selalu aman.
std::expected<BarisPenjualan, std::string> parseBaris(
const std::vector<std::string>& kolom) {
if (kolom.size() != 4) {
return std::unexpected("harus 4 kolom, ditemukan "
+ std::to_string(kolom.size()));
}
if (kolom[0].empty()) {
return std::unexpected("tanggal tidak boleh kosong");
}
if (kolom[1].empty()) {
return std::unexpected("produk tidak boleh kosong");
}
const auto jumlah = keAngka(kolom[2]);
if (!jumlah) {
return std::unexpected("jumlah bukan bilangan bulat: \"" + kolom[2] + "\"");
}
if (*jumlah <= 0) {
return std::unexpected("jumlah harus lebih dari 0");
}
const auto harga = keAngka(kolom[3]);
if (!harga) {
return std::unexpected("harga bukan bilangan bulat: \"" + kolom[3] + "\"");
}
if (*harga < 0) {
return std::unexpected("harga tidak boleh negatif");
}
return BarisPenjualan{kolom[0], kolom[1], *jumlah, *harga};
}
int main() {
// Skenario: header, dua baris valid, dan lima baris rusak — satu
// per gerbang validasi.
std::istringstream masuk{
"tanggal,produk,jumlah,harga\n"
"2025-11-01,Buku Tulis,10,3500\n"
"2025-11-05,Penggaris\n"
"2025-11-08,,3,1000\n"
"2025-11-06,Stabilo,dua,9000\n"
"2025-11-06,Tip-Ex,0,12000\n"
"2025-11-07,Penggaris,4,3000x\n"
"2025-11-09,Pensil 2B,25,2000\n"};
std::string baris;
int nomor = 0;
int sah = 0;
while (std::getline(masuk, baris)) {
++nomor;
const std::vector<std::string> kolom = pisah(baris, ',');
if (nomor == 1 && !kolom.empty() && kolom[0] == "tanggal") {
std::cout << "baris " << nomor << ": dilewati (header)\n";
continue;
}
auto data = parseBaris(kolom);
if (data) {
++sah;
std::cout << "baris " << nomor << ": OK (" << data->produk
<< ", nilai " << data->nilai() << ")\n";
} else {
std::cout << "baris " << nomor << ": DITOLAK (" << data.error()
<< ")\n";
}
}
// Fail-closed: hanya baris yang lolos enam gerbang yang masuk hitung.
std::cout << "\nmasuk perhitungan: " << sah << " baris\n";
}g++ -std=c++23 -Wall -Wextra validasi-demo.cpp -o validasi-demo
./validasi-demoOutput yang Dihasilkan:
baris 1: dilewati (header)
baris 2: OK (Buku Tulis, nilai 35000)
baris 3: DITOLAK (harus 4 kolom, ditemukan 2)
baris 4: DITOLAK (produk tidak boleh kosong)
baris 5: DITOLAK (jumlah bukan bilangan bulat: "dua")
baris 6: DITOLAK (jumlah harus lebih dari 0)
baris 7: DITOLAK (harga bukan bilangan bulat: "3000x")
baris 8: OK (Pensil 2B, nilai 50000)
masuk perhitungan: 2 barisSetiap baris DITOLAK memetakan ke satu gerbang, berurutan persis seperti rancangan kita: baris 3 kekurangan kolom (gerbang 1), baris 4 kehilangan nama produk (gerbang 3), baris 5 berisi "dua" pada kolom jumlah (gerbang 4), baris 6 memiliki jumlah nol (gerbang 5), dan baris 7 menyisakan x pada harga — tertangkap oleh pemeriksaan ptr != akhir di dalam keAngka (gerbang 6). Baris 8 membuktikan loop tidak berhenti di kegagalan: setelah lima penolakan, ia tetap diproses dan masuk perhitungan. Baris yang masuk perhitungan hanya dua — dan itulah angka yang bisa dipercaya sepenuhnya.
Dengan tujuh gerbang ini, vector valid hanya berisi data yang layak: empat kolom lengkap, angka sungguhan, jumlah positif, harga non-negatif. Tahap berikutnya akhirnya bisa berhitung dengan tenang — Bagian 3 mengagregasikan baris-baris itu menjadi statistik per produk dengan std::map.
Rujukan resmi: std::from_chars (cppreference) , std::expected (cppreference) , std::optional (cppreference) .