Data transaksi sudah bisa disimpan dan dimuat. Sekarang saatnya mengubah data mentah menjadi insight: laporan per kategori yang menunjukkan kategori mana yang paling banyak pengeluarannya, dan laporan bulanan yang menunjukkan tren pemasukan/pengeluaran per bulan. Pada bagian ini, kita bangun modul report.c dengan dua fungsi utama.

Konsep #

Laporan per kategori mengelompokkan transaksi berdasarkan field category (case-insensitive). Untuk setiap kategori unik, kita hitung total pemasukan, total pengeluaran, dan saldo. Hasil disimpan ke array CategoryReport.

Laporan bulanan mengelompokkan transaksi berdasarkan 7 karakter pertama tanggal (YYYY-MM). Karena data sudah terurut kronologis (atau bisa diurutkan dulu), kita iterasi dan cetak subtotal tiap kali bulan berganti.

Mekanisme kerja: untuk laporan per kategori, kita iterasi transaksi dan untuk setiap transaksi cari apakah kategori-nya sudah ada di array out. Bila ada, tambah ke kategori yang sudah ada. Bila belum, buat entri baru. Pola ini mirip dengan dictionary/map di bahasa lain, diimplementasikan secara manual di C.

Implementasi #

Struktur CategoryReport dan prototype:

c
/* report.h - Laporan keuangan */
#ifndef REPORT_H
#define REPORT_H

#include "finance.h"

typedef struct {
    char category[TXN_CATEGORY_MAX];
    double income;
    double expense;
    double balance;
    int    count;
} CategoryReport;

int  report_by_category(const Finance *f, CategoryReport *out, int out_cap);
void report_print_category(const CategoryReport *r, int count);
void report_print_monthly(const Finance *f);

#endif /* REPORT_H */

Implementasi report_by_category:

c
static void cat_report_init(CategoryReport *r, const char *category) {
    memset(r, 0, sizeof(*r));
    strncpy(r->category, category, TXN_CATEGORY_MAX - 1);
    r->category[TXN_CATEGORY_MAX - 1] = '\0';
}

int report_by_category(const Finance *f, CategoryReport *out, int out_cap) {
    if (f == NULL || out == NULL || out_cap <= 0) {
        return 0;
    }
    int unique = 0;
    for (int i = 0; i < f->count; i++) {
        const Transaction *t = &f->items[i];
        int found = -1;
        for (int k = 0; k < unique; k++) {
            if (eq_ci(out[k].category, t->category)) {
                found = k;
                break;
            }
        }
        if (found < 0) {
            if (unique >= out_cap) {
                continue;
            }
            cat_report_init(&out[unique], t->category);
            found = unique;
            unique++;
        }
        if (t->type == TXN_INCOME) {
            out[found].income += t->amount;
        } else {
            out[found].expense += t->amount;
        }
        out[found].balance += transaction_signed_value(t);
        out[found].count++;
    }
    return unique;
}

Fungsi cetak laporan per kategori:

c
void report_print_category(const CategoryReport *r, int count) {
    if (r == NULL || count <= 0) {
        printf("Tidak ada data untuk laporan.\n");
        return;
    }
    printf("\n=== Laporan per Kategori ===\n");
    printf("%-20s %12s %12s %12s  %6s\n",
           "Kategori", "Pemasukan", "Pengeluaran", "Saldo", "#Txn");
    double total_inc = 0, total_exp = 0;
    for (int i = 0; i < count; i++) {
        printf("%-20s %12.2f %12.2f %12.2f  %6d\n",
               r[i].category, r[i].income, r[i].expense,
               r[i].balance, r[i].count);
        total_inc += r[i].income;
        total_exp += r[i].expense;
    }
    printf("%-20s %12.2f %12.2f %12.2f\n",
           "TOTAL", total_inc, total_exp, total_inc - total_exp);
}

Fungsi laporan bulanan:

c
void report_print_monthly(const Finance *f) {
    if (f == NULL || f->count == 0) {
        printf("Tidak ada data untuk laporan bulanan.\n");
        return;
    }
    printf("\n=== Laporan Bulanan ===\n");
    printf("%-8s %12s %12s %12s\n",
           "Bulan", "Pemasukan", "Pengeluaran", "Saldo");

    char current[8] = "";
    double m_inc = 0, m_exp = 0;
    for (int i = 0; i < f->count; i++) {
        const Transaction *t = &f->items[i];
        char month[8];
        snprintf(month, sizeof(month), "%.7s", t->date);
        if (strcmp(month, current) != 0) {
            if (current[0] != '\0') {
                printf("%-8s %12.2f %12.2f %12.2f\n",
                       current, m_inc, m_exp, m_inc - m_exp);
            }
            strcpy(current, month);
            m_inc = 0;
            m_exp = 0;
        }
        if (t->type == TXN_INCOME) {
            m_inc += t->amount;
        } else {
            m_exp += t->amount;
        }
    }
    if (current[0] != '\0') {
        printf("%-8s %12.2f %12.2f %12.2f\n",
               current, m_inc, m_exp, m_inc - m_exp);
    }
}

