Skip to content

Sort Lines & Remove Duplicates — C source

Alphabetize, reverse, shuffle, dedupe, or length-sort lines of text. Supports case-insensitive and natural sorting (file2 before file10).

This is the C implementation — the same logic the interactive tool runs, in a shareable, citable form.

/* sort-lines — multi-mode line sorter. Language: C (C11). Port of src/lib/sortLines.ts — same contract as this dir's go.go (the live Go twin): split on '\n', apply the mode (asc/desc/length-asc/length-desc/reverse/shuffle/unique), join back. Insertion sort is stable, so ties keep input order like the TS sort; shuffle uses mulberry32, so a seed reproduces the TS order. */
#include <ctype.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

enum mode { ASC, DESC, LEN_ASC, LEN_DESC, REVERSE, SHUFFLE, UNIQUE };
struct opts { int case_sensitive, trim, natural, seed; }; /* TS defaults: 1, 0, 0, 1 */
struct result { char **lines; size_t n; int removed; char *text; };

/* mulberry32 — deterministic PRNG (not cryptographic); a seed reproduces the same shuffle. */
static double rng_next(uint32_t *a)
{
    *a += 0x6d2b79f5u;
    uint32_t t = (*a ^ (*a >> 15)) * (1u | *a);
    t = (t + (t ^ (t >> 7)) * (61u | t)) ^ t;
    return (double)(t ^ (t >> 14)) / 4294967296.0;
}

static int isdig(char c) { return c >= '0' && c <= '9'; }

/* Plain order: byte compare, case-folded unless case_sensitive — localeCompare for ASCII. */
static int plain_cmp(const char *a, const char *b, int cs)
{
    while (*a && *b) {
        int x = cs ? (unsigned char)*a : tolower((unsigned char)*a);
        int y = cs ? (unsigned char)*b : tolower((unsigned char)*b);
        if (x != y) return x < y ? -1 : 1;
        a++, b++;
    }
    return *a ? 1 : *b ? -1 : 0;
}

/* Natural order: walk both strings in ASCII digit / non-digit runs, comparing chunk-wise so numbers order by value — "file2" sorts before "file10". Digit runs compare by numeric value: strip leading zeros, longer run wins, then lex (an overflow-free parseInt diff). */
static int natural_cmp(const char *a, const char *b, int cs)
{
    while (*a || *b) {
        int da = isdig(*a), db = isdig(*b);
        const char *pa = a, *pb = b;
        while (da ? isdig(*a) : *a && !isdig(*a)) a++;
        while (db ? isdig(*b) : *b && !isdig(*b)) b++;
        size_t la = (size_t)(a - pa), lb = (size_t)(b - pb);
        if (da != db) { /* digit run vs text run: raw byte compare, shorter prefix first */
            int c = strncmp(pa, pb, la < lb ? la : lb);
            return c ? (c < 0 ? -1 : 1) : (la < lb ? -1 : la > lb ? 1 : 0);
        }
        if (da) {
            while (la && *pa == '0') pa++, la--;
            while (lb && *pb == '0') pb++, lb--;
            if (la != lb) return la < lb ? -1 : 1;
            int c = la ? strncmp(pa, pb, la) : 0;
            if (c) return c < 0 ? -1 : 1;
        } else {
            for (size_t i = 0; i < la && i < lb; i++) {
                int x = cs ? (unsigned char)pa[i] : tolower((unsigned char)pa[i]);
                int y = cs ? (unsigned char)pb[i] : tolower((unsigned char)pb[i]);
                if (x != y) return x < y ? -1 : 1;
            }
            if (la != lb) return la < lb ? -1 : 1;
        }
    }
    return 0;
}

static struct opts O; /* options + direction for the comparators (single-threaded demo) */
static int Dir;

static int cmp_text(const char *x, const char *y)
{
    int c = O.natural ? natural_cmp(x, y, O.case_sensitive) : plain_cmp(x, y, O.case_sensitive);
    return c * Dir;
}

static int cmp_len(const char *x, const char *y)
{
    size_t lx = strlen(x), ly = strlen(y);
    return lx < ly ? -1 : lx > ly ? 1 : 0;
}

