Setelah csv_baca berhasil membuka file dan membaca setiap baris dengan fgets, langkah berikutnya adalah memecah satu baris teks menjadi field-field terpisah. Pada bagian ini kita mengganti stub pisahkan_baris di csv_reader.c dengan implementasi nyata yang memindai karakter koma sebagai pemisah. Tujuan bagian ini adalah memastikan setiap baris CSV terurai menjadi field-field string bersih yang siap divalidasi dan diagregasi pada bagian selanjutnya.
Konsep #
CSV adalah format yang tampak sederhana tetapi punya banyak sudut kasus: field kosong di tengah ("Siti,,6200"), baris tanpa koma sama sekali (satu field), baris dengan spasi di sekitar koma, hingga baris yang berakhir newline atau carriage return. Dua pendekatan umum untuk memisahkan field adalah strtok dari <string.h> dan pemindaian manual karakter-per-karakter. Kita memilih pemindaian manual karena strtok memodifikasi string input dan mengabaikan field kosong berturut-turut — perilaku yang tidak kita inginkan.
Mekanisme pemindai manual sederhana: kita menjaga dua indeks — i melacak posisi di string input, dan j melacak posisi penulisan di field yang sedang diisi. Saat menemui koma, kita menutup field saat ini dengan karakter null, naikkan jumlah_field, dan reset j ke nol. Karakter newline (\n) dan carriage return (\r) diabaikan agar tidak ikut tersalin ke field terakhir. Setelah loop selesai, field terakhir ditutup dengan null terminator.
Implementasi #
Fungsi pisahkan_baris di csv_reader.c:
static int pisahkan_baris(const char *line, CsvRow *row) {
if (row == 0) return 0;
row->jumlah_field = 0;
int i = 0;
int j = 0;
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;
}Demo mandiri yang menguji tiga skenario: baris normal, baris dengan field kosong, dan baris tanpa koma:
#include <stdio.h>
#define CSV_MAKS_FIELD 16
#define CSV_MAKS_PANJANG_FIELD 64
typedef struct {
char fields[CSV_MAKS_FIELD][CSV_MAKS_PANJANG_FIELD];
int jumlah_field;
} CsvRow;
static int pisahkan_baris(const char *line, CsvRow *row) {
if (row == 0) return 0;
row->jumlah_field = 0;
int i = 0;
int j = 0;
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++;
}
row->fields[row->jumlah_field][j] = '\0';
row->jumlah_field++;
return row->jumlah_field;
}
int main(void) {
const char *baris = "Budi,25,5000\n";
CsvRow row;
int n = pisahkan_baris(baris, &row);
printf("Jumlah field: %d\n", n);
for (int i = 0; i < n; i++) {
printf(" field[%d] = '%s'\n", i, row.fields[i]);
}
const char *baris2 = "Siti,,6200";
CsvRow row2;
int n2 = pisahkan_baris(baris2, &row2);
printf("Jumlah field (ada kosong): %d\n", n2);
for (int i = 0; i < n2; i++) {
printf(" field[%d] = '%s'\n", i, row2.fields[i]);
}
const char *baris3 = "header-only";
CsvRow row3;
int n3 = pisahkan_baris(baris3, &row3);
printf("Jumlah field (tanpa koma): %d, field[0]='%s'\n",
n3, row3.fields[0]);
return 0;
}Output yang Dihasilkan:
Jumlah field: 3
field[0] = 'Budi'
field[1] = '25'
field[2] = '5000'
Jumlah field (ada kosong): 3
field[0] = 'Siti'
field[1] = ''
field[2] = '6200'
Jumlah field (tanpa koma): 1, field[0]='header-only'Pengujian #
Untuk menguji integrasi pisahkan_baris dengan csv_baca, rebuild program utama lalu jalankan terhadap sample.csv:
gcc -std=c11 -Wall -Wextra -o program main.c csv_reader.c parser.c -lm
./program sample.csv 2Output yang Dihasilkan (potongan akhir):
Statistik kolom 'kolom':
Jumlah data : 5
Total : 32200
Minimum : 4500
Maksimum : 9000
Rata-rata : 6440
Hasil ditulis ke output.csvMari kita telaah baris-baris kunci pada pisahkan_baris:
- Baris
if (row == 0) return 0;: penjaga NULL. Bila pemanggil lupa mengalokasikanCsvRow, kita kembalikan 0 alih-alih dereference pointer NULL yang menyebabkan crash. - Baris
while (line[i] != '\0' && row->jumlah_field < CSV_MAKS_FIELD): dua kondisi keluar — habisnya string atau penuhnya array field. Kondisi kedua mencegah overflowrow->fields[]bila satu baris berisi lebih dari 16 koma. - Baris
if (c == ','): koma menandai akhir field. Kita tutup field dengan'\0', tambahjumlah_field, dan resetjuntuk field berikutnya. Penting: tidak ada incrementidi sini —i++di akhir loop mengurusnya. - Baris
else if (c != '\n' && c != '\r'): filter karakter newline. Tanpa filter ini, field terakhir akan berisi trailing newline yang merusak perbandingan string. - Baris
if (j < CSV_MAKS_PANJANG_FIELD - 1): batas panjang field. Kita sisakan satu byte untuk null terminator. Bila field lebih panjang dari 63 karakter, sisanya dibuang — tidak crash. - Baris
row->fields[row->jumlah_field][j] = '\0';di luar loop: menutup field terakhir. Tanpa baris ini, field terakhir tidak berakhir null dan berisi karakter sisa dari field sebelumnya. - Baris
row->jumlah_field++; return row->jumlah_field;: tambah satu untuk field terakhir (karena field terakhir tidak diawali koma), lalu pulangkan jumlahnya.
Pada Bagian 3 kita akan memvalidasi field-field ini sebagai angka sebelum dipakai dalam perhitungan.
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;
}