List Converter — Rust 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 Rust implementation — the same logic the interactive tool runs, in a shareable, citable form.
//! list-converter — convert lists between separators (newline/comma/space/pipe/...).
//!
//! Language: Rust (edition 2021, standard library only)
//! Source: CosmoDev polyglot showcase port of the List Converter tool, ported
//! from cli/list-converter/list-converter.go (the Go CLI twin), which
//! is itself the lock-step mirror of src/lib/list-converter.ts (the
//! canonical TypeScript implementation).
//! License: display source — part of CosmoDev's polyglot tool pages.
//!
//! Design goals:
//! - Pure + deterministic; never panics (the public API returns `String`).
//! - Functionally equivalent to the Go twin: same inputs -> same outputs.
//! - Self-contained: std only (no crates.io dependencies).
//!
//! Pipeline: split on the "from" separator -> (trim each item) -> (drop empties)
//! -> (dedup, optionally case-insensitive, first occurrence wins) ->
//! (stable sort, optionally case-insensitive) -> join with the "to" separator.
//! The `Separator` enum, `Options`, and `convert` mirror the Go types/`Convert`
//! exactly; defaults are From=Newline, To=Comma (the TS `resolveSep` fallbacks).
use std::cmp::Ordering;
/// A built-in list separator. Mirrors the `Separator` enum in the Go twin; the
/// default `Newline` matches the TS `from` default.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Separator {
#[default]
Newline,
Comma,
Space,
Pipe,
Semicolon,
Tab,
}
impl Separator {
/// The literal string this separator splits/joins on. Mirrors Go's
/// `sepString`. Returns `'static str` so joining needs no allocation for
/// the glue itself.
pub fn as_str(self) -> &'static str {
match self {
Separator::Comma => ",",
Separator::Space => " ",
Separator::Pipe => "|",
Separator::Semicolon => ";",
Separator::Tab => "\t",
Separator::Newline => "\n",
}
}
}
/// Options mirror the Go twin's `Options`. `Default` reproduces the TS
/// `resolveSep` fallbacks (from -> newline, to -> comma); every flag defaults
/// to off, so callers can override incrementally with `..Default::default()`.
///
/// `Default` is hand-rolled (not derived) precisely because the two separator
/// fields need DIFFERENT defaults: `from` -> [`Separator::Newline`], `to` ->
/// [`Separator::Comma`]. A derived impl would give both fields
/// [`Separator::Newline`] (the enum's `#[default]`), which would make the
/// common "newline list to comma list" case a no-op.
#[derive(Debug, Clone)]
pub struct Options {
/// Source separator. Defaults to [`Separator::Newline`].
pub from: Separator,
/// Target separator. Defaults to [`Separator::Comma`] (the TS `to` default).
pub to: Separator,
/// `trim()` each split item (runs before `remove_empty`).
pub trim: bool,
/// Drop items equal to `""` (after `trim` if enabled).
pub remove_empty: bool,
/// Keep the first occurrence of each item, dropping later duplicates.
pub unique: bool,
/// Stable-sort the items ascending.
pub sort: bool,
/// When set, `unique`/`sort` compare lowercased keys but keep original
/// casing in the output. Mirrors Go's `CaseInsensitive` flag.
pub case_insensitive: bool,
}
impl Default for Options {
fn default() -> Self {
Options {
from: Separator::Newline,
to: Separator::Comma,
trim: false,
remove_empty: false,
unique: false,
sort: false,
case_insensitive: false,
}
}
}
/// Lowercase a string with the same observable effect as Go's
/// `strings.ToLower` for the ASCII range this tool targets. Rust's
/// `to_lowercase` handles Unicode; Go does too via its case tables, so both
/// agree on the inputs the tool is designed for.
fn lower(s: &str) -> String {
s.to_lowercase()
}
/// Compare two items, honoring `case_insensitive`. Wraps `str::cmp` so the
/// caller gets a total `Ordering`; used as the comparator for stable sort.
fn cmp_items(a: &str, b: &str, case_insensitive: bool) -> Ordering {
if case_insensitive {
lower(a).cmp(&lower(b))
} else {
a.cmp(b)
}
}
/// Convert a list between separators. Never panics; an empty `input` simply
/// yields a one-item list joined back together (mirroring Go's
/// `strings.Split("", sep) == [""]`).
pub fn convert(input: &str, opts: &Options) -> String {
let mut items: Vec<&str> = input.split(opts.from.as_str()).collect();
if opts.trim {
// `trim` needs owned strings to hold the trimmed views, so we widen
// the vec to `String` at this point. Later stages stay owned.
let mut trimmed: Vec<String> = items.iter().map(|s| s.trim().to_owned()).collect();
if opts.remove_empty {
trimmed.retain(|s| !s.is_empty());
}
if opts.unique {
let mut seen = std::collections::HashSet::new();
trimmed.retain(|s| {
let key = if opts.case_insensitive { lower(s) } else { s.clone() };
seen.insert(key)
});
}
if opts.sort {
trimmed.sort_by(|a, b| cmp_items(a, b, opts.case_insensitive));
}
return trimmed.join(opts.to.as_str());
}
// No-trim path: keep borrowing until a stage forces ownership.
if opts.remove_empty {
items.retain(|s| !s.is_empty());
}
if opts.unique {
let mut seen = std::collections::HashSet::new();
items.retain(|s| {
let key = if opts.case_insensitive { lower(s) } else { (*s).to_owned() };
seen.insert(key)
});
}
if opts.sort {
items.sort_by(|a, b| cmp_items(a, b, opts.case_insensitive));
}
items.join(opts.to.as_str())
}
/// Convenience wrapper using default options — the common "newline list to a
/// comma list" case (matches the tool's default From/To).
pub fn convert_default(input: &str) -> String {
convert(input, &Options::default())
}
// ---------- tests (showcase-only; the canonical suite lives in src/lib + cli/) ----------
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn newline_to_comma_default() {
// Default From=Newline, To=Comma — the primary use case.
assert_eq!(convert_default("a\nb\nc"), "a,b,c");
}
#[test]
fn roundtrip_comma_to_newline() {
let opts = Options { from: Separator::Comma, to: Separator::Newline, ..Default::default() };
assert_eq!(convert("a,b,c", &opts), "a\nb\nc");
}
#[test]
fn trim_and_remove_empty() {
// trim runs before remove_empty, so the blank between the two \n
// collapses to "" and is then dropped.
let opts = Options { to: Separator::Comma, trim: true, remove_empty: true, ..Default::default() };
assert_eq!(convert(" a \n\n b \n c", &opts), "a,b,c");
}
#[test]
fn unique_case_insensitive_keeps_first() {
// Keyed by lowercased value; the first-seen casing survives.
let opts = Options { to: Separator::Comma, unique: true, case_insensitive: true, ..Default::default() };
assert_eq!(convert("A\na\nB", &opts), "A,B");
}
#[test]
fn sort_case_insensitive() {
let opts = Options { to: Separator::Comma, sort: true, case_insensitive: true, ..Default::default() };
assert_eq!(convert("banana\nApple\ncherry", &opts), "Apple,banana,cherry");
}
}
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 →