str

tape
import str from "str";

let parts: [3][]const u8 = ["usr", "local", "bin"];
let p: string = str.join(parts[0..3], "/");          // "usr/local/bin"
let title: string = str.to_upper("tape");           // "TAPE"
let row: string = str.pad_right("name", 8, 32);      // "name    "

str makes new strings. Everything that only looks at text (searching, comparing, trimming, splitting) is in bytes and works on views without copying; str is for when you need an owned string as the result.

Every text parameter is a []const u8, so you can pass a string, a literal or a slice of either (see Strings as byte views). Every string a function returns is new and yours: it’s freed automatically when it goes out of scope.

str is a pure module: it allocates, but has no other effect.

Builder

A Builder collects text piece by piece and turns it into a string at the end. Appending is amortized constant time, so it’s the way to build long text in a loop.

FunctionDoes
str.builder() -> BuilderAn empty builder (allocates on the first append)
str.with_capacity(cap: i64) -> BuilderAn empty builder with room for cap bytes
b.append(s: []const u8)Appends the bytes of s
b.append_byte(b: u8)Appends one byte
b.append_char(cp: u32)Appends a code point as UTF-8 (U+FFFD if it isn’t a valid one)
b.append_i64(v: i64)Appends v in decimal, as "{v}" writes it
b.len() -> i64Bytes appended so far
b.as_view() -> []const u8The bytes so far, as a view
b.clear()Empties the builder, keeping its memory
b.truncate(n: i64)Keeps only the first n bytes (traps if n is negative or past the end)
b.finish() -> stringThe contents as a new string; the builder is emptied
b.free()Releases the builder’s memory without making a string
tape
fn csv_line(values: []const i64) -> string {
    var b: str.Builder = str.builder();
    for (i in 0..values.len) {
        if (i > 0) { b.append(","); }
        b.append_i64(values[i]);
    }
    return b.finish();
}

A builder holds memory until finish() or free(). If a function can return before finishing, free it with defer; freeing a builder that was already finished does nothing:

tape
fn render(items: []const []const u8) -> string or RenderError {
    var b: str.Builder = str.builder();
    defer b.free();
    for (item in items) {
        if (item.len == 0) { return RenderError.EmptyItem; }   // b is freed
        b.append(item);
        b.append_byte(10);
    }
    return b.finish();
}

The view from as_view() points into the builder’s memory: use it before the next append, clear, finish or free.

Building strings

FunctionReturns
from_view(s) -> stringA copy of s as an owned string
repeat(s, n: i64) -> strings repeated n times
join(parts: []const []const u8, sep) -> stringThe parts with sep between them
replace(s, needle, replacement) -> strings with every needle replaced
to_upper(s) -> strings with ASCII a-z made upper case
to_lower(s) -> strings with ASCII A-Z made lower case
tape
let line: string = str.repeat("-", 20);                    // 20 dashes
let words: [3][]const u8 = ["one", "two", "three"];
let csv: string = str.join(words[0..3], ",");              // "one,two,three"
let path: string = str.replace("a\\b\\c", "\\", "/");      // "a/b/c"
let lower: string = str.to_lower("Content-Type");          // "content-type"

replace replaces non-overlapping occurrences from left to right: str.replace("aaa", "aa", "b") is "ba". to_upper and to_lower change only ASCII letters; other bytes, including UTF-8 text, are copied unchanged.

repeat stops the program with a trap when n is negative, and replace when needle is empty.

Padding

FunctionReturns
pad_left(s, width: i64, fill: u8) -> strings right-aligned: fill added before it
pad_right(s, width: i64, fill: u8) -> strings left-aligned: fill added after it
center(s, width: i64, fill: u8) -> strings centered, the extra fill byte after it

The width counts characters, not bytes: "ä" is one character. A string that is already width characters or wider comes back unchanged (never cut). fill must be an ASCII byte (below 128); anything else traps.

tape
let id: string = str.pad_left("42", 5, 48);        // "00042"
let cell: string = str.pad_right("tape", 8, 32);   // "tape    "
let title: string = str.center("menu", 10, 61);    // "===menu==="
let wide: string = str.pad_left("toolong", 3, 32); // "toolong"

Searching and splitting

For convenience, str repeats the most used view functions from bytes. They are the same functions: no copies, results are views into the input.

FunctionReturns
eq(a, b) -> booltrue when a and b have the same bytes
contains(haystack, needle) -> booltrue when needle occurs
starts_with(s, prefix) -> booltrue when s begins with prefix
ends_with(s, suffix) -> booltrue when s ends with suffix
index_of(haystack, needle) -> ?i64Offset of the first needle, or null
trim(s) -> []const u8s without leading and trailing whitespace
split(s, sep) -> bytes.SplitIterThe parts between separators, for for
lines(s) -> bytes.LineIterThe lines of s, for for
tape
for (field in str.split("a, b, c", ",")) {
    let f: []const u8 = str.trim(field);   // "a", "b", "c"
}

Anything else (last_index_of, cut, trim_left, count, …) is in bytes.

C strings and the old API

str before 2.1 worked on NUL-terminated *u8 pointers with separate u32 lengths. Those functions moved to cstr unchanged and are deprecated there; cstr lists the replacement for each.

Last modified: