Proyek pengelola keuangan dimulai dari pertanyaan: bagaimana satu transaksi keuangan direpresentasikan dalam program? Setiap transaksi punya jenis (pemasukan atau pengeluaran), jumlah uang, deskripsi, kategori, dan tanggal. Pada bagian ini, kita rancang struct Transaction dengan field-field tersebut dan enum TxnType untuk menandai arah aliran uang.

Konsep #

Transaksi keuangan memiliki dua arah: uang masuk (income) dan uang keluar (expense). Kita pakai enum TxnType dengan nilai +1 untuk income dan -1 untuk expense agar mudah menghitung signed value:

text
signed_value = type * amount
            = (+1) * amount   (untuk income, menghasilkan positif)
            = (-1) * amount   (untuk expense, menghasilkan negatif)

Dengan pola ini, saldo bisa dihitung dengan menjumlahkan signed value semua transaksi tanpa perlu if-else di setiap perhitungan.

Mekanisme kerja validator tanggal: kita periksa panjang string (10 karakter), format posisi tanda hubung di indeks 4 dan 7, dan pastikan semua karakter lain adalah digit. Setelah format valid, kita parse dengan sscanf ke (y, m, d) dan periksa rentang: tahun 1900-2999, bulan 1-12, hari 1-31. Validator ini sederhana tetapi cukup untuk demo pembelajaran.

Implementasi #

Header transaction.h:

c
/* transaction.h - Model data transaksi keuangan */
#ifndef TRANSACTION_H
#define TRANSACTION_H

#define TXN_DESC_MAX     64
#define TXN_CATEGORY_MAX 32

typedef enum {
    TXN_INCOME = 1,
    TXN_EXPENSE = -1
} TxnType;

typedef struct {
    int      id;
    TxnType  type;
    double   amount;
    char     description[TXN_DESC_MAX];
    char     category[TXN_CATEGORY_MAX];
    char     date[11]; /* YYYY-MM-DD + null */
} Transaction;

int    transaction_create(Transaction *t, int id, TxnType type,
                          double amount, const char *description,
                          const char *category, const char *date);
double transaction_signed_value(const Transaction *t);
void   transaction_print(const Transaction *t);
int    transaction_date_is_valid(const char *date);

#endif /* TRANSACTION_H */

Implementasi validator tanggal dan create:

c
/* transaction.c - Implementasi model Transaction */
#include "transaction.h"
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <ctype.h>

int transaction_date_is_valid(const char *date) {
    if (date == NULL) {
        return 0;
    }
    if (strlen(date) != 10) {
        return 0;
    }
    for (int i = 0; i < 10; i++) {
        char c = date[i];
        if (i == 4 || i == 7) {
            if (c != '-') {
                return 0;
            }
        } else {
            if (!isdigit((unsigned char)c)) {
                return 0;
            }
        }
    }
    int y, m, d;
    if (sscanf(date, "%d-%d-%d", &y, &m, &d) != 3) {
        return 0;
    }
    if (y < 1900 || y > 2999) {
        return 0;
    }
    if (m < 1 || m > 12) {
        return 0;
    }
    if (d < 1 || d > 31) {
        return 0;
    }
    return 1;
}

int transaction_create(Transaction *t, int id, TxnType type,
                       double amount, const char *description,
                       const char *category, const char *date) {
    if (t == NULL || description == NULL ||
        category == NULL || date == NULL) {
        return -1;
    }
    if (id <= 0) {
        return -2;
    }
    if (type != TXN_INCOME && type != TXN_EXPENSE) {
        return -3;
    }
    if (amount <= 0.0 || isnan(amount) || isinf(amount)) {
        return -4;
    }
    if (!transaction_date_is_valid(date)) {
        return -5;
    }
    t->id = id;
    t->type = type;
    t->amount = amount;
    strncpy(t->description, description, TXN_DESC_MAX - 1);
    t->description[TXN_DESC_MAX - 1] = '\0';
    strncpy(t->category, category, TXN_CATEGORY_MAX - 1);
    t->category[TXN_CATEGORY_MAX - 1] = '\0';
    strncpy(t->date, date, sizeof(t->date) - 1);
    t->date[sizeof(t->date) - 1] = '\0';
    return 0;
}

