Field-field hasil parsing di Bagian 2 berupa string bebas. Sebelum string tersebut dapat dipakai sebagai angka untuk agregat, kita perlu mengonversi ke double dengan cara yang aman. Pada bagian ini kita menulis parser_ke_double dan parser_valid_angka di parser.c. Tujuan bagian ini adalah memastikan setiap field numerik terkonversi dengan rapi dan field non-numerik (misalnya "bukan-angka") ditolak tanpa crash.

Konsep #

Konversi string-ke-double paling rawan dilakukan dengan atof dari <stdlib.h>. Fungsi atof mengembalikan 0.0 bila string bukan angka — sebuah nilai yang sah secara numerik, sehingga pemanggil tidak bisa membedakan antara “input benar-benar nol” dan “input gagal konversi”. Untuk membedakan kedua kasus ini, kita memakai strtod dengan parameter endptr yang melaporkan posisi karakter pertama yang tidak terkonversi.

Mekanisme strtod: fungsi ini melewati whitespace di awal, lalu membaca angka dalam format [-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)? dan menyimpan alamat karakter pertama yang tidak dikenali ke *endptr. Bila endptr == s (pointer tidak maju sama sekali), tidak ada karakter yang berhasil dikonversi — string bukan angka. Bila *endptr menunjuk ke karakter bukan null dan bukan whitespace sisa, ada karakter sampah setelah angka (misalnya "12abc").

Untuk memperketat aturan, kita menambahkan dua pemeriksaan: (1) skip whitespace awal dan tolak string kosong, (2) setelah strtod, lompati whitespace sisa lalu pastikan *endptr == '\0'. Aturan ini membuat " 42 " valid (dibersihkan) tetapi "12abc" dan "1,5" (titik desimal Indonesia) ditolak.

Implementasi #

Fungsi parser_ke_double di parser.c:

c
#include <stdlib.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;
}

Versi ketat yang juga menolak karakter sampah setelah angka:

c
#include <stdio.h>
#include <stdlib.h>

typedef enum {
    PARSER_OK = 0,
    PARSER_ERR_KONVERSI = 3
} ParserStatus;

static ParserStatus parser_ke_double(const char *s, double *out) {
    if (s == 0 || out == 0) return PARSER_ERR_KONVERSI;
    while (*s == ' ' || *s == '\t') s++;
    if (*s == '\0') return PARSER_ERR_KONVERSI;
    char *sisa = 0;
    double v = strtod(s, &sisa);
    while (*sisa == ' ' || *sisa == '\t') sisa++;
    if (*sisa != '\0' || sisa == s) return PARSER_ERR_KONVERSI;
    *out = v;
    return PARSER_OK;
}

static int parser_valid_angka(const char *s) {
    double dummy;
    return parser_ke_double(s, &dummy) == PARSER_OK;
}

int main(void) {
    const char *samples[] = {
        "3.14", "  42  ", "100", "0.5", "-7.2e3",
        "bukan-angka", "", "12abc", "1,5"
    };
    int n = (int)(sizeof(samples) / sizeof(samples[0]));
    for (int i = 0; i < n; i++) {
        double v = 0.0;
        ParserStatus s = parser_ke_double(samples[i], &v);
        if (s == PARSER_OK) {
            printf("'%s' valid -> %g (valid_angka=%d)\n",
                   samples[i], v, parser_valid_angka(samples[i]));
        } else {
            printf("'%s' TIDAK valid (valid_angka=%d)\n",
                   samples[i], parser_valid_angka(samples[i]));
        }
    }
    return 0;
}

Output yang Dihasilkan:

text
'3.14' valid -> 3.14 (valid_angka=1)
'  42  ' valid -> 42 (valid_angka=1)
'100' valid -> 100 (valid_angka=1)
'0.5' valid -> 0.5 (valid_angka=1)
'-7.2e3' valid -> -7200 (valid_angka=1)
'bukan-angka' TIDAK valid (valid_angka=0)
'' TIDAK valid (valid_angka=0)
'12abc' TIDAK valid (valid_angka=0)
'1,5' TIDAK valid (valid_angka=0)

Pengujian #

Untuk memverifikasi integrasi dengan pipeline utama, kita tampilkan apa yang terjadi saat file CSV berisi field non-numerik di kolom yang seharusnya angka. Buat file /tmp/p5b3_bad.csv:

bash
printf 'nama,umur\nBudi,bukan-umur\nSiti,30\n' > /tmp/p5b3_bad.csv
gcc -std=c11 -Wall -Wextra -o /tmp/p5b3_program \
    /home/z/my-project/content/c/proyek-c/proyek-05-pemroses-file-csv/main.c \
    /home/z/my-project/content/c/proyek-c/proyek-05-pemroses-file-csv/csv_reader.c \
    /home/z/my-project/content/c/proyek-c/proyek-05-pemroses-file-csv/parser.c -lm
/tmp/p5b3_program /tmp/p5b3_bad.csv 1 2>&1

Output yang Dihasilkan:

text
Berhasil memuat 3 baris (termasuk header).

nama | umur
Budi | bukan-umur
Siti | 30
Gagal menghitung statistik kolom 1 (kode 3)

Baris Gagal menghitung ... ditulis ke stderr oleh main.c — redirect 2>&1 menggabungkannya dengan stdout agar tampak dalam urutan yang sama. Kode 3 adalah PARSER_ERR_KONVERSI — pipeline berhenti di baris "Budi" karena field "bukan-umur" tidak dapat dikonversi menjadi double. Pesan error spesifik ini muncul berkat pemisahan kode status di enum ParserStatus.

Mari kita telaah baris-baris kunci pada parser_ke_double versi ketat:

  1. Baris if (s == 0 || out == 0) return PARSER_ERR_KONVERSI;: penjaga NULL ganda. Parameter input maupun output diperiksa — bila salah satu NULL, kita laporkan error konversi alih-alih crash.
  2. Baris while (*s == ' ' || *s == '\t') s++;: skip whitespace awal. Setelah loop, s menunjuk ke karakter pertama yang bukan whitespace. Pointer s adalah parameter lokal — perubahan tidak memengaruhi pemanggil karena s adalah const char * yang di-pass by value.
  3. Baris if (*s == '\0') return PARSER_ERR_KONVERSI;: tolak string kosong setelah trim. Tanpa baris ini, strtod("", ...) mengembalikan 0.0 yang terlihat valid — namun sebenarnya tidak ada angka yang diparse.
  4. Baris double v = strtod(s, &sisa);: konversi inti. strtod menulis alamat karakter pertama yang tidak dikonversi ke sisa. Bila sisa == s (tidak maju), tidak ada angka sama sekali.
  5. Baris while (*sisa == ' ' || *sisa == '\t') sisa++;: skip whitespace sisa. " 42 " seharusnya valid — kita toleransi spasi setelah angka.
  6. Baris if (*sisa != '\0' || sisa == s) return PARSER_ERR_KONVERSI;: dua kondisi penolakan. Kondisi pertama menolak karakter sampah ("12abc" — sisa menunjuk ke 'a'). Kondisi kedua menolak string tanpa angka ("abc" — sisa == s).
  7. Baris *out = v; return PARSER_OK;: tulis hasil konversi hanya bila semua pemeriksaan lulus. Pola ini mencegah pemanggil membaca nilai sampah dari *out saat error.

Pada Bagian 4 kita akan memakai parser_ke_double untuk menghitung agregat statistik (jumlah, total, min, maks, rata-rata) dalam satu pass.

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;
}