Skip to content

List Converter — Zig source

Transform a list between separators (newline, comma, space, pipe, semicolon, tab) with trim, dedupe, sort, and empty-removal options. Runs entirely in your browser, with a shareable link.

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

//! List Converter — split/trim/dedup/sort/join between separators — Zig port of the list-converter tool.

const std = @import("std");

/// Separators are all single characters: resolveSep maps the TS-style name
/// to its glue byte; an unknown name passes through as a literal separator.
fn resolveSep(name: []const u8) u8 {
    const names = [_][]const u8{ "newline", "comma", "space", "pipe", "semicolon", "tab" };
    const chars = [_]u8{ '\n', ',', ' ', '|', ';', '\t' };
    for (names, chars) |n, c| {
        if (std.mem.eql(u8, name, n)) return c;
    }
    return name[0];
}

/// Options mirror the TS ListOptions; defaults match (from=newline, to=comma).
pub const Options = struct {
    trim: bool = false,
    remove_empty: bool = false,
    unique: bool = false,
    sort: bool = false,
    case_insensitive: bool = false,
};

/// The unique/sort comparison key: byte order, optionally ASCII-folded.
/// Equality (unique) is derived by trichotomy: !less(a,b) and !less(b,a).
fn keyLess(a: []const u8, b: []const u8, ci: bool) bool {
    var i: usize = 0;
    while (i < a.len and i < b.len) : (i += 1) {
        const ca = if (ci) std.ascii.toLower(a[i]) else a[i];
        const cb = if (ci) std.ascii.toLower(b[i]) else b[i];
        if (ca != cb) return ca < cb;
    }
    return a.len < b.len;
}

/// Convert a list between separators: split -> (trim) -> (drop empties) ->
/// (dedup, first occurrence wins) -> (stable sort) -> join. Mirrors the Go
/// twin; the returned slice is owned by `alloc`.
pub fn convert(alloc: std.mem.Allocator, input: []const u8, from: []const u8, to: []const u8, o: Options) ![]u8 {
    const fs = resolveSep(from);
    const ts = resolveSep(to);

    const buf = try alloc.dupe(u8, input);
    defer alloc.free(buf); // item slices point into buf; out is built below
    var items = std.ArrayList([]const u8).init(alloc);
    defer items.deinit();
    {
        var start: usize = 0; // split in place: the sep byte ends each item
        var i: usize = 0;
        while (i <= buf.len) : (i += 1) {
            const at_end = i == buf.len;
            if (!at_end and buf[i] != fs) continue;
            var s = buf[start..i];
            if (o.trim) s = std.mem.trim(u8, s, " \t\r\n");
            if (!(o.remove_empty and s.len == 0)) try items.append(s);
            start = i + 1;
        }
    }
    if (o.unique) { // keep the first occurrence of each (optionally folded) key
        var m: usize = 0;
        for (items.items) |s| {
            var dup = false;
            for (items.items[0..m]) |k| {
                if (!keyLess(s, k, o.case_insensitive) and !keyLess(k, s, o.case_insensitive)) {
                    dup = true;
                    break;
                }
            }
            if (!dup) {
                items.items[m] = s;
                m += 1;
            }
        }
        items.shrinkRetainingCapacity(m);
    }
    if (o.sort) { // insertion sort: stable, plenty for list-sized inputs
        const slice = items.items;
        var i: usize = 1;
        while (i < slice.len) : (i += 1) {
            const key = slice[i];
            var j = i;
            while (j > 0 and keyLess(key, slice[j - 1], o.case_insensitive)) : (j -= 1) {
                slice[j] = slice[j - 1];
            }
            slice[j] = key;
        }
    }

    var total: usize = items.items.len; // room for one joiner per gap
    for (items.items) |s| total += s.len;
    const out = try alloc.alloc(u8, total);
    var w: usize = 0;
    for (items.items, 0..) |s, idx| {
        if (idx != 0) {
            out[w] = ts;
            w += 1;
        }
        @memcpy(out[w..][0..s.len], s);
        w += s.len;
    }
    return out;
}

pub fn main() !void {
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit();
    // "b\n a \nB\na\n\nc" -> trim, drop empties, case-insensitive dedup+sort
    const out = try convert(arena.allocator(), "b\n a \nB\na\n\nc", "newline", "comma", .{
        .trim = true,
        .remove_empty = true,
        .unique = true,
        .sort = true,
        .case_insensitive = true,
    });
    std.debug.print("{s}\n", .{out}); // a,b,c
}

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 →