double transaction_signed_value(const Transaction *t) {
    if (t == NULL) {
        return 0.0;
    }
    return t->type * t->amount;
}

void transaction_print(const Transaction *t) {
    if (t == NULL) {
        printf("(null transaction)\n");
        return;
    }
    const char *label = (t->type == TXN_INCOME) ? "MASUK " : "KELUAR";
    printf("[%04d] %s %s %s  Rp%12.2f  %s\n",
           t->id, t->date, label, t->category,
           t->type * t->amount, t->description);
}

Verifikasi model:

c
/* demo_txn.c - Uji coba model Transaction */
#include <stdio.h>
#include "transaction.h"

int main(void) {
    Transaction t1;
    int rc = transaction_create(&t1, 1, TXN_INCOME, 5000000.0,
                                "Gaji bulanan", "Gaji", "2024-03-01");
    printf("rc=%d signed=%.2f\n", rc, transaction_signed_value(&t1));
    transaction_print(&t1);

    Transaction t2;
    rc = transaction_create(&t2, 2, TXN_EXPENSE, 50000.0,
                            "Bensin", "Transport", "2024-03-02");
    printf("rc=%d signed=%.2f\n", rc, transaction_signed_value(&t2));
    transaction_print(&t2);

    /* Cek tanggal invalid */
    printf("cek '2024/03/01': %d\n", transaction_date_is_valid("2024/03/01"));
    printf("cek '2024-13-01': %d\n", transaction_date_is_valid("2024-13-01"));
    printf("cek '2024-03-32': %d\n", transaction_date_is_valid("2024-03-32"));
    printf("cek '2024-03-15': %d\n", transaction_date_is_valid("2024-03-15"));
    return 0;
}

Output yang Dihasilkan:

text
rc=0 signed=5000000.00
[0001] 2024-03-01 MASUK  Gaji  Rp  5000000.00  Gaji bulanan
rc=0 signed=-50000.00
[0002] 2024-03-02 KELUAR Transport  Rp   -50000.00  Bensin
cek '2024/03/01': 0
cek '2024-13-01': 0
cek '2024-03-32': 0
cek '2024-03-15': 1

Mari kita telaah baris-baris kode di atas:

  1. Baris 7-10 (TxnType enum): Nilai enum +1/-1 bukan kebetulan. signed_value = type * amount menghasilkan positif untuk income dan negatif untuk expense tanpa if-else. Pola ini mempermudah akumulasi saldo.
  2. Baris 13 (char date[11]): Field date berukuran 11 untuk menampung YYYY-MM-DD (10 karakter) + null terminator. Ukuran eksplisit menghindari overflow.
  3. Baris 24-30 (strlen(date) != 10): Validasi panjang paling awal. Bila tanggal salah panjang, kita bisa langsung return tanpa iterasi karakter.
  4. Baris 31-41 (loop karakter): Iterasi 10 karakter. Posisi 4 dan 7 harus -, posisi lain harus digit. Pakai isdigit dari <ctype.h> dengan cast (unsigned char) untuk menghindari undefined behavior pada karakter negatif.
  5. Baris 43-46 (sscanf): Setelah format karakter valid, parse ke (y, m, d) untuk validasi rentang. sscanf dipakai karena format sudah dipastikan benar.
  6. Baris 80-82 (transaction_print): Format cetak menampilkan tanda signed value (positif untuk income, negatif untuk expense). Rp%12.2f menghasilkan lebar 12 karakter dengan 2 desimal, sehingga kolom nominal rapi sejajar.

Kode Lengkap #

Kode lengkap untuk transaction.h dan transaction.c ada di direktori proyek. Kode ini menjadi fondasi untuk semua bagian berikutnya.

Pengujian #

Verifikasi model dengan test edge case:

  • Tanggal dengan bulan 13 → ditolak.
  • Tanggal dengan hari 32 → ditolak.
  • Tanggal dengan format slash 2024/03/15 → ditolak.
  • Tanggal dengan tahun 1899 → ditolak (di bawah batas minimum).
  • Amount negatif → ditolak.
  • Amount NaN → ditolak.
bash
gcc -std=c11 -Wall -Wextra -o /tmp/demo_txn demo_txn.c \
    transaction.c -lm
/tmp/demo_txn