/* Stable insertion sort — ties keep input order, like the TS (stable) sort. */
static void sort_stable(char **v, size_t n, int (*cmp)(const char *, const char *))
{
    for (size_t i = 1; i < n; i++) {
        char *key = v[i];
        size_t j = i;
        while (j > 0 && cmp(v[j - 1], key) > 0) { v[j] = v[j - 1]; j--; }
        v[j] = key;
    }
}

static void reverse_lines(char **v, size_t n)
{
    for (size_t i = 0; i < n / 2; i++) { char *t = v[i]; v[i] = v[n - 1 - i]; v[n - 1 - i] = t; }
}

static struct result sort_lines(const char *input, enum mode m, struct opts o)
{
    struct result r = { NULL, 0, 0, NULL };
    O = o, Dir = m == DESC ? -1 : 1;
    /* split on '\n' — the trailing empty line is kept, like JS split("\n") */
    size_t n = 1;
    for (const char *p = input; *p; p++) n += *p == '\n';
    r.lines = malloc(n * sizeof *r.lines), r.n = n;
    size_t i = 0;
    const char *s = input;
    for (;;) {
        const char *nl = strchr(s, '\n');
        size_t len = nl ? (size_t)(nl - s) : strlen(s);
        char *line = malloc(len + 1);
        memcpy(line, s, len), line[len] = '\0';
        if (o.trim) { /* trim spaces / tabs / CR both ends */
            size_t b = 0, e = len;
            while (b < e && (line[b] == ' ' || line[b] == '\t' || line[b] == '\r')) b++;
            while (e > b && (line[e - 1] == ' ' || line[e - 1] == '\t' || line[e - 1] == '\r')) e--;
            memmove(line, line + b, e - b), line[e - b] = '\0';
        }
        r.lines[i++] = line;
        if (!nl) break;
        s = nl + 1;
    }

    if (m == UNIQUE) { /* keep each normalized line's first occurrence; count the rest */
        char **out = malloc(n * sizeof *out);
        size_t no = 0;
        for (size_t k = 0; k < n; k++) {
            size_t t = 0;
            while (t < no && plain_cmp(r.lines[k], out[t], o.case_sensitive) != 0) t++;
            if (t < no) r.removed++;
            else out[no++] = r.lines[k];
        }
        free(r.lines), r.lines = out, r.n = no;
    } else if (m == SHUFFLE) { /* Fisher-Yates with the seeded PRNG -> reproducible order */
        uint32_t a = (uint32_t)o.seed;
        for (size_t k = n; k-- > 1; ) {
            size_t j = (size_t)(rng_next(&a) * (double)(k + 1));
            char *t = r.lines[k]; r.lines[k] = r.lines[j], r.lines[j] = t;
        }
    } else if (m == REVERSE) {
        reverse_lines(r.lines, r.n);
    } else if (m == LEN_ASC || m == LEN_DESC) { /* stable by length, then reverse for desc */
        sort_stable(r.lines, r.n, cmp_len);
        if (m == LEN_DESC) reverse_lines(r.lines, r.n);
    } else { /* ASC / DESC — stable, ties keep input order in both directions */
        sort_stable(r.lines, r.n, cmp_text);
    }

    size_t total = 1; /* join back with '\n' */
    for (size_t k = 0; k < r.n; k++) total += strlen(r.lines[k]) + 1;
    r.text = malloc(total);
    char *w = r.text;
    for (size_t k = 0; k < r.n; k++) {
        size_t l = strlen(r.lines[k]);
        memcpy(w, r.lines[k], l), w += l;
        if (k + 1 < r.n) *w++ = '\n';
    }
    *w = '\0';
    return r;
}

int main(void)
{
    const char *text = "pear\napple\nBanana\napple\nfig10\nfig2";
    struct opts def = { 1, 0, 0, 1 }, ci = { 0, 0, 0, 1 }, sh = { 1, 0, 0, 7 }, nat = { 0, 0, 1, 1 };
    struct result r = sort_lines(text, ASC, def);
    printf("asc:    %s\n", r.text);
    r = sort_lines(text, ASC, ci);
    printf("ci-asc: %s\n", r.text);
    r = sort_lines(text, UNIQUE, ci);
    printf("uniq:   %s  (removed %d)\n", r.text, r.removed);
    r = sort_lines(text, SHUFFLE, sh);
    printf("shuf-7: %s\n", r.text);
    r = sort_lines(text, ASC, nat);
    printf("nat-ci: %s\n", r.text);
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →