Setelah statistik terhitung dan tabel ditampilkan ke layar, kita perlu menyimpan hasil pemrosesan ke file agar konsumen lain (spreadsheet, pipeline data, program berikutnya) dapat memakainya. Pada bagian ini kita menulis logika export di main.c yang mengiterasi CsvTable dan menulis setiap baris kembali ke output.csv dengan pemisah koma. Tujuan bagian ini adalah menghasilkan file CSV yang dapat dibuka oleh aplikasi lain tanpa modifikasi lebih lanjut.
Konsep #
Penulisan CSV adalah kebalikan dari pembacaan: kita mengiterasi setiap CsvRow di CsvTable lalu menulis field-field dipisah koma. Tiga pilihan teknis penting: (1) memakai fprintf dengan format "%s" untuk setiap field, (2) memakai fputs lalu fputc(',', out), atau (3) memakai fwrite untuk seluruh buffer. Kita pilih fprintf karena paling jelas dibaca dan otomatis menangani format string dengan %s.
Pertimbangan praktis: koma di akhir baris. Bila kita menulis field0,field1,field2, (dengan koma trailing), banyak parser CSV akan mengira baris berisi 4 field (yang keempat kosong). Solusi: tulis field pertama tanpa koma, lalu untuk field berikutnya tulis "," + field. Atau, tulis field dengan koma di depan kecuali field pertama: if (j > 0) fprintf(out, ","); sebelum fprintf(out, "%s", field).
Pemformatan numerik juga penting. Untuk angka bulat, format "%.0f" menghilangkan desimal (5000.0 -> 5000). Untuk angka dengan dua desimal, "%.2f" menghasilkan 5000.00. Pemilihan format bergantung pada kebutuhan konsumen — Excel lebih konsisten memproses CSV dengan format desimal lengkap.
Implementasi #
Potongan export di main.c:
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");
}Demo mandiri yang menulis tiga record ke file lalu membaca kembali untuk verifikasi:
#include <stdio.h>
#include <string.h>
typedef struct {
char nama[32];
int umur;
double gaji;
} Record;
int main(void) {
Record data[] = {
{"Budi", 25, 5000.0},
{"Siti", 30, 7500.0},
{"Andi", 28, 6200.0}
};
int n = (int)(sizeof(data) / sizeof(data[0]));
const char *out_path = "/tmp/p5b5_output.csv";
FILE *out = fopen(out_path, "w");
if (out == 0) {
fprintf(stderr, "Gagal membuka %s\n", out_path);
return 1;
}
fprintf(out, "nama,umur,gaji\n");
for (int i = 0; i < n; i++) {
fprintf(out, "%s,%d,%.0f\n",
data[i].nama, data[i].umur, data[i].gaji);
}
fclose(out);
printf("Hasil ditulis ke %s\n", out_path);
FILE *in = fopen(out_path, "r");
if (in == 0) {
fprintf(stderr, "Gagal membaca kembali %s\n", out_path);
return 1;
}
char baris[256];
printf("Isi file:\n");
while (fgets(baris, sizeof(baris), in) != 0) {
fputs(baris, stdout);
}
fclose(in);
return 0;
}Output yang Dihasilkan:
Hasil ditulis ke /tmp/p5b5_output.csv
Isi file:
nama,umur,gaji
Budi,25,5000
Siti,30,7500
Andi,28,6200Pengujian #
Jalankan program utama terhadap sample.csv lalu periksa isi output.csv:
gcc -std=c11 -Wall -Wextra -o program main.c csv_reader.c parser.c -lm
./program sample.csv 2 > /dev/null
cat output.csvOutput yang Dihasilkan:
nama,umur,gaji
Budi,25,5000
Siti,30,7500
Andi,28,6200
Dewi,35,9000
Eka,22,4500Verifikasi bahwa file dapat dibuka oleh parser sendiri (round-trip test):
./program output.csv 2 | tail -10Output yang Dihasilkan (potongan akhir):
Statistik kolom 'kolom':
Jumlah data : 5
Total : 32200
Minimum : 4500
Maksimum : 9000
Rata-rata : 6440
Hasil ditulis ke output.csvRound-trip test — file output kita baca kembali sebagai input dan menghasilkan statistik identik — membuktikan format CSV yang kita tulis dapat diparse dengan benar oleh pipeline sendiri.
Mari kita telaah baris-baris kunci pada loop export:
- Baris
FILE *out = fopen("output.csv", "w");: buka file untuk ditulis. Mode"w"membuat file baru (bila ada, isinya dihapus). Penting: jalankanfclosedi akhir agar buffer diflush ke disk. - Baris
if (out != 0): penjaga NULL.fopendapat gagal bila direktori tidak ada atau izin tulis ditolak. Tanpa penjaga,fprintf(out, ...)dereference NULL dan crash. - Baris
for (int i = 0; i < tabel.jumlah_baris; i++): iterasi luar mengakses setiapCsvRow. Indeksiberjalan dari 0 sampaijumlah_baris - 1— kita tidak menulis baris di luar array. - Baris
const CsvRow *r = &tabel.baris[i];: alias pointer. Alih-alih menulistabel.baris[i].fields[j]berulang, kita simpan alamat ke pointerr. Kompiler mengoptimalkan ini dengan baik tanpa overhead runtime. - Baris
for (int j = 0; j < r->jumlah_field; j++): iterasi dalam mengakses setiap field di baris. Setiap baris bisa memiliki jumlah field berbeda — kita pakair->jumlah_field(bukan konstanta global). - Baris
fprintf(out, "%s", r->fields[j]);: tulis field tanpa format. Penting memakai"%s"(bukanr->fields[j]langsung sebagai format string) — bila field berisi karakter%, fprintf akan salah menginterpretasikannya sebagai format specifier. - Baris
if (j < r->jumlah_field - 1) fprintf(out, ",");: koma pemisah. Tulis koma hanya bila bukan field terakhir. Pola ini mencegah koma trailing di akhir baris. - Baris
fprintf(out, "\n");: newline di akhir baris. Tanpa newline, semua baris menyatu menjadi satu baris panjang.
Pada Bagian 6 kita akan menulis rangkaian test yang memverifikasi seluruh pipeline — dari pembacaan CSV hingga statistik — berjalan benar.
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;
}