Pattern Matching sem Regex em Zig

Introdução

Zig não inclui uma biblioteca de regex na stdlib. Isso é intencional — regex é complexo e difícil de implementar com performance previsível. Em vez disso, Zig oferece funções de busca e manipulação de strings eficientes na std.mem que cobrem a maioria dos casos de uso sem a complexidade de um motor regex.

Para manipulação de strings em geral, veja Buscar Substrings, Split String e Comparar Strings.

Pré-requisitos

Busca Simples

Verificar se Contém Substring

const std = @import("std");

fn contem(haystack: []const u8, needle: []const u8) bool {
    return std.mem.indexOf(u8, haystack, needle) != null;
}

fn comecaCom(texto: []const u8, prefixo: []const u8) bool {
    return std.mem.startsWith(u8, texto, prefixo);
}

fn terminaCom(texto: []const u8, sufixo: []const u8) bool {
    return std.mem.endsWith(u8, texto, sufixo);
}

test "busca simples" {
    try std.testing.expect(contem("Zig Brasil", "Brasil"));
    try std.testing.expect(comecaCom("hello.zig", "hello"));
    try std.testing.expect(terminaCom("hello.zig", ".zig"));
}

Validação de Padrões

Validar Email (simplificado)

fn validarEmail(email: []const u8) bool {
    // Encontrar @
    const arroba_pos = std.mem.indexOf(u8, email, "@") orelse return false;

    // Parte antes do @ não pode ser vazia
    if (arroba_pos == 0) return false;

    // Parte depois do @
    const dominio = email[arroba_pos + 1 ..];
    if (dominio.len == 0) return false;

    // Domínio deve ter pelo menos um ponto
    const ponto_pos = std.mem.indexOf(u8, dominio, ".") orelse return false;

    // Parte antes e depois do ponto não podem ser vazias
    if (ponto_pos == 0) return false;
    if (ponto_pos >= dominio.len - 1) return false;

    // Verificar caracteres válidos
    for (email[0..arroba_pos]) |c| {
        if (!std.ascii.isAlphanumeric(c) and c != '.' and c != '_' and c != '-') {
            return false;
        }
    }

    return true;
}

test "validar email" {
    try std.testing.expect(validarEmail("user@example.com"));
    try std.testing.expect(validarEmail("nome.sobrenome@dominio.com.br"));
    try std.testing.expect(!validarEmail("invalido"));
    try std.testing.expect(!validarEmail("@semlocal.com"));
    try std.testing.expect(!validarEmail("sem@dominio"));
}

Validar Número

fn ehNumero(texto: []const u8) bool {
    if (texto.len == 0) return false;

    var inicio: usize = 0;
    if (texto[0] == '-' or texto[0] == '+') {
        inicio = 1;
        if (texto.len == 1) return false;
    }

    var tem_ponto = false;
    for (texto[inicio..]) |c| {
        if (c == '.' and !tem_ponto) {
            tem_ponto = true;
        } else if (!std.ascii.isDigit(c)) {
            return false;
        }
    }

    return true;
}

test "validar número" {
    try std.testing.expect(ehNumero("42"));
    try std.testing.expect(ehNumero("-3.14"));
    try std.testing.expect(ehNumero("+100"));
    try std.testing.expect(!ehNumero("abc"));
    try std.testing.expect(!ehNumero(""));
    try std.testing.expect(!ehNumero("1.2.3"));
}

Extrair Padrões

Extrair Valores entre Delimitadores

fn extrairEntre(texto: []const u8, inicio: []const u8, fim: []const u8) ?[]const u8 {
    const pos_inicio = std.mem.indexOf(u8, texto, inicio) orelse return null;
    const conteudo_inicio = pos_inicio + inicio.len;

    const pos_fim = std.mem.indexOf(u8, texto[conteudo_inicio..], fim) orelse return null;

    return texto[conteudo_inicio .. conteudo_inicio + pos_fim];
}

fn extrairTodosEntre(
    allocator: std.mem.Allocator,
    texto: []const u8,
    inicio: []const u8,
    fim: []const u8,
) ![][]const u8 {
    var resultados = std.ArrayList([]const u8).init(allocator);
    errdefer resultados.deinit();

    var pos: usize = 0;
    while (pos < texto.len) {
        const pos_inicio = std.mem.indexOf(u8, texto[pos..], inicio) orelse break;
        const abs_inicio = pos + pos_inicio + inicio.len;

        const pos_fim = std.mem.indexOf(u8, texto[abs_inicio..], fim) orelse break;
        const abs_fim = abs_inicio + pos_fim;

        try resultados.append(texto[abs_inicio..abs_fim]);
        pos = abs_fim + fim.len;
    }

    return resultados.toOwnedSlice();
}

