data-structures · intermediate · ~35 min

Final Project: todo-list-cli — add/remove/list

Stable identifiers vs. positional indices; allocation discipline.

Challenge

Build a fixed-capacity to-do list with stable item IDs.

Task

Implement the following API. Each added item gets a fresh, monotonically increasing ID; removing an item never renumbers or reuses IDs. todo_text indexes surviving items by 0-based position (in insertion order).

typedef struct todo todo_t;
todo_t *todo_create(int capacity);
int     todo_add(todo_t *t, const char *text);  /* returns id >= 1, or -1 if full */
int     todo_remove(todo_t *t, int id);         /* returns 1 if removed, 0 if not found */
int     todo_count(todo_t *t);
const char *todo_text(todo_t *t, int index);    /* by 0-based position */
void    todo_destroy(todo_t *t);

Input

  • capacity: maximum number of items (capacity > 0).
  • text: a NUL-terminated string; the list stores its own copy.
  • id: a previously returned item ID.
  • index: a 0-based position among the current items.

Output

  • todo_add: the new item's ID (>= 1), or -1 if the list is full.
  • todo_remove: 1 if an item with that ID was removed, 0 if not found.
  • todo_count: the current number of items.
  • todo_text: a pointer to the item's text at that position, or NULL if out of range.

Example

todo_create(3)
todo_add("buy milk")   ->   1
todo_add("walk dog")   ->   2
todo_add("write code") ->   3
todo_add("nope")       ->   -1   (full)
todo_remove(2)         ->   1
todo_add("yet another")->   4    (new ID, never reuses 2)
todo_text(0)           ->   "buy milk"

Edge cases

  • Adding when full returns -1.
  • Removing a non-existent ID returns 0.
  • todo_text with an out-of-range index returns NULL.

Rules

  • IDs are monotonic and never reused after removal. Copy the text with strdup and free it on remove and destroy.

Why this matters

A CRUD-style to-do list with stable identifiers is the simplest realistic API design exercise. It teaches you the difference between an index (positional) and an ID (stable, never reused).

Input format

capacity > 0; text NUL-terminated; id a returned ID; index 0-based.

Output format

See the API: add returns an ID or -1; remove returns 1/0; text returns a pointer or NULL.

Constraints

Use strdup for text; free on remove and destroy. IDs are never reused.

Starter code

typedef struct todo todo_t;
todo_t *todo_create(int capacity);
int     todo_add(todo_t *t, const char *text);
int     todo_remove(todo_t *t, int id);
int     todo_count(todo_t *t);
const char *todo_text(todo_t *t, int index);
void    todo_destroy(todo_t *t);

Common mistakes

Reusing IDs after delete (breaks every caller that stored the ID). Confusing index with ID. Forgetting to free the strdup'd text on remove and destroy.

Edge cases to handle

Add when full returns -1. Remove non-existent returns 0. Index out of range for todo_text returns NULL.

Complexity

O(n) per op (linear array). A future variant could use a balanced tree for O(log n).

Background lessons

Up next

Solve this exercise in the browser editor — compile and run against the test harness, no setup required.