Verifikasi laporan:

c
/* demo_report.c - Uji coba laporan */
#include <stdio.h>
#include "finance.h"
#include "report.h"

int main(void) {
    Finance f;
    finance_init(&f);
    finance_add(&f, TXN_INCOME, 5000000.0, "Gaji", "Gaji", "2024-03-01");
    finance_add(&f, TXN_EXPENSE, 50000.0, "Bensin", "Transport", "2024-03-02");
    finance_add(&f, TXN_EXPENSE, 30000.0, "Makan siang", "Makan", "2024-03-02");
    finance_add(&f, TXN_EXPENSE, 20000.0, "Cafe", "makan", "2024-03-03");
    finance_add(&f, TXN_INCOME, 1000000.0, "Bonus", "Gaji", "2024-03-15");
    finance_add(&f, TXN_EXPENSE, 75000.0, "Bensin", "Transport", "2024-04-05");
    finance_add(&f, TXN_EXPENSE, 40000.0, "Makan", "Makan", "2024-04-05");

    /* Laporan per kategori. */
    CategoryReport reports[16];
    int n = report_by_category(&f, reports, 16);
    report_print_category(reports, n);

    /* Laporan bulanan. */
    report_print_monthly(&f);
    return 0;
}

Output yang Dihasilkan:

text
=== Laporan per Kategori ===
Kategori              Pemasukan  Pengeluaran        Saldo   #Txn
Gaji                6000000.00        0.00   6000000.00       2
Transport                 0.00    125000.00   -125000.00       2
Makan                     0.00    90000.00    -90000.00       3
TOTAL               6000000.00   215000.00  5785000.00

=== Laporan Bulanan ===
Bulan    Pemasukan  Pengeluaran        Saldo
2024-03  6000000.00    100000.00  5900000.00
2024-04         0.00    115000.00  -115000.00

Mari kita telaah baris-baris kode di atas:

  1. Baris 1-7 (cat_report_init): Helper inisialisasi entry baru. Pakai memset(r, 0, sizeof(*r)) agar semua field numerik mulai dari 0. strncpy dengan terminator eksplisit memastikan string aman.
  2. Baris 9-33 (report_by_category): Linear search untuk kategori yang sudah ada di out. Bila tidak ada, buat entri baru di posisi unique. Variabel found berfungsi sebagai indeks ke kategori yang akan diakumulasi.
  3. Baris 25-28 (out_cap check): Bila kategori baru tidak muat di out, continue ke transaksi berikutnya. Kategori tersebut tidak dimasukkan ke laporan. Pemanggil tahu bahwa laporan mungkin tidak lengkap bila out_cap terlalu kecil.
  4. Baris 30-34 (akumulasi): Tambah amount ke field yang sesuai berdasarkan type. transaction_signed_value dipakai untuk balance agar otomatis positif untuk income, negatif untuk expense.
  5. Baris 51-59 (subtotal per kategori): Format %-20s (left-aligned, lebar 20) untuk nama kategori. %12.2f (right-aligned, lebar 12, 2 desimal) untuk angka uang. Baris terakhir cetak TOTAL di kolom kategori.
  6. Baris 78-80 (extract month): snprintf(month, sizeof(month), "%.7s", t->date) mengambil 7 karakter pertama tanggal. Hasilnya 2024-03 dari 2024-03-15. Pola ini bekerja karena format tanggal sudah diperbaiki (YYYY-MM-DD).
  7. Baris 82-90 (deteksi pergantian bulan): Bandingkan month saat ini dengan current. Bila beda, cetak subtotal bulan sebelumnya dan reset counter. Pola “state machine” sederhana.
  8. Baris 100-103 (flush terakhir): Setelah loop selesai, cetak subtotal bulan terakhir yang belum sempat dicetak di dalam loop. Tanpa langkah ini, bulan terakhir akan hilang dari laporan.

Kode Lengkap #

Kode lengkap untuk report.h dan report.c ada di direktori proyek. Fungsi eq_ci dipakai ulang dari finance.c (atau bisa ditaruh di file helper bersama).

Pengujian #

Edge case yang harus dipastikan:

  • Finance kosong → cetak “Tidak ada data”.
  • Finance dengan satu kategori → laporan hanya punya satu baris + TOTAL.
  • Finance dengan kategori yang sama persis dengan kapitalisasi berbeda (“Makan” vs “makan”) → dianggap satu kategori karena eq_ci.
  • Laporan bulanan dengan transaksi yang tanggal tidak berurutan → grouping bisa salah. Solusi: sort by date dulu sebelum cetak laporan bulanan.
bash
gcc -std=c11 -Wall -Wextra -o /tmp/demo_report demo_report.c \
    report.c finance.c transaction.c -lm
/tmp/demo_report