test "extrair entre delimitadores" {
    const resultado = extrairEntre("<title>Zig Brasil</title>", "<title>", "</title>");
    try std.testing.expectEqualStrings("Zig Brasil", resultado.?);
}

Extrair Números de uma String

fn extrairNumeros(allocator: std.mem.Allocator, texto: []const u8) ![]i64 {
    var numeros = std.ArrayList(i64).init(allocator);
    errdefer numeros.deinit();

    var inicio: ?usize = null;

    for (texto, 0..) |c, i| {
        if (std.ascii.isDigit(c) or (c == '-' and inicio == null)) {
            if (inicio == null) inicio = i;
        } else {
            if (inicio) |s| {
                const num = std.fmt.parseInt(i64, texto[s..i], 10) catch {
                    inicio = null;
                    continue;
                };
                try numeros.append(num);
                inicio = null;
            }
        }
    }

    // Verificar último número
    if (inicio) |s| {
        if (std.fmt.parseInt(i64, texto[s..], 10)) |num| {
            try numeros.append(num);
        } else |_| {}
    }

    return numeros.toOwnedSlice();
}

test "extrair números" {
    const nums = try extrairNumeros(std.testing.allocator, "Tenho 3 gatos e 2 cachorros");
    defer std.testing.allocator.free(nums);
    try std.testing.expectEqual(@as(usize, 2), nums.len);
    try std.testing.expectEqual(@as(i64, 3), nums[0]);
    try std.testing.expectEqual(@as(i64, 2), nums[1]);
}

Matching com Wildcards

Glob-style Pattern Matching

fn matchGlob(pattern: []const u8, texto: []const u8) bool {
    var pi: usize = 0;
    var ti: usize = 0;
    var star_pi: ?usize = null;
    var star_ti: usize = 0;

    while (ti < texto.len) {
        if (pi < pattern.len and (pattern[pi] == '?' or pattern[pi] == texto[ti])) {
            pi += 1;
            ti += 1;
        } else if (pi < pattern.len and pattern[pi] == '*') {
            star_pi = pi;
            star_ti = ti;
            pi += 1;
        } else if (star_pi) |sp| {
            pi = sp + 1;
            star_ti += 1;
            ti = star_ti;
        } else {
            return false;
        }
    }

    while (pi < pattern.len and pattern[pi] == '*') {
        pi += 1;
    }

    return pi == pattern.len;
}

test "glob matching" {
    try std.testing.expect(matchGlob("*.zig", "main.zig"));
    try std.testing.expect(matchGlob("src/*.zig", "src/lib.zig"));
    try std.testing.expect(matchGlob("test_?", "test_1"));
    try std.testing.expect(!matchGlob("*.zig", "main.c"));
}

Substituição

fn substituir(
    allocator: std.mem.Allocator,
    texto: []const u8,
    buscar: []const u8,
    novo: []const u8,
) ![]u8 {
    var resultado = std.ArrayList(u8).init(allocator);
    errdefer resultado.deinit();

    var pos: usize = 0;
    while (pos < texto.len) {
        if (std.mem.indexOf(u8, texto[pos..], buscar)) |found| {
            try resultado.appendSlice(texto[pos .. pos + found]);
            try resultado.appendSlice(novo);
            pos += found + buscar.len;
        } else {
            try resultado.appendSlice(texto[pos..]);
            break;
        }
    }

    return resultado.toOwnedSlice();
}

test "substituir" {
    const resultado = try substituir(
        std.testing.allocator,
        "Olá Mundo! Olá Zig!",
        "Olá",
        "Oi",
    );
    defer std.testing.allocator.free(resultado);
    try std.testing.expectEqualStrings("Oi Mundo! Oi Zig!", resultado);
}

Conclusão

A maioria dos casos de uso de regex pode ser resolvida com funções de std.mem e lógica de parsing manual em Zig. Para casos que realmente exigem regex (como validação de formatos complexos), considere usar uma biblioteca C de regex via @cImport.

Para mais manipulação de strings, veja Formatar Strings e Converter String para Número. Para testes dessas funções, consulte Testes Unitários.

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.