Pada proyek keempat ini kita membangun aplikasi pengelola tugas (to-do list) yang lebih kompleks dari manajemen kontak: setiap tugas punya judul, deskripsi, status (Belum/Proses/Selesai), dan prioritas (Rendah/Sedang/Tinggi). Kita menambahkan dua fitur baru yang tidak ada di Proyek 03: filter berdasarkan status dan pencarian judul berdasarkan substring. Program tetap mempertahankan data ke file tugas.txt dengan format pipa-separated.
Program yang akan kita bangun punya tujuh menu: tambah, tampilkan, filter status, cari judul, set status, hapus, dan keluar. Saat keluar, seluruh tugas disimpan ke file; saat dibuka kembali, file dimuat otomatis sehingga daftar tugas tetap utuh lintas sesi.
Tampilan Program Akhir #
Sesi pemakaian berikut menunjukkan alur: tambah dua tugas, tampilkan, ubah status satu tugas ke Proses, filter status Proses, lalu keluar:
Belum ada file tugas.txt. Memulai dengan daftar kosong.
=== Pengelola Tugas ===
1. Tambah
2. Tampilkan
3. Filter status
4. Cari judul
5. Set status
6. Hapus
7. Keluar
Pilih: Judul : Deskripsi : Prioritas (1=Rendah,2=Sedang,3=Tinggi): Tugas ditambahkan (id=1).
=== Pengelola Tugas ===
1. Tambah
2. Tampilkan
3. Filter status
4. Cari judul
5. Set status
6. Hapus
7. Keluar
Pilih: Judul : Deskripsi : Prioritas (1=Rendah,2=Sedang,3=Tinggi): Tugas ditambahkan (id=2).
=== Pengelola Tugas ===
1. Tambah
2. Tampilkan
3. Filter status
4. Cari judul
5. Set status
6. Hapus
7. Keluar
Pilih: ID Judul Status Prioritas
1 Belajar C Belum Tinggi
2 Belanja Belum Sedang
Total: 2 tugas
=== Pengelola Tugas ===
1. Tambah
2. Tampilkan
3. Filter status
4. Cari judul
5. Set status
6. Hapus
7. Keluar
Pilih: Filter status (0=Belum,1=Proses,2=Selesai): ID Judul Status Prioritas
2 Belanja Proses Sedang
Ditemukan: 1 tugas
Menyimpan 2 tugas ke tugas.txt.
Terima kasih.Saat program dibuka kembali, daftar tugas dimuat dari tugas.txt sehingga data sebelumnya tetap tersedia.
Tujuan Pembelajaran #
Setelah menyelesaikan proyek ini, kita mampu:
- mendefinisikan
struct Taskdengan beberapa field bertipe enum (TaskStatus,TaskPrioritas), - mengimplementasikan operasi CRUD lengkap plus filter berbasis status dan pencarian substring dengan
strstr, - memisahkan modul
task.c(logika) daristorage.c(persistensi) danmain.c(I/O), - mempertahankan enum ke file teks dengan casting ke
intdan kembali ke enum saat memuat, - menulis test yang menguji filter, pencarian, dan round-trip simpan-muat.
Prasyarat #
Proyek ini memakai materi bab sebelumnya:
- Struct dan Enum —
Task,TaskList,TaskStatus,TaskPrioritas, - Array dan String —
strstr,strncpy,strcmp, - Fungsi dan Header Files — modularisasi tiga modul,
- File I/O Dasar —
fopen,fprintf,sscanf,fgets,fclose, - Pointer —
TaskList *list,FILE *out.
Tahapan Pengerjaan #
Proyek dikerjakan dalam enam bagian berurutan:
- Bagian 1: Task Model — mendefinisikan
struct Task,TaskList, dan dua enum status/prioritas. - Bagian 2: CRUD Tugas — implementasi tambah, tampilkan, set status, dan hapus.
- Bagian 3: Filter dan Pencarian —
task_filter_statusdantask_caridenganstrstr. - Bagian 4: Penyimpanan Data —
storage_simpandanstorage_muatdengan enum dipertahankan sebagai integer. - Bagian 5: Pemisahan Modul — memisahkan
task.h/.c,storage.h/.c, danmain.cdengan boundary yang jelas. - Bagian 6: Testing —
tests.cdengan 21 test case danMakefileakhir.
Mulai dari Bagian 1 dan ikuti urutannya. Berkas sumber lengkap tersedia pada bagian bawah halaman masing-masing part.
Definisikan struct Task, TaskList, dan enum status/prioritas.
Buka → # Bagian 2: CRUD TugasImplementasi tambah, tampilkan, set status, dan hapus.
Buka → # Bagian 3: Filter dan PencarianImplementasi task_filter_status dan task_cari dengan strstr.
Buka → # Bagian 4: Penyimpanan DataImplementasi storage_simpan dan storage_muat dengan enum dipertahankan sebagai integer.
Buka → # Bagian 5: Pemisahan ModulSusun task.h/.c, storage.h/.c, dan main.c dengan boundary jelas.
Buka → # Bagian 6: TestingTulis tests.c dengan 21 test case dan verifikasi Makefile.
Buka → # Buka →Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
main.c
Unduh berkas/* main.c -- titik masuk aplikasi pengelola tugas. */
#include "task.h"
#include "storage.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define DB_PATH "tugas.txt"
static void baca_baris(const char *prompt, char *buf, size_t ukuran) {
printf("%s", prompt);
if (fgets(buf, (int)ukuran, stdin) == 0) {
buf[0] = '\0';
return;
}
size_t n = strlen(buf);
while (n > 0 && (buf[n - 1] == '\n' || buf[n - 1] == '\r')) {
buf[--n] = '\0';
}
}
static int baca_int(const char *prompt, int *out) {
char buf[32];
baca_baris(prompt, buf, sizeof(buf));
char *sisa = 0;
long v = strtol(buf, &sisa, 10);
if (sisa == buf) return 0;
*out = (int)v;
return 1;
}
static void menu_tambah(TaskList *list) {
char judul[TASK_JUDUL_MAKS];
char deskripsi[TASK_DESKRIPSI_MAKS];
baca_baris("Judul : ", judul, sizeof(judul));
baca_baris("Deskripsi : ", deskripsi, sizeof(deskripsi));
int p = 0;
if (!baca_int("Prioritas (1=Rendah,2=Sedang,3=Tinggi): ", &p)) {
printf("Prioritas tidak valid.\n"); return;
}
TaskResult r = task_tambah(list, judul, deskripsi, (TaskPrioritas)p);
if (r == TASK_OK) {
printf("Tugas ditambahkan (id=%d).\n", list->data[list->jumlah - 1].id);
} else {
printf("Gagal menambah (kode %d).\n", r);
}
}
static void menu_set_status(TaskList *list) {
int id, s;
if (!baca_int("ID tugas : ", &id)) { printf("ID tidak valid.\n"); return; }
if (!baca_int("Status (0=Belum,1=Proses,2=Selesai): ", &s)) {
printf("Status tidak valid.\n"); return;
}
TaskResult r = task_set_status(list, id, (TaskStatus)s);
if (r == TASK_OK) printf("Status diubah.\n");
else if (r == TASK_ERR_TIDAK_DITEMUKAN) printf("Tugas tidak ditemukan.\n");
else printf("Input tidak valid.\n");
}
static void menu_hapus(TaskList *list) {
int id;
if (!baca_int("ID tugas yang dihapus: ", &id)) { printf("ID tidak valid.\n"); return; }
TaskResult r = task_hapus(list, id);
if (r == TASK_OK) printf("Tugas dihapus.\n");
else if (r == TASK_ERR_TIDAK_DITEMUKAN) printf("Tugas tidak ditemukan.\n");
}
static void menu_filter(const TaskList *list) {
int s;
if (!baca_int("Filter status (0=Belum,1=Proses,2=Selesai): ", &s)) {
printf("Status tidak valid.\n"); return;
}
if (s < TASK_STATUS_BELUM || s > TASK_STATUS_SELESAI) {
printf("Status tidak valid.\n"); return;
}
task_filter_status(list, (TaskStatus)s, stdout);
}
static void menu_cari(const TaskList *list) {
char kata[TASK_JUDUL_MAKS];
baca_baris("Kata kunci judul: ", kata, sizeof(kata));
int idx = task_cari(list, kata);
if (idx < 0) {
printf("Tidak ada tugas dengan judul mengandung '%s'.\n", kata);
} else {
const Task *t = &list->data[idx];
printf("Ditemukan: [%d] %s | %s | %s\n",
t->id, t->judul,
task_status_nama(t->status),
task_prioritas_nama(t->prioritas));
}
}
int main(void) {
TaskList list;
task_list_init(&list);
StorageStatus sm = storage_muat(&list, DB_PATH);
if (sm == STORAGE_OK) {
printf("Memuat %d tugas dari %s.\n", list.jumlah, DB_PATH);
} else if (sm == STORAGE_ERR_BUKA_FILE) {
printf("Belum ada file %s. Memulai dengan daftar kosong.\n", DB_PATH);
} else {
printf("Peringatan: format file rusak (kode %d).\n", sm);
}
int lanjut = 1;
while (lanjut) {
printf("\n=== Pengelola Tugas ===\n");
printf("1. Tambah\n2. Tampilkan\n3. Filter status\n");
printf("4. Cari judul\n5. Set status\n6. Hapus\n7. Keluar\n");
int pilihan = 0;
if (!baca_int("Pilih: ", &pilihan)) { printf("Pilihan tidak valid.\n"); continue; }
switch (pilihan) {
case 1: menu_tambah(&list); break;
case 2: task_tampilkan(&list, stdout); break;
case 3: menu_filter(&list); break;
case 4: menu_cari(&list); break;
case 5: menu_set_status(&list); break;
case 6: menu_hapus(&list); break;
case 7: lanjut = 0; break;
default: printf("Pilihan tidak dikenal.\n"); break;
}
}
StorageStatus ss = storage_simpan(&list, DB_PATH);
if (ss == STORAGE_OK) printf("Menyimpan %d tugas ke %s.\n", list.jumlah, DB_PATH);
else printf("Gagal menyimpan (kode %d).\n", ss);
printf("Terima kasih.\n");
return 0;
}
storage.c
Unduh berkas/* storage.c -- persistensi tugas ke file teks. */
#include "storage.h"
#include <stdio.h>
#include <string.h>
static void potong_newline(char *s) {
size_t n = strlen(s);
while (n > 0 && (s[n - 1] == '\n' || s[n - 1] == '\r')) {
s[--n] = '\0';
}
}
StorageStatus storage_simpan(const TaskList *list, const char *path) {
if (list == 0 || path == 0) return STORAGE_ERR_BUKA_FILE;
FILE *f = fopen(path, "w");
if (f == 0) return STORAGE_ERR_BUKA_FILE;
fprintf(f, "%d\n", list->jumlah);
for (int i = 0; i < list->jumlah; i++) {
const Task *t = &list->data[i];
/* Ganti | di judul/deskripsi dengan spasi agar parsing tetap aman. */
fprintf(f, "%d|%s|%s|%d|%d\n",
t->id, t->judul, t->deskripsi, (int)t->status, (int)t->prioritas);
}
if (fclose(f) != 0) return STORAGE_ERR_TULIS;
return STORAGE_OK;
}
StorageStatus storage_muat(TaskList *list, const char *path) {
if (list == 0 || path == 0) return STORAGE_ERR_BUKA_FILE;
FILE *f = fopen(path, "r");
if (f == 0) return STORAGE_ERR_BUKA_FILE;
int jumlah = 0;
if (fscanf(f, "%d\n", &jumlah) != 1) {
fclose(f);
return STORAGE_ERR_FORMAT;
}
if (jumlah < 0 || jumlah > TASK_MAKS) {
fclose(f);
return STORAGE_ERR_FORMAT;
}
char baris[512];
int id_terbesar = list->id_berikutnya - 1;
for (int i = 0; i < jumlah; i++) {
if (fgets(baris, sizeof(baris), f) == 0) {
fclose(f);
return STORAGE_ERR_FORMAT;
}
potong_newline(baris);
int id, status, prioritas;
char judul[TASK_JUDUL_MAKS];
char deskripsi[TASK_DESKRIPSI_MAKS];
int parsed = sscanf(baris, "%d|%79[^|]|%199[^|]|%d|%d",
&id, judul, deskripsi, &status, &prioritas);
if (parsed < 5) {
fclose(f);
return STORAGE_ERR_FORMAT;
}
if (list->jumlah >= TASK_MAKS) break;
Task *t = &list->data[list->jumlah];
t->id = id;
strncpy(t->judul, judul, TASK_JUDUL_MAKS - 1);
t->judul[TASK_JUDUL_MAKS - 1] = '\0';
strncpy(t->deskripsi, deskripsi, TASK_DESKRIPSI_MAKS - 1);
t->deskripsi[TASK_DESKRIPSI_MAKS - 1] = '\0';
t->status = (TaskStatus)status;
t->prioritas = (TaskPrioritas)prioritas;
list->jumlah++;
if (id > id_terbesar) id_terbesar = id;
}
list->id_berikutnya = id_terbesar + 1;
fclose(f);
return STORAGE_OK;
}
task.c
Unduh berkas/* task.c -- implementasi model dan operasi tugas. */
#include "task.h"
#include <string.h>
#include <stdio.h>
void task_list_init(TaskList *list) {
if (list == 0) return;
list->jumlah = 0;
list->id_berikutnya = 1;
}
int task_valid_judul(const char *judul) {
if (judul == 0 || judul[0] == '\0') return 0;
if (strlen(judul) >= TASK_JUDUL_MAKS) return 0;
return 1;
}
int task_valid_prioritas(int p) {
return p == TASK_PRIORITAS_RENDAH
|| p == TASK_PRIORITAS_SEDANG
|| p == TASK_PRIORITAS_TINGGI;
}
TaskResult task_tambah(TaskList *list,
const char *judul,
const char *deskripsi,
TaskPrioritas prioritas) {
if (list == 0) return TASK_ERR_INPUT_INVALID;
if (!task_valid_judul(judul)) return TASK_ERR_INPUT_INVALID;
if (!task_valid_prioritas((int)prioritas)) return TASK_ERR_INPUT_INVALID;
if (list->jumlah >= TASK_MAKS) return TASK_ERR_PENUH;
Task *t = &list->data[list->jumlah];
t->id = list->id_berikutnya++;
strncpy(t->judul, judul, TASK_JUDUL_MAKS - 1);
t->judul[TASK_JUDUL_MAKS - 1] = '\0';
strncpy(t->deskripsi, deskripsi ? deskripsi : "", TASK_DESKRIPSI_MAKS - 1);
t->deskripsi[TASK_DESKRIPSI_MAKS - 1] = '\0';
t->status = TASK_STATUS_BELUM;
t->prioritas = prioritas;
list->jumlah++;
return TASK_OK;
}
void task_tampilkan(const TaskList *list, FILE *out) {
if (list == 0 || out == 0) return;
fprintf(out, "%-4s %-30s %-12s %-10s\n",
"ID", "Judul", "Status", "Prioritas");
for (int i = 0; i < list->jumlah; i++) {
const Task *t = &list->data[i];
fprintf(out, "%-4d %-30s %-12s %-10s\n",
t->id, t->judul,
task_status_nama(t->status),
task_prioritas_nama(t->prioritas));
}
fprintf(out, "Total: %d tugas\n", list->jumlah);
}
TaskResult task_set_status(TaskList *list, int id, TaskStatus status) {
if (list == 0) return TASK_ERR_INPUT_INVALID;
if (status < TASK_STATUS_BELUM || status > TASK_STATUS_SELESAI) {
return TASK_ERR_INPUT_INVALID;
}
for (int i = 0; i < list->jumlah; i++) {
if (list->data[i].id == id) {
list->data[i].status = status;
return TASK_OK;
}
}
return TASK_ERR_TIDAK_DITEMUKAN;
}
TaskResult task_hapus(TaskList *list, int id) {
if (list == 0) return TASK_ERR_INPUT_INVALID;
for (int i = 0; i < list->jumlah; i++) {
if (list->data[i].id == id) {
for (int j = i; j < list->jumlah - 1; j++) {
list->data[j] = list->data[j + 1];
}
list->jumlah--;
return TASK_OK;
}
}
return TASK_ERR_TIDAK_DITEMUKAN;
}
int task_filter_status(const TaskList *list, TaskStatus status, FILE *out) {
if (list == 0 || out == 0) return 0;
int count = 0;
fprintf(out, "%-4s %-30s %-12s %-10s\n",
"ID", "Judul", "Status", "Prioritas");
for (int i = 0; i < list->jumlah; i++) {
const Task *t = &list->data[i];
if (t->status == status) {
fprintf(out, "%-4d %-30s %-12s %-10s\n",
t->id, t->judul,
task_status_nama(t->status),
task_prioritas_nama(t->prioritas));
count++;
}
}
fprintf(out, "Ditemukan: %d tugas\n", count);
return count;
}
int task_cari(const TaskList *list, const char *kata_kunci) {
if (list == 0 || kata_kunci == 0) return -1;
for (int i = 0; i < list->jumlah; i++) {
if (strstr(list->data[i].judul, kata_kunci) != 0) {
return i;
}
}
return -1;
}
const char *task_status_nama(TaskStatus s) {
switch (s) {
case TASK_STATUS_BELUM: return "Belum";
case TASK_STATUS_PROSES: return "Proses";
case TASK_STATUS_SELESAI:return "Selesai";
default: return "?";
}
}
const char *task_prioritas_nama(TaskPrioritas p) {
switch (p) {
case TASK_PRIORITAS_RENDAH: return "Rendah";
case TASK_PRIORITAS_SEDANG: return "Sedang";
case TASK_PRIORITAS_TINGGI: return "Tinggi";
default: return "?";
}
}
tests.c
Unduh berkas/* tests.c -- rangkaian test untuk modul task dan storage. */
#include "task.h"
#include "storage.h"
#include <stdio.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 void test_tambah(void) {
TaskList list;
task_list_init(&list);
TaskResult r1 = task_tambah(&list, "Belajar C", "Bab struct", TASK_PRIORITAS_TINGGI);
TaskResult r2 = task_tambah(&list, "Belanja", "Beli susu", TASK_PRIORITAS_SEDANG);
PERIKSA(r1 == TASK_OK && r2 == TASK_OK, "Tambah 2 tugas berhasil");
PERIKSA(list.jumlah == 2, "Jumlah = 2");
PERIKSA(list.data[0].id == 1, "ID pertama = 1");
PERIKSA(list.data[0].status == TASK_STATUS_BELUM, "Status awal = BELUM");
}
static void test_tambah_invalid(void) {
TaskList list;
task_list_init(&list);
PERIKSA(task_tambah(&list, "", "x", TASK_PRIORITAS_SEDANG) == TASK_ERR_INPUT_INVALID,
"Judul kosong ditolak");
PERIKSA(task_tambah(&list, "X", "Y", (TaskPrioritas)99) == TASK_ERR_INPUT_INVALID,
"Prioritas invalid ditolak");
PERIKSA(list.jumlah == 0, "Tidak ada tugas ditambahkan");
}
static void test_set_status(void) {
TaskList list;
task_list_init(&list);
task_tambah(&list, "T1", "D1", TASK_PRIORITAS_SEDANG);
TaskResult r = task_set_status(&list, 1, TASK_STATUS_SELESAI);
PERIKSA(r == TASK_OK, "Set status SELESAI berhasil");
PERIKSA(list.data[0].status == TASK_STATUS_SELESAI, "Status = SELESAI");
PERIKSA(task_set_status(&list, 999, TASK_STATUS_PROSES) == TASK_ERR_TIDAK_DITEMUKAN,
"Set status ID tidak ada -> error");
}
static void test_hapus(void) {
TaskList list;
task_list_init(&list);
task_tambah(&list, "T1", "D1", TASK_PRIORITAS_SEDANG);
task_tambah(&list, "T2", "D2", TASK_PRIORITAS_SEDANG);
task_tambah(&list, "T3", "D3", TASK_PRIORITAS_SEDANG);
TaskResult r = task_hapus(&list, 2);
PERIKSA(r == TASK_OK && list.jumlah == 2, "Hapus id=2 berhasil");
PERIKSA(strcmp(list.data[1].judul, "T3") == 0,
"Setelah hapus, elemen 1 = 'T3'");
}
static void test_filter_status(void) {
TaskList list;
task_list_init(&list);
task_tambah(&list, "T1", "D1", TASK_PRIORITAS_SEDANG); /* BELUM */
task_tambah(&list, "T2", "D2", TASK_PRIORITAS_SEDANG);
task_tambah(&list, "T3", "D3", TASK_PRIORITAS_SEDANG);
task_set_status(&list, 2, TASK_STATUS_PROSES);
task_set_status(&list, 3, TASK_STATUS_SELESAI);
int n = task_filter_status(&list, TASK_STATUS_PROSES, stdout);
PERIKSA(n == 1, "Filter PROSES menemukan 1 tugas");
}
static void test_cari(void) {
TaskList list;
task_list_init(&list);
task_tambah(&list, "Belajar C", "", TASK_PRIORITAS_SEDANG);
task_tambah(&list, "Belajar Python", "", TASK_PRIORITAS_SEDANG);
int idx = task_cari(&list, "Python");
PERIKSA(idx == 1, "Cari 'Python' -> indeks 1");
PERIKSA(task_cari(&list, "Rust") == -1, "Cari 'Rust' -> -1");
}
static void test_penyimpanan(void) {
TaskList list;
task_list_init(&list);
task_tambah(&list, "T1", "D1", TASK_PRIORITAS_TINGGI);
task_tambah(&list, "T2", "D2", TASK_PRIORITAS_RENDAH);
task_set_status(&list, 2, TASK_STATUS_SELESAI);
StorageStatus ss = storage_simpan(&list, "/tmp/test_tugas.txt");
PERIKSA(ss == STORAGE_OK, "Simpan ke /tmp/test_tugas.txt");
TaskList dimuat;
task_list_init(&dimuat);
ss = storage_muat(&dimuat, "/tmp/test_tugas.txt");
PERIKSA(ss == STORAGE_OK, "Muat dari /tmp/test_tugas.txt");
PERIKSA(dimuat.jumlah == 2, "Jumlah dimuat = 2");
PERIKSA(strcmp(dimuat.data[0].judul, "T1") == 0, "Judul dimuat = 'T1'");
PERIKSA(dimuat.data[1].status == TASK_STATUS_SELESAI, "Status dimuat = SELESAI");
PERIKSA(dimuat.id_berikutnya == 3, "id_berikutnya = 3");
}
int main(void) {
test_tambah();
test_tambah_invalid();
test_set_status();
test_hapus();
test_filter_status();
test_cari();
test_penyimpanan();
printf("\nRingkasan: %d lulus, %d gagal\n", lulus, gagal);
return gagal == 0 ? 0 : 1;
}