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):

text
$ ./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.csv

Hasil 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 strtod dan endptr,
  • 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/.c dan parser.h/.c,
  • Struct dan Enum — CsvRow, CsvTable, Statistik,
  • Command Line Arguments — argc dan argv.

Tahapan Pengerjaan #

Proyek dikerjakan dalam enam bagian berurutan:

  1. Bagian 1: Membaca File — csv_baca yang membuka file, membaca baris dengan fgets, dan memanggil pisahkan_baris.
  2. Bagian 2: Parsing Record — fungsi pisahkan_baris yang memisahkan string berdasarkan koma.
  3. Bagian 3: Validasi Data — parser_ke_double dan parser_valid_angka untuk konversi numerik aman.
  4. Bagian 4: Agregasi dan Statistik — parser_statistik_kolom yang menghitung jumlah, total, min, maks, rata-rata dalam satu pass.
  5. Bagian 5: Export Hasil — main.c menulis tabel kembali ke output.csv.
  6. Bagian 6: Testing dan Build — tests.c dengan 16 test case dan Makefile akhir.

Mulai dari Bagian 1 dan ikuti urutannya. Berkas sumber lengkap tersedia pada bagian bawah halaman masing-masing part.

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

csv_reader.c

Unduh berkas
c
/* 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
c
/* 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
c
/* 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
c
/* 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;
}