Zig String Operations

Zig's standard library provides a rich set of string operations through std.mem and std.fmt. Because strings in Zig are plain byte slices, these functions compose cleanly — you pass slices in and get slices or allocations back. This topic covers searching, splitting, trimming, replacing, and formatting strings.

Searching

const std = @import("std");
const text = "The quick brown fox jumps over the lazy dog";

// Find first occurrence of a substring
if (std.mem.indexOf(u8, text, "fox")) |pos| {
    std.debug.print("'fox' at index {d}\n", .{pos});  // 16
}

// Find last occurrence
if (std.mem.lastIndexOf(u8, text, "the")) |pos| {
    std.debug.print("last 'the' at {d}\n", .{pos});  // 31
}

// Check prefix and suffix
std.debug.print("starts with 'The': {}\n",
    .{std.mem.startsWith(u8, text, "The")});   // true
std.debug.print("ends with 'dog': {}\n",
    .{std.mem.endsWith(u8, text, "dog")});     // true

// Count occurrences
const n = std.mem.count(u8, text, "the");
std.debug.print("'the' count: {d}\n", .{n});  // 1 (case-sensitive)

Slicing Substrings

const sentence = "Hello, Zig World!";

// Extract a fixed range
const sub = sentence[7..10];  // "Zig"
std.debug.print("{s}\n", .{sub});

// Extract from a position to end
const tail = sentence[7..];   // "Zig World!"
std.debug.print("{s}\n", .{tail});
  "Hello, Zig World!"
   0123456789...

  [7..10] → index 7='Z', 8='i', 9='g' → "Zig"
  [7..]   → index 7 to end            → "Zig World!"

Splitting Strings

Use an iterator to split by a delimiter without allocating:

const csv = "apple,banana,cherry,date";
var it = std.mem.splitScalar(u8, csv, ',');

while (it.next()) |token| {
    std.debug.print("Fruit: {s}\n", .{token});
}
  "apple,banana,cherry,date"
         |       |      |
  split on ','
  → "apple" → "banana" → "cherry" → "date"

For splitting on a multi-character delimiter, use std.mem.splitSequence:

var it2 = std.mem.splitSequence(u8, "one::two::three", "::");
while (it2.next()) |part| {
    std.debug.print("{s}\n", .{part});
}
// one
// two
// three

Trimming Whitespace

const padded = "   hello world   ";

const trimmed = std.mem.trim(u8, padded, &std.ascii.whitespace);
std.debug.print("'{s}'\n", .{trimmed});  // 'hello world'

// Trim only left side:
const ltrimmed = std.mem.trimLeft(u8, padded, &std.ascii.whitespace);
// Trim only right side:
const rtrimmed = std.mem.trimRight(u8, padded, &std.ascii.whitespace);
  Before: "   hello world   "
            ↑↑↑            ↑↑↑
         whitespace    whitespace

  After trim: "hello world"

Replacing Characters

var buf: [64]u8 = undefined;
const src = "hello-zig-world";

// Replace all '-' with '_'
_ = std.mem.replace(u8, src, "-", "_", &buf);
const result = buf[0 .. src.len];
std.debug.print("{s}\n", .{result});  // hello_zig_world

Converting Case

var lower_buf: [20]u8 = undefined;
var upper_buf: [20]u8 = undefined;

const word = "ZigLang";

for (word, 0..) |c, i| lower_buf[i] = std.ascii.toLower(c);
for (word, 0..) |c, i| upper_buf[i] = std.ascii.toUpper(c);

std.debug.print("Lower: {s}\n", .{lower_buf[0..word.len]});  // ziglang
std.debug.print("Upper: {s}\n", .{upper_buf[0..word.len]});  // ZIGLANG

Checking Character Types

const std_ascii = std.ascii;

std.debug.print("{}\n", .{std_ascii.isAlpha('A')});    // true
std.debug.print("{}\n", .{std_ascii.isDigit('7')});    // true
std.debug.print("{}\n", .{std_ascii.isAlNum('z')});    // true
std.debug.print("{}\n", .{std_ascii.isSpace(' ')});    // true
std.debug.print("{}\n", .{std_ascii.isUpper('A')});    // true
std.debug.print("{}\n", .{std_ascii.isLower('a')});    // true
std.debug.print("{}\n", .{std_ascii.isPunct('!')});    // true

Formatting Numbers into Strings

var buf: [32]u8 = undefined;

// Integer to string
const int_str = try std.fmt.bufPrint(&buf, "{d}", .{12345});
std.debug.print("{s}\n", .{int_str});   // 12345

// Float with 2 decimal places
const flt_str = try std.fmt.bufPrint(&buf, "{d:.2}", .{3.14159});
std.debug.print("{s}\n", .{flt_str});   // 3.14

// Hex
const hex_str = try std.fmt.bufPrint(&buf, "{x}", .{255});
std.debug.print("{s}\n", .{hex_str});   // ff

Parsing Strings into Numbers

const n = try std.fmt.parseInt(i32,  "-42", 10);  // -42
const u = try std.fmt.parseInt(u32,  "100", 10);  // 100
const h = try std.fmt.parseInt(u8,   "FF",  16);  // 255 (hex)
const f = try std.fmt.parseFloat(f64, "3.14");    // 3.14

Joining Strings

const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const alloc = gpa.allocator();

    const parts = [_][]const u8{ "Zig", "is", "awesome" };
    const joined = try std.mem.join(alloc, " ", &parts);
    defer alloc.free(joined);

    std.debug.print("{s}\n", .{joined});  // Zig is awesome
}

Practical Example: CSV Parser

const std = @import("std");

pub fn main() void {
    const data =
        \\Name,Score,Grade
        \\Alice,95,A
        \\Bob,82,B
        \\Carol,91,A
    ;

    var lines = std.mem.splitScalar(u8, data, '\n');
    var first = true;
    while (lines.next()) |line| {
        if (first) { first = false; continue; } // skip header
        var fields = std.mem.splitScalar(u8, line, ',');
        const name  = fields.next() orelse continue;
        const score = fields.next() orelse continue;
        const grade = fields.next() orelse continue;
        std.debug.print("{s} scored {s} ({s})\n", .{name, score, grade});
    }
}

Output:

Alice scored 95 (A)
Bob scored 82 (B)
Carol scored 91 (A)

Leave a Comment

Your email address will not be published. Required fields are marked *