Berkas CSV (Comma-Separated Values) adalah format pertukaran data paling umum di dunia data. Pada proyek ini kita membangun pemroses CSV dari nol: membaca file baris per baris, memisahkan field berdasarkan koma, memvalidasi field numerik, menghitung statistik agregat (jumlah, total, min, maks, rata-rata), dan mengekspor hasil. Proyek ini melatih keterampilan pemrosesan string, validasi konversi, dan agregasi data — pondasi untuk aplikasi data analytics sederhana.
Program menerima argumen command line: path file CSV dan indeks kolom numerik yang akan dihitung statistiknya. Output berisi tabel CSV yang dibaca, statistik kolom, dan file hasil output.csv.
Tampilan Program Akhir #
Sesi pemakaian berikut menunjukkan hasil akhir proyek saat dijalankan dengan sample.csv dan kolom 2 (gaji):
$ ./program sample.csv 2
Berhasil memuat 6 baris (termasuk header).
nama | umur | gaji
Budi | 25 | 5000
Siti | 30 | 7500
Andi | 28 | 6200
Dewi | 35 | 9000
Eka | 22 | 4500
Statistik kolom 'kolom':
Jumlah data : 5
Total : 32200
Minimum : 4500
Maksimum : 9000
Rata-rata : 6440
Hasil ditulis ke output.csvHasil statistik membuktikan bahwa pemroses berhasil memisahkan field, mengkonversi string ke double, dan menghitung agregat dengan benar.
Tujuan Pembelajaran #
Setelah menyelesaikan proyek ini, kita mampu:
- memisahkan string berdasarkan delimiter (koma) secara manual tanpa pustaka eksternal,
- memvalidasi field numerik dengan
strtoddanendptr, - menghitung agregat statistik (jumlah, total, min, maks, rata-rata) dalam satu pass,
- menangani error konversi tanpa crash,
- memproses argumen command line lewat
argc/argv, - memisahkan modul pembaca (
csv_reader) dari modul analisis (parser).
Prasyarat #
Proyek ini memakai materi bab sebelumnya:
- String — iterasi karakter, pembandingan,
strtod, - Array dan Pointer —
char fields[N][M],char **argv, - File I/O Dasar —
fopen,fgets,fprintf,fclose, - Fungsi dan Header Files — modularisasi
csv_reader.h/.cdanparser.h/.c, - Struct dan Enum —
CsvRow,CsvTable,Statistik, - Command Line Arguments —
argcdanargv.
Tahapan Pengerjaan #
Proyek dikerjakan dalam enam bagian berurutan:
- Bagian 1: Membaca File —
csv_bacayang membuka file, membaca baris denganfgets, dan memanggilpisahkan_baris. - Bagian 2: Parsing Record — fungsi
pisahkan_barisyang memisahkan string berdasarkan koma. - Bagian 3: Validasi Data —
parser_ke_doubledanparser_valid_angkauntuk konversi numerik aman. - Bagian 4: Agregasi dan Statistik —
parser_statistik_kolomyang menghitung jumlah, total, min, maks, rata-rata dalam satu pass. - Bagian 5: Export Hasil —
main.cmenulis tabel kembali keoutput.csv. - Bagian 6: Testing dan Build —
tests.cdengan 16 test case danMakefileakhir.
Mulai dari Bagian 1 dan ikuti urutannya. Berkas sumber lengkap tersedia pada bagian bawah halaman masing-masing part.
Implementasi csv_baca dengan fopen, fgets, dan pemanggilan pisahkan_baris.
Buka → # Bagian 2: Parsing RecordPisahkan field dengan strtok atau manual, hindari overflow.
Buka → # Bagian 3: Validasi DataValidasi tipe dan panjang field sebelum diproses.
Buka → # Bagian 4: Agregasi dan StatistikHitung total, rata-rata, min, max dari kolom numerik.
Buka → # Bagian 5: Export HasilTulis hasil agregasi ke file CSV baru.
Buka → # Bagian 6: Testing dan BuildTulis test parser dan build dengan Makefile.
Buka → # Buka →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;
}