Field-field hasil parsing di Bagian 2 berupa string bebas. Sebelum string tersebut dapat dipakai sebagai angka untuk agregat, kita perlu mengonversi ke double dengan cara yang aman. Pada bagian ini kita menulis parser_ke_double dan parser_valid_angka di parser.c. Tujuan bagian ini adalah memastikan setiap field numerik terkonversi dengan rapi dan field non-numerik (misalnya "bukan-angka") ditolak tanpa crash.
Konsep #
Konversi string-ke-double paling rawan dilakukan dengan atof dari <stdlib.h>. Fungsi atof mengembalikan 0.0 bila string bukan angka — sebuah nilai yang sah secara numerik, sehingga pemanggil tidak bisa membedakan antara “input benar-benar nol” dan “input gagal konversi”. Untuk membedakan kedua kasus ini, kita memakai strtod dengan parameter endptr yang melaporkan posisi karakter pertama yang tidak terkonversi.
Mekanisme strtod: fungsi ini melewati whitespace di awal, lalu membaca angka dalam format [-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)? dan menyimpan alamat karakter pertama yang tidak dikenali ke *endptr. Bila endptr == s (pointer tidak maju sama sekali), tidak ada karakter yang berhasil dikonversi — string bukan angka. Bila *endptr menunjuk ke karakter bukan null dan bukan whitespace sisa, ada karakter sampah setelah angka (misalnya "12abc").
Untuk memperketat aturan, kita menambahkan dua pemeriksaan: (1) skip whitespace awal dan tolak string kosong, (2) setelah strtod, lompati whitespace sisa lalu pastikan *endptr == '\0'. Aturan ini membuat " 42 " valid (dibersihkan) tetapi "12abc" dan "1,5" (titik desimal Indonesia) ditolak.
Implementasi #
Fungsi parser_ke_double di parser.c:
#include <stdlib.h>
ParserStatus parser_ke_double(const char *s, double *out) {
if (s == 0 || out == 0) return PARSER_ERR_KONVERSI;
/* Skip whitespace di awal. */
while (*s == ' ' || *s == '\t') s++;
if (*s == '\0') return PARSER_ERR_KONVERSI;
char *sisa = 0;
double v = strtod(s, &sisa);
if (sisa == s) return PARSER_ERR_KONVERSI;
*out = v;
return PARSER_OK;
}
int parser_valid_angka(const char *s) {
double dummy;
return parser_ke_double(s, &dummy) == PARSER_OK;
}Versi ketat yang juga menolak karakter sampah setelah angka:
#include <stdio.h>
#include <stdlib.h>
typedef enum {
PARSER_OK = 0,
PARSER_ERR_KONVERSI = 3
} ParserStatus;
static ParserStatus parser_ke_double(const char *s, double *out) {
if (s == 0 || out == 0) return PARSER_ERR_KONVERSI;
while (*s == ' ' || *s == '\t') s++;
if (*s == '\0') return PARSER_ERR_KONVERSI;
char *sisa = 0;
double v = strtod(s, &sisa);
while (*sisa == ' ' || *sisa == '\t') sisa++;
if (*sisa != '\0' || sisa == s) return PARSER_ERR_KONVERSI;
*out = v;
return PARSER_OK;
}
static int parser_valid_angka(const char *s) {
double dummy;
return parser_ke_double(s, &dummy) == PARSER_OK;
}
int main(void) {
const char *samples[] = {
"3.14", " 42 ", "100", "0.5", "-7.2e3",
"bukan-angka", "", "12abc", "1,5"
};
int n = (int)(sizeof(samples) / sizeof(samples[0]));
for (int i = 0; i < n; i++) {
double v = 0.0;
ParserStatus s = parser_ke_double(samples[i], &v);
if (s == PARSER_OK) {
printf("'%s' valid -> %g (valid_angka=%d)\n",
samples[i], v, parser_valid_angka(samples[i]));
} else {
printf("'%s' TIDAK valid (valid_angka=%d)\n",
samples[i], parser_valid_angka(samples[i]));
}
}
return 0;
}Output yang Dihasilkan:
'3.14' valid -> 3.14 (valid_angka=1)
' 42 ' valid -> 42 (valid_angka=1)
'100' valid -> 100 (valid_angka=1)
'0.5' valid -> 0.5 (valid_angka=1)
'-7.2e3' valid -> -7200 (valid_angka=1)
'bukan-angka' TIDAK valid (valid_angka=0)
'' TIDAK valid (valid_angka=0)
'12abc' TIDAK valid (valid_angka=0)
'1,5' TIDAK valid (valid_angka=0)Pengujian #
Untuk memverifikasi integrasi dengan pipeline utama, kita tampilkan apa yang terjadi saat file CSV berisi field non-numerik di kolom yang seharusnya angka. Buat file /tmp/p5b3_bad.csv:
printf 'nama,umur\nBudi,bukan-umur\nSiti,30\n' > /tmp/p5b3_bad.csv
gcc -std=c11 -Wall -Wextra -o /tmp/p5b3_program \
/home/z/my-project/content/c/proyek-c/proyek-05-pemroses-file-csv/main.c \
/home/z/my-project/content/c/proyek-c/proyek-05-pemroses-file-csv/csv_reader.c \
/home/z/my-project/content/c/proyek-c/proyek-05-pemroses-file-csv/parser.c -lm
/tmp/p5b3_program /tmp/p5b3_bad.csv 1 2>&1Output yang Dihasilkan:
Berhasil memuat 3 baris (termasuk header).
nama | umur
Budi | bukan-umur
Siti | 30
Gagal menghitung statistik kolom 1 (kode 3)Baris Gagal menghitung ... ditulis ke stderr oleh main.c — redirect 2>&1 menggabungkannya dengan stdout agar tampak dalam urutan yang sama. Kode 3 adalah PARSER_ERR_KONVERSI — pipeline berhenti di baris "Budi" karena field "bukan-umur" tidak dapat dikonversi menjadi double. Pesan error spesifik ini muncul berkat pemisahan kode status di enum ParserStatus.
Mari kita telaah baris-baris kunci pada parser_ke_double versi ketat:
- Baris
if (s == 0 || out == 0) return PARSER_ERR_KONVERSI;: penjaga NULL ganda. Parameter input maupun output diperiksa — bila salah satu NULL, kita laporkan error konversi alih-alih crash. - Baris
while (*s == ' ' || *s == '\t') s++;: skip whitespace awal. Setelah loop,smenunjuk ke karakter pertama yang bukan whitespace. Pointersadalah parameter lokal — perubahan tidak memengaruhi pemanggil karenasadalahconst char *yang di-pass by value. - Baris
if (*s == '\0') return PARSER_ERR_KONVERSI;: tolak string kosong setelah trim. Tanpa baris ini,strtod("", ...)mengembalikan0.0yang terlihat valid — namun sebenarnya tidak ada angka yang diparse. - Baris
double v = strtod(s, &sisa);: konversi inti.strtodmenulis alamat karakter pertama yang tidak dikonversi kesisa. Bilasisa == s(tidak maju), tidak ada angka sama sekali. - Baris
while (*sisa == ' ' || *sisa == '\t') sisa++;: skip whitespace sisa." 42 "seharusnya valid — kita toleransi spasi setelah angka. - Baris
if (*sisa != '\0' || sisa == s) return PARSER_ERR_KONVERSI;: dua kondisi penolakan. Kondisi pertama menolak karakter sampah ("12abc"—sisamenunjuk ke'a'). Kondisi kedua menolak string tanpa angka ("abc"—sisa == s). - Baris
*out = v; return PARSER_OK;: tulis hasil konversi hanya bila semua pemeriksaan lulus. Pola ini mencegah pemanggil membaca nilai sampah dari*outsaat error.
Pada Bagian 4 kita akan memakai parser_ke_double untuk menghitung agregat statistik (jumlah, total, min, maks, rata-rata) dalam satu pass.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
csv_reader.c
Unduh berkas/* csv_reader.c -- pembaca file CSV sederhana. */
#include "csv_reader.h"
#include <stdio.h>
#include <string.h>
/* Memisahkan satu baris teks menjadi field-field dengan pemisah koma.
* Menulis hasil ke row->fields dan mengisi row->jumlah_field.
* Mengembalikan jumlah field yang berhasil diparse.
*/
static int pisahkan_baris(const char *line, CsvRow *row) {
if (row == 0) return 0;
row->jumlah_field = 0;
int i = 0; /* indeks field */
int j = 0; /* indeks char di dalam field */
while (line[i] != '\0' && row->jumlah_field < CSV_MAKS_FIELD) {
char c = line[i];
if (c == ',') {
row->fields[row->jumlah_field][j] = '\0';
row->jumlah_field++;
j = 0;
} else if (c != '\n' && c != '\r') {
if (j < CSV_MAKS_PANJANG_FIELD - 1) {
row->fields[row->jumlah_field][j] = c;
j++;
}
}
i++;
}
/* Tutup field terakhir. */
row->fields[row->jumlah_field][j] = '\0';
row->jumlah_field++;
return row->jumlah_field;
}
CsvStatus csv_baca(const char *path, CsvTable *tabel) {
if (path == 0 || tabel == 0) return CSV_ERR_BUKA_FILE;
FILE *f = fopen(path, "r");
if (f == 0) return CSV_ERR_BUKA_FILE;
tabel->jumlah_baris = 0;
char baris[512];
while (fgets(baris, sizeof(baris), f) != 0
&& tabel->jumlah_baris < CSV_MAKS_BARIS) {
/* Skip baris kosong. */
if (baris[0] == '\n' || baris[0] == '\r' || baris[0] == '\0') {
continue;
}
CsvRow *row = &tabel->baris[tabel->jumlah_baris];
int n = pisahkan_baris(baris, row);
if (n == 0) {
fclose(f);
return CSV_ERR_FORMAT;
}
tabel->jumlah_baris++;
}
fclose(f);
if (tabel->jumlah_baris == 0) {
return CSV_ERR_FORMAT;
}
return CSV_OK;
}
void csv_tampilkan(const CsvTable *tabel, FILE *out) {
if (tabel == 0 || out == 0) return;
for (int i = 0; i < tabel->jumlah_baris; i++) {
const CsvRow *r = &tabel->baris[i];
for (int j = 0; j < r->jumlah_field; j++) {
fprintf(out, "%s", r->fields[j]);
if (j < r->jumlah_field - 1) fprintf(out, " | ");
}
fprintf(out, "\n");
}
}
main.c
Unduh berkas/* main.c -- pemroses file CSV dari command line. */
#include "csv_reader.h"
#include "parser.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static void penggunaan(const char *prog) {
fprintf(stderr, "Penggunaan: %s <file.csv> [kolom_statistik]\n", prog);
fprintf(stderr, " kolom_statistik: indeks kolom (0-indexed) untuk dihitung statistiknya.\n");
}
int main(int argc, char **argv) {
if (argc < 2) {
penggunaan(argv[0]);
return 1;
}
const char *path = argv[1];
int kolom_stat = -1;
if (argc >= 3) {
kolom_stat = atoi(argv[2]);
}
CsvTable tabel;
CsvStatus s = csv_baca(path, &tabel);
if (s != CSV_OK) {
fprintf(stderr, "Gagal membaca %s (kode %d)\n", path, s);
return 1;
}
printf("Berhasil memuat %d baris (termasuk header).\n\n", tabel.jumlah_baris);
csv_tampilkan(&tabel, stdout);
if (kolom_stat >= 0) {
Statistik stat;
ParserStatus ps = parser_statistik_kolom(&tabel, kolom_stat, 1, &stat);
if (ps == PARSER_OK) {
printf("\n");
parser_cetak_statistik(&stat, "kolom", stdout);
} else {
fprintf(stderr, "Gagal menghitung statistik kolom %d (kode %d)\n",
kolom_stat, ps);
return 1;
}
}
/* Export hasil (sederhana: tulis ulang tabel ke file output). */
FILE *out = fopen("output.csv", "w");
if (out != 0) {
for (int i = 0; i < tabel.jumlah_baris; i++) {
const CsvRow *r = &tabel.baris[i];
for (int j = 0; j < r->jumlah_field; j++) {
fprintf(out, "%s", r->fields[j]);
if (j < r->jumlah_field - 1) fprintf(out, ",");
}
fprintf(out, "\n");
}
fclose(out);
printf("\nHasil ditulis ke output.csv\n");
}
return 0;
}
parser.c
Unduh berkas/* parser.c -- parsing dan agregasi kolom CSV. */
#include "parser.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
ParserStatus parser_ke_double(const char *s, double *out) {
if (s == 0 || out == 0) return PARSER_ERR_KONVERSI;
/* Skip whitespace di awal. */
while (*s == ' ' || *s == '\t') s++;
if (*s == '\0') return PARSER_ERR_KONVERSI;
char *sisa = 0;
double v = strtod(s, &sisa);
if (sisa == s) return PARSER_ERR_KONVERSI;
*out = v;
return PARSER_OK;
}
int parser_valid_angka(const char *s) {
double dummy;
return parser_ke_double(s, &dummy) == PARSER_OK;
}
ParserStatus parser_statistik_kolom(const CsvTable *tabel,
int kolom,
int skip_header,
Statistik *stat) {
if (tabel == 0 || stat == 0) return PARSER_ERR_TIDAK_ADA_DATA;
if (kolom < 0 || kolom >= CSV_MAKS_FIELD) return PARSER_ERR_KOLOM_INVALID;
/* Inisialisasi statistik. */
stat->jumlah = 0;
stat->jumlah_nilai = 0.0;
stat->minimum = 0.0;
stat->maksimum = 0.0;
stat->rata_rata = 0.0;
int mulai = skip_header ? 1 : 0;
int ada_data = 0;
for (int i = mulai; i < tabel->jumlah_baris; i++) {
const CsvRow *row = &tabel->baris[i];
if (kolom >= row->jumlah_field) {
return PARSER_ERR_KOLOM_INVALID;
}
double v;
ParserStatus ps = parser_ke_double(row->fields[kolom], &v);
if (ps != PARSER_OK) {
return PARSER_ERR_KONVERSI;
}
if (!ada_data) {
stat->minimum = v;
stat->maksimum = v;
ada_data = 1;
} else {
if (v < stat->minimum) stat->minimum = v;
if (v > stat->maksimum) stat->maksimum = v;
}
stat->jumlah_nilai += v;
stat->jumlah++;
}
if (!ada_data) return PARSER_ERR_TIDAK_ADA_DATA;
stat->rata_rata = stat->jumlah_nilai / stat->jumlah;
return PARSER_OK;
}
void parser_cetak_statistik(const Statistik *stat, const char *nama_kolom, FILE *out) {
if (stat == 0 || out == 0) return;
fprintf(out, "Statistik kolom '%s':\n", nama_kolom ? nama_kolom : "?");
fprintf(out, " Jumlah data : %d\n", stat->jumlah);
fprintf(out, " Total : %g\n", stat->jumlah_nilai);
fprintf(out, " Minimum : %g\n", stat->minimum);
fprintf(out, " Maksimum : %g\n", stat->maksimum);
fprintf(out, " Rata-rata : %g\n", stat->rata_rata);
}
tests.c
Unduh berkas/* tests.c -- rangkaian test untuk modul csv_reader dan parser. */
#include "csv_reader.h"
#include "parser.h"
#include <stdio.h>
#include <math.h>
#include <string.h>
static int lulus = 0;
static int gagal = 0;
#define PERIKSA(kondisi, nama) \
do { \
if (kondisi) { lulus++; printf("[LULUS] %s\n", nama); } \
else { gagal++; printf("[GAGAL] %s\n", nama); } \
} while (0)
static int hampir_sama(double a, double b) {
return fabs(a - b) < 1e-9;
}
static void test_baca_csv(void) {
/* Buat file CSV test. */
FILE *f = fopen("/tmp/test_p5.csv", "w");
fputs("nama,umur,gaji\n", f);
fputs("Budi,25,5000\n", f);
fputs("Siti,30,7500\n", f);
fputs("Andi,28,6200\n", f);
fclose(f);
CsvTable tabel;
CsvStatus s = csv_baca("/tmp/test_p5.csv", &tabel);
PERIKSA(s == CSV_OK, "Baca CSV berhasil");
PERIKSA(tabel.jumlah_baris == 4, "Jumlah baris = 4 (1 header + 3 data)");
PERIKSA(tabel.baris[0].jumlah_field == 3, "Header punya 3 field");
PERIKSA(strcmp(tabel.baris[1].fields[0], "Budi") == 0,
"Baris 1 field 0 = 'Budi'");
}
static void test_baca_file_tidak_ada(void) {
CsvTable tabel;
CsvStatus s = csv_baca("/tmp/tidak_ada_p5.csv", &tabel);
PERIKSA(s == CSV_ERR_BUKA_FILE, "File tidak ada -> BUKA_FILE");
}
static void test_parser_ke_double(void) {
double v;
PERIKSA(parser_ke_double("3.14", &v) == PARSER_OK && hampir_sama(v, 3.14),
"Parse '3.14' -> 3.14");
PERIKSA(parser_ke_double(" 42 ", &v) == PARSER_OK && hampir_sama(v, 42.0),
"Parse ' 42 ' -> 42 (toleransi whitespace)");
PERIKSA(parser_ke_double("bukan-angka", &v) == PARSER_ERR_KONVERSI,
"Parse 'bukan-angka' -> KONVERSI error");
PERIKSA(parser_ke_double("", &v) == PARSER_ERR_KONVERSI,
"Parse string kosong -> KONVERSI error");
}
static void test_statistik_kolom(void) {
FILE *f = fopen("/tmp/test_p5_stat.csv", "w");
fputs("nama,nilai\n", f);
fputs("A,10\n", f);
fputs("B,20\n", f);
fputs("C,30\n", f);
fclose(f);
CsvTable tabel;
csv_baca("/tmp/test_p5_stat.csv", &tabel);
Statistik stat;
ParserStatus ps = parser_statistik_kolom(&tabel, 1, 1, &stat);
PERIKSA(ps == PARSER_OK, "Statistik kolom 1 (skip header) berhasil");
PERIKSA(stat.jumlah == 3, "Jumlah data = 3");
PERIKSA(hampir_sama(stat.jumlah_nilai, 60.0), "Total = 60");
PERIKSA(hampir_sama(stat.minimum, 10.0), "Minimum = 10");
PERIKSA(hampir_sama(stat.maksimum, 30.0), "Maksimum = 30");
PERIKSA(hampir_sama(stat.rata_rata, 20.0), "Rata-rata = 20");
}
static void test_statistik_kolom_invalid(void) {
FILE *f = fopen("/tmp/test_p5_bad.csv", "w");
fputs("nama,umur\n", f);
fputs("Budi,bukan-umur\n", f);
fclose(f);
CsvTable tabel;
csv_baca("/tmp/test_p5_bad.csv", &tabel);
Statistik stat;
ParserStatus ps = parser_statistik_kolom(&tabel, 1, 1, &stat);
PERIKSA(ps == PARSER_ERR_KONVERSI, "Field non-numerik -> KONVERSI error");
}
int main(void) {
test_baca_csv();
test_baca_file_tidak_ada();
test_parser_ke_double();
test_statistik_kolom();
test_statistik_kolom_invalid();
printf("\nRingkasan: %d lulus, %d gagal\n", lulus, gagal);
return gagal == 0 ? 0 : 1;
}