Setelah field numerik terkonversi menjadi double, langkah berikutnya adalah menghitung agregat statistik pada satu kolom. Pada bagian ini kita menulis parser_statistik_kolom di parser.c yang menghasilkan struct Statistik berisi jumlah data, total, minimum, maksimum, dan rata-rata. Tujuan bagian ini adalah menghasilkan angka-angka yang siap ditampilkan ke pengguna dan ditulis ke file output.
Konsep #
Agregat statistik dapat dihitung dengan dua strategi: dua pass (pass pertama mencari min/maks, pass kedua menghitung total) atau satu pass (semua agregat dihitung dalam satu iterasi). Kita pilih strategi satu pass karena lebih efisien — cukup sekular menyentuh setiap baris. Keuntungan praktis: file CSV besar hanya dibaca sekali dari memori (sudah ditampung di CsvTable), dan kompleksitas waktu tetap O(n) dengan n = jumlah baris.
Mekanisme perhitungan satu pass: inisialisasi minimum dan maksimum dengan nilai pertama, lalu untuk setiap nilai berikutnya bandingkan dengan minimum dan maksimum yang ada dan update bila perlu. Akumulasi jumlah_nilai dilakukan dengan += setiap iterasi. Setelah loop selesai, hitung rata_rata = jumlah_nilai / jumlah. Penting untuk menangani kasus data kosong secara eksplisit — bila tidak ada data sama sekali, kita laporkan PARSER_ERR_TIDAK_ADA_DATA dan tidak melakukan pembagian (yang akan menghasilkan NaN atau pembagian dengan nol).
Header skip juga merupakan pertimbangan: baris pertama file CSV biasanya berisi nama kolom ("nama,umur,gaji"), bukan data. Fungsi parser_statistik_kolom menerima parameter skip_header — bila 1, iterasi dimulai dari indeks 1, bukan 0.
Implementasi #
Fungsi parser_statistik_kolom di parser.c:
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;
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;
}Demo mandiri yang menghitung agregat dari array double:
#include <stdio.h>
typedef struct {
int jumlah;
double jumlah_nilai;
double minimum;
double maksimum;
double rata_rata;
} Statistik;
static void hitung_statistik(const double *data, int n, Statistik *s) {
s->jumlah = 0;
s->jumlah_nilai = 0.0;
s->minimum = 0.0;
s->maksimum = 0.0;
s->rata_rata = 0.0;
if (n <= 0) return;
s->minimum = data[0];
s->maksimum = data[0];
for (int i = 0; i < n; i++) {
if (data[i] < s->minimum) s->minimum = data[i];
if (data[i] > s->maksimum) s->maksimum = data[i];
s->jumlah_nilai += data[i];
s->jumlah++;
}
s->rata_rata = s->jumlah_nilai / s->jumlah;
}
int main(void) {
double gaji[] = {5000, 7500, 6200, 9000, 4500};
int n = (int)(sizeof(gaji) / sizeof(gaji[0]));
Statistik s;
hitung_statistik(gaji, n, &s);
printf("Statistik kolom gaji:\n");
printf(" Jumlah data : %d\n", s.jumlah);
printf(" Total : %g\n", s.jumlah_nilai);
printf(" Minimum : %g\n", s.minimum);
printf(" Maksimum : %g\n", s.maksimum);
printf(" Rata-rata : %g\n", s.rata_rata);
Statistik kosong;
hitung_statistik(0, 0, &kosong);
printf("\nKasus data kosong: jumlah=%d total=%g\n",
kosong.jumlah, kosong.jumlah_nilai);
return 0;
}Output yang Dihasilkan:
Statistik kolom gaji:
Jumlah data : 5
Total : 32200
Minimum : 4500
Maksimum : 9000
Rata-rata : 6440
Kasus data kosong: jumlah=0 total=0Pengujian #
Untuk memverifikasi integrasi dengan pipeline utama, jalankan program terhadap sample.csv dengan kolom 2 (gaji):
gcc -std=c11 -Wall -Wextra -o program main.c csv_reader.c parser.c -lm
./program sample.csv 2Output yang Dihasilkan:
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.csvMari kita telaah baris-baris kunci pada parser_statistik_kolom:
- Baris
if (tabel == 0 || stat == 0) return PARSER_ERR_TIDAK_ADA_DATA;: penjaga NULL. Parameter input dan output diperiksa bersama. Memakai kodeTIDAK_ADA_DATA(bukanKONVERSI) karena error ini bukan masalah konversi melainkan masalah pemanggilan. - Baris
if (kolom < 0 || kolom >= CSV_MAKS_FIELD) return PARSER_ERR_KOLOM_INVALID;: validasi rentang kolom. Indeks kolom di luar[0, 16)langsung ditolak sebelum loop dimulai — mencegah akses array out-of-bounds. - Baris
int mulai = skip_header ? 1 : 0;: header skip. Bilaskip_header == 1, iterasi dimulai dari indeks 1 (baris kedua). Parameter ini memungkinkan fungsi yang sama dipakai untuk CSV dengan atau tanpa header. - Baris
if (kolom >= row->jumlah_field) return PARSER_ERR_KOLOM_INVALID;: pemeriksaan per-baris. Setiap baris bisa memiliki jumlah field berbeda (bila CSV tidak rapi). Kita periksa sebelum mengaksesrow->fields[kolom]. - Baris
if (!ada_data) { ... } else { ... }: inisialisasi min/maks pada elemen pertama. Pola ini lebih aman daripada menginisialisasiminimum = 1e18(angka besar ajaib) yang dapat gagal bila data berisi nilai lebih besar. - Baris
stat->jumlah_nilai += v; stat->jumlah++;: akumulasi total dan jumlah. Dilakukan setelah pemeriksaan min/maks agarjumlahbenar-benar menghitung elemen yang valid. - Baris
if (!ada_data) return PARSER_ERR_TIDAK_ADA_DATA;di luar loop: kasus tabel kosong. Bila file hanya berisi header (atauskip_header == 1dan tidak ada baris data),ada_datatetap0— kita laporkan error spesifik. - Baris
stat->rata_rata = stat->jumlah_nilai / stat->jumlah;: pembagian rata-rata. Aman karenastat->jumlah > 0(sudah diperiksa di atas). Tanpa pemeriksaan, pembagian dengan nol menghasilkan NaN.
Pada Bagian 5 kita akan menulis tabel dan statistik tersebut ke file output.csv untuk konsumen lain.
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;
}