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

list points to initialized todo_list_t; text is non-NULL null-terminated string.

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

Memory leak by not freeing duplicated string on removal or failure.

Edge cases to handle

List full, adding duplicate text, removing non-existent id.

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.