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 →