9. Mini projects: a unit converter and a word-frequency counter
9.1 Mini project — CLI unit converter
Goal: apply everything above (parsing argv, strtod, switch,
snprintf) in one small, real tool.
// unitconv.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
const char *from, *to;
double factor; // multiply "from" units by factor to get "to" units
} Conversion;
static const Conversion TABLE[] = {
{"km", "mi", 0.621371},
{"mi", "km", 1.60934},
{"kg", "lb", 2.20462},
{"lb", "kg", 0.453592},
{"c", "f", 0}, // handled specially below
{"f", "c", 0},
};
#define TABLE_LEN (sizeof(TABLE) / sizeof(TABLE[0]))
int main(int argc, char *argv[]) {
if (argc != 4) {
fprintf(stderr, "usage: %s <value> <from> <to>\n", argv[0]);
return 1;
}
char *end;
double value = strtod(argv[1], &end);
if (*end != '\0') {
fprintf(stderr, "error: '%s' is not a number\n", argv[1]);
return 1;
}
const char *from = argv[2], *to = argv[3];
if (strcmp(from, "c") == 0 && strcmp(to, "f") == 0) {
printf("%.2f\n", value * 9.0 / 5.0 + 32.0);
return 0;
}
if (strcmp(from, "f") == 0 && strcmp(to, "c") == 0) {
printf("%.2f\n", (value - 32.0) * 5.0 / 9.0);
return 0;
}
for (size_t i = 0; i < TABLE_LEN; i++) {
if (strcmp(TABLE[i].from, from) == 0 && strcmp(TABLE[i].to, to) == 0) {
printf("%.4f\n", value * TABLE[i].factor);
return 0;
}
}
fprintf(stderr, "error: no conversion from '%s' to '%s'\n", from, to);
return 1;
}
clang -std=c17 -Wall -Wextra -Werror -o unitconv unitconv.c
./unitconv 10 km mi # 6.2137
./unitconv 100 c f # 212.00
9.2 Mini project — word-frequency counter
Goal: fgets, strtok, a fixed-size hash-free lookup via linear scan
(you'll replace this with a real hash table in Section 18), and dynamic
memory ownership done correctly for the first time.
// wordfreq.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
typedef struct {
char *word;
int count;
} Entry;
#define MAX_WORDS 10000
static Entry entries[MAX_WORDS];
static int num_entries = 0;
static int record(const char *word) {
for (int i = 0; i < num_entries; i++) {
if (strcmp(entries[i].word, word) == 0) {
entries[i].count++;
return 1;
}
}
if (num_entries >= MAX_WORDS) return 1;
entries[num_entries].word = strdup(word); // heap copy — word buffer gets reused/mutated
if (!entries[num_entries].word) return 0;
entries[num_entries].count = 1;
num_entries++;
return 1;
}
static int by_count_desc(const void *a, const void *b) {
const Entry *ea = a, *eb = b;
return (eb->count > ea->count) - (eb->count < ea->count); // negative/zero/positive
}
int main(void) {
char line[1024];
while (fgets(line, sizeof line, stdin)) {
for (char *p = line; *p; p++) *p = (char)tolower((unsigned char)*p);
char *tok = strtok(line, " \t\n.,!?;:\"'()");
while (tok) {
if (!record(tok)) { perror("strdup"); return 1; }
tok = strtok(NULL, " \t\n.,!?;:\"'()");
}
}
qsort(entries, (size_t)num_entries, sizeof(Entry), by_count_desc);
for (int i = 0; i < num_entries && i < 20; i++) {
printf("%5d %s\n", entries[i].count, entries[i].word);
}
for (int i = 0; i < num_entries; i++) free(entries[i].word); // own what you strdup
return 0;
}
clang -std=gnu17 -Wall -Wextra -Werror -o wordfreq wordfreq.c # gnu17: wordfreq.c calls
# strdup(), a POSIX function
# strict c17 hides on Linux/glibc
curl -s https://www.gutenberg.org/files/1342/1342-0.txt | ./wordfreq | head
Note the ownership rule established here and used for the rest of the
guide: whoever calls strdup/malloc is responsible for the matching
free. record() allocates; main()'s final loop frees. This
convention — and what happens when you break it — is the entire subject
of Part II.