Sebelum dapat memisahkan field, kita perlu membaca file CSV baris per baris. Pada bagian ini kita membuat csv_reader.h yang mendefinisikan tipe data CsvRow dan CsvTable, lalu csv_reader.c yang berisi fungsi csv_baca. Fungsi ini membuka file dengan fopen, membaca setiap baris dengan fgets, dan memanggil pisahkan_baris untuk memecah baris menjadi field-field. Tujuan bagian ini adalah memastikan file CSV dapat dibaca dan ditampung ke struktur memori tanpa kebocoran sumber daya.
Konsep #
Struktur penyimpanan CSV kita pilih statis untuk kesederhanaan: CsvRow berisi array 2D char fields[16][64] yang menampung maksimal 16 field dengan panjang 64 karakter masing-masing. CsvTable berisi array 100 CsvRow — cukup untuk file CSV kecil. Untuk file besar, pendekatan dinamis (malloc per baris) akan lebih efisien, tetapi memerlukan manajemen memori manual yang dibahas di Proyek 06.
Fungsi csv_baca mengembalikan enum CsvStatus yang melaporkan hasil: CSV_OK bila berhasil, CSV_ERR_BUKA_FILE bila fopen gagal (file tidak ada), CSV_ERR_FORMAT bila file kosong atau baris pertama tidak dapat diparse, dan CSV_ERR_PENUH bila kapasitas CSV_MAKS_BARIS terlampaui. Pemisahan kode error memungkinkan main.c memberi pesan yang spesifik.
Setiap baris dibaca dengan fgets(baris, sizeof(baris), f). fgets berhenti di newline atau saat buffer penuh. Baris kosong (hanya newline) diabaikan dengan continue agar tidak masuk ke tabel. Setelah baris valid, pisahkan_baris dipanggil untuk memisahkan field berdasarkan koma.
Implementasi #
Struktur data di csv_reader.h:
#define CSV_MAKS_BARIS 100
#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;
typedef struct {
CsvRow baris[CSV_MAKS_BARIS];
int jumlah_baris;
} CsvTable;
typedef enum {
CSV_OK = 0,
CSV_ERR_BUKA_FILE = 1,
CSV_ERR_FORMAT = 2,
CSV_ERR_PENUH = 3
} CsvStatus;Fungsi csv_baca di csv_reader.c:
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) {
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;
}Kode Lengkap #
Berkas csv_reader.h:
#ifndef CSV_READER_H
#define CSV_READER_H
#include <stdio.h>
#define CSV_MAKS_BARIS 100
#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;
typedef struct {
CsvRow baris[CSV_MAKS_BARIS];
int jumlah_baris;
} CsvTable;
typedef enum {
CSV_OK = 0,
CSV_ERR_BUKA_FILE = 1,
CSV_ERR_FORMAT = 2,
CSV_ERR_PENUH = 3
} CsvStatus;
CsvStatus csv_baca(const char *path, CsvTable *tabel);
void csv_tampilkan(const CsvTable *tabel, FILE *out);
#endif /* CSV_READER_H */Stub pisahkan_baris dan csv_baca minimal di csv_reader.c (lihat Bagian 2 untuk pisahkan_baris lengkap):
#include "csv_reader.h"
#include <stdio.h>
#include <string.h>
static int pisahkan_baris(const char *line, CsvRow *row) {
/* Stub: salin seluruh baris ke field 0. */
if (row == 0) return 0;
strncpy(row->fields[0], line, CSV_MAKS_PANJANG_FIELD - 1);
row->fields[0][CSV_MAKS_PANJANG_FIELD - 1] = '\0';
row->jumlah_field = 1;
return 1;
}
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) {
if (baris[0] == '\n' || baris[0] == '\r' || baris[0] == '\0') continue;
CsvRow *row = &tabel->baris[tabel->jumlah_baris];
if (pisahkan_baris(baris, row) == 0) { fclose(f); return CSV_ERR_FORMAT; }
tabel->jumlah_baris++;
}
fclose(f);
return tabel->jumlah_baris == 0 ? CSV_ERR_FORMAT : CSV_OK;
}Pengujian #
Demo membaca file CSV:
cat > /tmp/p5b1_demo.c << 'EOF'
#include "csv_reader.h"
#include <stdio.h>
int main(void) {
/* Buat file test. */
FILE *f = fopen("/tmp/p5b1.csv", "w");
fputs("nama,umur\n", f);
fputs("Budi,25\n", f);
fputs("Siti,30\n", f);
fclose(f);
CsvTable tabel;
CsvStatus s = csv_baca("/tmp/p5b1.csv", &tabel);
printf("Status baca = %d, jumlah baris = %d\n", s, tabel.jumlah_baris);
return 0;
}
EOF
gcc -std=c11 -Wall -Wextra -I/home/z/my-project/content/c/proyek-c/proyek-05-pemroses-file-csv/ -o /tmp/p5b1_demo /tmp/p5b1_demo.c /home/z/my-project/content/c/proyek-c/proyek-05-pemroses-file-csv/csv_reader.c && /tmp/p5b1_demoOutput yang Dihasilkan:
Status baca = 0, jumlah baris = 3Uji file tidak ada:
cat > /tmp/p5b1_notfound.c << 'EOF'
#include "csv_reader.h"
#include <stdio.h>
int main(void) {
CsvTable tabel;
CsvStatus s = csv_baca("/tmp/tidak_ada.csv", &tabel);
printf("Status = %d (1 = BUKA_FILE)\n", s);
return 0;
}
EOF
gcc -std=c11 -Wall -Wextra -I/home/z/my-project/content/c/proyek-c/proyek-05-pemroses-file-csv/ -o /tmp/p5b1_notfound /tmp/p5b1_notfound.c /home/z/my-project/content/c/proyek-c/proyek-05-pemroses-file-csv/csv_reader.c && /tmp/p5b1_notfoundOutput yang Dihasilkan:
Status = 1 (1 = BUKA_FILE)Mari kita telaah baris-baris kunci pada csv_baca:
- Baris
if (path == 0 || tabel == 0) return CSV_ERR_BUKA_FILE;: dua pointer check. Bila pemanggil lupa mengisi parameter, kita tidak melanjutkan kefopenyang akan crash. - Baris
FILE *f = fopen(path, "r");: buka file untuk dibaca. Mode"r"membatasi ke operasi baca; file baru tidak akan dibuat. - Baris
while (fgets(baris, sizeof(baris), f) != 0 ...): loop baca baris.fgetsmengembalikanNULLsaat EOF — kita keluar dari loop. - Baris
&& tabel->jumlah_baris < CSV_MAKS_BARIS: kondisi kedua mencegah overflow array. Bila file lebih besar dariCSV_MAKS_BARIS, kita berhenti membaca — baris sisanya diabaikan. - Baris
if (baris[0] == '\n' || ...): skip baris kosong. Tanpa skip ini, tabel akan berisi baris kosong yang dapat merusak agregat. - Baris
if (pisahkan_baris(baris, row) == 0) { fclose(f); return CSV_ERR_FORMAT; }: bila pemisahan gagal, tutup file lalu laporkan error. Penting untuk tidak bocor file handle. - Baris
fclose(f);di akhir: tutup file setelah loop selesai (EOF atau kapasitas penuh). - Baris
return tabel->jumlah_baris == 0 ? CSV_ERR_FORMAT : CSV_OK;: bila tidak ada baris sama sekali, laporkan format error — file kosong tidak berguna.
Bagian 2 akan mengisi pisahkan_baris yang sesungguhnya.
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;
}