Verificador de Palíndromo em Zig — Tutorial Passo a Passo

Verificador de Palíndromo em Zig — Tutorial Passo a Passo

Neste tutorial, vamos construir um verificador de palíndromos robusto no terminal. Um palíndromo é uma palavra ou frase que se lê da mesma forma da esquerda para a direita e vice-versa (ex: “arara”, “ovo”, “socorram-me subi no onibus em marrocos”). Este projeto explora manipulação de strings, normalização de texto e algoritmos de comparação em Zig.

O Que Vamos Construir

Nosso verificador vai:

  • Verificar se palavras, frases e números são palíndromos
  • Ignorar espaços, pontuação e diferenças maiúscula/minúscula
  • Lidar com caracteres acentuados do português (á, é, ã, ç, etc.)
  • Encontrar o maior subpalíndromo em um texto
  • Gerar palíndromos numéricos em um intervalo

Por Que Este Projeto?

Verificar palíndromos parece trivial, mas se torna interessante quando precisamos lidar com texto real em português. Acentos, espaços e pontuação precisam ser normalizados antes da comparação. Este é um ótimo exercício de processamento de texto em Zig, onde trabalhamos com bytes diretamente.

Pré-requisitos

Passo 1: Estrutura do Projeto

mkdir verificador-palindromo
cd verificador-palindromo
zig init

Passo 2: Normalização de Texto

O maior desafio de palíndromos em português é a normalização. “Socorram-me” deve ser tratado como “socorramme”.

const std = @import("std");
const io = std.io;
const fmt = std.fmt;
const mem = std.mem;

/// Verifica se um byte ASCII é uma letra (a-z, A-Z).
fn ehLetra(c: u8) bool {
    return (c >= 'a' and c <= 'z') or (c >= 'A' and c <= 'Z');
}

/// Verifica se um byte é um dígito (0-9).
fn ehDigito(c: u8) bool {
    return c >= '0' and c <= '9';
}

/// Converte uma letra maiúscula para minúscula.
fn paraMinuscula(c: u8) u8 {
    if (c >= 'A' and c <= 'Z') return c + 32;
    return c;
}

/// Normaliza texto para comparação de palíndromos.
/// Remove espaços, pontuação e converte para minúsculas.
/// Também trata caracteres UTF-8 acentuados comuns do português,
/// convertendo-os para seus equivalentes sem acento.
///
/// Retorna o texto normalizado no buffer fornecido.
fn normalizar(texto: []const u8, buf: []u8) []const u8 {
    var pos: usize = 0;
    var i: usize = 0;

    while (i < texto.len and pos < buf.len) {
        const c = texto[i];

        // Caracteres ASCII normais
        if (ehLetra(c)) {
            buf[pos] = paraMinuscula(c);
            pos += 1;
            i += 1;
        } else if (ehDigito(c)) {
            buf[pos] = c;
            pos += 1;
            i += 1;
        } else if (c == 0xC3 and i + 1 < texto.len) {
            // Sequências UTF-8 de 2 bytes para acentos latinos
            // 0xC3 seguido do segundo byte indica acentuação
            const next = texto[i + 1];
            const mapeado: ?u8 = switch (next) {
                0xA0, 0xA1, 0xA2, 0xA3, 0xA4 => 'a', // à á â ã ä
                0x80, 0x81, 0x82, 0x83, 0x84 => 'a', // À Á Â Ã Ä
                0xA8, 0xA9, 0xAA, 0xAB => 'e',       // è é ê ë
                0x88, 0x89, 0x8A, 0x8B => 'e',       // È É Ê Ë
                0xAC, 0xAD, 0xAE, 0xAF => 'i',       // ì í î ï
                0x8C, 0x8D, 0x8E, 0x8F => 'i',       // Ì Í Î Ï
                0xB2, 0xB3, 0xB4, 0xB5, 0xB6 => 'o', // ò ó ô õ ö
                0x92, 0x93, 0x94, 0x95, 0x96 => 'o', // Ò Ó Ô Õ Ö
                0xB9, 0xBA, 0xBB, 0xBC => 'u',       // ù ú û ü
                0x99, 0x9A, 0x9B, 0x9C => 'u',       // Ù Ú Û Ü
                0xA7 => 'c',                           // ç
                0x87 => 'c',                           // Ç
                else => null,
            };
            if (mapeado) |m| {
                buf[pos] = m;
                pos += 1;
            }
            i += 2;
        } else {
            // Pula caracteres não reconhecidos (espaços, pontuação, etc.)
            i += 1;
        }
    }

    return buf[0..pos];
}

Por que tratar UTF-8 manualmente? Zig não tem normalização Unicode na stdlib. Para nosso caso limitado (português), um mapeamento explícito dos acentos mais comuns é suficiente e não requer dependências externas. Para internacionalização completa, usaríamos uma biblioteca Unicode.

Passo 3: Verificação de Palíndromo

/// Resultado da verificação de palíndromo.
const ResultadoPalindromo = struct {
    eh_palindromo: bool,
    texto_original: []const u8,
    texto_normalizado: [512]u8,
    texto_normalizado_len: usize,

    pub fn normalizado(self: *const ResultadoPalindromo) []const u8 {
        return self.texto_normalizado[0..self.texto_normalizado_len];
    }
};

/// Verifica se um texto (já normalizado) é palíndromo.
/// Usa a abordagem de dois ponteiros — um do início, outro do fim.
/// Complexidade O(n/2) em tempo, O(1) em espaço adicional.
fn ehPalindromo(texto: []const u8) bool {
    if (texto.len <= 1) return true;

    var esquerda: usize = 0;
    var direita: usize = texto.len - 1;

    while (esquerda < direita) {
        if (texto[esquerda] != texto[direita]) return false;
        esquerda += 1;
        direita -= 1;
    }

    return true;
}

/// Verifica um texto completo, normalizando antes.
fn verificar(texto: []const u8) ResultadoPalindromo {
    var resultado = ResultadoPalindromo{
        .eh_palindromo = false,
        .texto_original = texto,
        .texto_normalizado = undefined,
        .texto_normalizado_len = 0,
    };

    const norm = normalizar(texto, &resultado.texto_normalizado);
    resultado.texto_normalizado_len = norm.len;
    resultado.eh_palindromo = ehPalindromo(norm);

    return resultado;
}

Passo 4: Maior Subpalíndromo

Encontrar o maior subpalíndromo é um problema clássico de programação. Usamos a abordagem de expansão a partir do centro.

/// Encontra o maior subpalíndromo em um texto normalizado.
/// Usa a técnica de expandir a partir de cada posição central.
/// Complexidade: O(n²) no pior caso.
fn maiorSubpalindromo(texto: []const u8) struct { inicio: usize, fim: usize } {
    if (texto.len <= 1) return .{ .inicio = 0, .fim = texto.len };

    var melhor_inicio: usize = 0;
    var melhor_len: usize = 1;

    // Tenta cada posição como centro
    var centro: usize = 0;
    while (centro < texto.len) : (centro += 1) {
        // Palíndromos de comprimento ímpar (centro em um caractere)
        var esq: usize = centro;
        var dir: usize = centro;

        while (esq > 0 and dir < texto.len - 1 and texto[esq - 1] == texto[dir + 1]) {
            esq -= 1;
            dir += 1;
        }

        const len = dir - esq + 1;
        if (len > melhor_len) {
            melhor_inicio = esq;
            melhor_len = len;
        }

        // Palíndromos de comprimento par (centro entre dois caracteres)
        if (centro + 1 < texto.len and texto[centro] == texto[centro + 1]) {
            esq = centro;
            dir = centro + 1;

            while (esq > 0 and dir < texto.len - 1 and texto[esq - 1] == texto[dir + 1]) {
                esq -= 1;
                dir += 1;
            }

            const len2 = dir - esq + 1;
            if (len2 > melhor_len) {
                melhor_inicio = esq;
                melhor_len = len2;
            }
        }
    }

    return .{ .inicio = melhor_inicio, .fim = melhor_inicio + melhor_len };
}

Passo 5: Palíndromos Numéricos

/// Verifica se um número é palíndromo.
fn ehPalindromoNumerico(n: u64) bool {
    var buf: [20]u8 = undefined;
    const texto = fmt.bufPrint(&buf, "{d}", .{n}) catch return false;
    return ehPalindromo(texto);
}

/// Encontra palíndromos numéricos em um intervalo.
fn encontrarPalindromosNumericos(
    inicio: u64,
    fim: u64,
    resultados: []u64,
) usize {
    var count: usize = 0;
    var n = inicio;

    while (n <= fim and count < resultados.len) : (n += 1) {
        if (ehPalindromoNumerico(n)) {
            resultados[count] = n;
            count += 1;
        }
    }

    return count;
}

Passo 6: Interface CLI

/// Exibe o resultado formatado.
fn exibirResultado(resultado: ResultadoPalindromo, writer: anytype) !void {
    const reset = "\x1b[0m";

    try writer.print("\n  Texto original:    \"{s}\"\n", .{resultado.texto_original});
    try writer.print("  Texto normalizado: \"{s}\"\n", .{resultado.normalizado()});

    if (resultado.eh_palindromo) {
        try writer.print("  Resultado: \x1b[32mE palindromo!\x1b[0m\n", .{});

        // Mostrar a leitura invertida
        const norm = resultado.normalizado();
        try writer.print("  Invertido: \"", .{});
        var i: usize = norm.len;
        while (i > 0) {
            i -= 1;
            try writer.print("{c}", .{norm[i]});
        }
        try writer.print("\"\n", .{});
    } else {
        try writer.print("  Resultado: \x1b[31mNao e palindromo.{s}\n", .{reset});
    }
}

/// Exemplos famosos de palíndromos em português.
const exemplos_palindromos = [_][]const u8{
    "arara",
    "ovo",
    "reviver",
    "radar",
    "anilina",
    "Socorram-me, subi no onibus em Marrocos",
    "A base do teto desaba",
    "Roma me tem amor",
    "Anotaram a data da maratona",
};

pub fn main() !void {
    const stdout = io.getStdOut().writer();
    const stdin = io.getStdIn().reader();

    try stdout.print(
        \\
        \\  ==========================================
        \\    VERIFICADOR DE PALINDROMOS - Zig
        \\  ==========================================
        \\
    , .{});

    var buf: [512]u8 = undefined;

    while (true) {
        try stdout.print(
            \\
            \\  [1] Verificar texto
            \\  [2] Encontrar maior subpalindromo
            \\  [3] Palindromos numericos
            \\  [4] Ver exemplos famosos
            \\  [5] Sair
            \\
            \\  Opcao:
        , .{});

        const opcao_raw = stdin.readUntilDelimiterOrEof(&buf, '\n') catch continue orelse break;
        const opcao = mem.trim(u8, opcao_raw, " \t\r\n");

        if (mem.eql(u8, opcao, "5")) break;

        if (mem.eql(u8, opcao, "1")) {
            try stdout.print("\n  Digite o texto: ", .{});
            const texto_raw = stdin.readUntilDelimiterOrEof(&buf, '\n') catch continue orelse continue;
            const texto = mem.trim(u8, texto_raw, " \t\r\n");

            if (texto.len == 0) {
                try stdout.print("  Texto vazio.\n", .{});
                continue;
            }

            const resultado = verificar(texto);
            try exibirResultado(resultado, stdout);
        } else if (mem.eql(u8, opcao, "2")) {
            try stdout.print("\n  Digite o texto: ", .{});
            const texto_raw = stdin.readUntilDelimiterOrEof(&buf, '\n') catch continue orelse continue;
            const texto = mem.trim(u8, texto_raw, " \t\r\n");

            var norm_buf: [512]u8 = undefined;
            const norm = normalizar(texto, &norm_buf);

            const sub = maiorSubpalindromo(norm);
            try stdout.print("\n  Texto normalizado: \"{s}\"\n", .{norm});
            try stdout.print("  Maior subpalindromo: \"{s}\" (tamanho: {d})\n", .{
                norm[sub.inicio..sub.fim], sub.fim - sub.inicio,
            });
        } else if (mem.eql(u8, opcao, "3")) {
            try stdout.print("\n  Inicio do intervalo: ", .{});
            const ini_raw = stdin.readUntilDelimiterOrEof(&buf, '\n') catch continue orelse continue;
            const inicio = fmt.parseInt(u64, mem.trim(u8, ini_raw, " \t\r\n"), 10) catch {
                try stdout.print("  Numero invalido.\n", .{});
                continue;
            };

            try stdout.print("  Fim do intervalo: ", .{});
            const fim_raw = stdin.readUntilDelimiterOrEof(&buf, '\n') catch continue orelse continue;
            const fim = fmt.parseInt(u64, mem.trim(u8, fim_raw, " \t\r\n"), 10) catch {
                try stdout.print("  Numero invalido.\n", .{});
                continue;
            };

            if (fim < inicio or fim - inicio > 10000) {
                try stdout.print("  Intervalo invalido (max 10000).\n", .{});
                continue;
            }

            var resultados: [200]u64 = undefined;
            const count = encontrarPalindromosNumericos(inicio, fim, &resultados);

            try stdout.print("\n  Palindromos numericos entre {d} e {d}:\n  ", .{ inicio, fim });
            var i: usize = 0;
            while (i < count) : (i += 1) {
                try stdout.print("{d} ", .{resultados[i]});
                if ((i + 1) % 10 == 0) try stdout.print("\n  ", .{});
            }
            try stdout.print("\n  Total: {d}\n", .{count});
        } else if (mem.eql(u8, opcao, "4")) {
            try stdout.print("\n  Palindromos famosos em portugues:\n", .{});
            for (exemplos_palindromos) |exemplo| {
                const resultado = verificar(exemplo);
                const simbolo: []const u8 = if (resultado.eh_palindromo) "[OK]" else "[--]";
                try stdout.print("  {s} \"{s}\"\n", .{ simbolo, exemplo });
            }
        } else {
            try stdout.print("  Opcao invalida.\n", .{});
        }
    }

    try stdout.print("\n  Ate logo!\n", .{});
}

Passo 7: Testes

test "palindromo simples" {
    try std.testing.expect(ehPalindromo("arara"));
    try std.testing.expect(ehPalindromo("ovo"));
    try std.testing.expect(ehPalindromo("a"));
    try std.testing.expect(ehPalindromo(""));
}

test "nao palindromo" {
    try std.testing.expect(!ehPalindromo("zig"));
    try std.testing.expect(!ehPalindromo("hello"));
}

test "normalizar remove espacos e pontuacao" {
    var buf: [512]u8 = undefined;
    const resultado = normalizar("Roma me tem amor!", &buf);
    try std.testing.expectEqualStrings("romametemamor", resultado);
}

test "normalizar converte maiusculas" {
    var buf: [512]u8 = undefined;
    const resultado = normalizar("AaBb", &buf);
    try std.testing.expectEqualStrings("aabb", resultado);
}

test "verificar frase palindroma" {
    const resultado = verificar("Roma me tem amor");
    try std.testing.expect(resultado.eh_palindromo);
}

test "palindromo numerico" {
    try std.testing.expect(ehPalindromoNumerico(121));
    try std.testing.expect(ehPalindromoNumerico(12321));
    try std.testing.expect(!ehPalindromoNumerico(123));
}

test "maior subpalindromo" {
    const sub = maiorSubpalindromo("abacaba");
    try std.testing.expectEqualStrings("abacaba", "abacaba"[sub.inicio..sub.fim]);
}

test "encontrar palindromos numericos" {
    var resultados: [200]u64 = undefined;
    const count = encontrarPalindromosNumericos(10, 30, &resultados);
    try std.testing.expectEqual(@as(usize, 2), count); // 11, 22
    try std.testing.expectEqual(@as(u64, 11), resultados[0]);
    try std.testing.expectEqual(@as(u64, 22), resultados[1]);
}

Compilando e Executando

zig build test
zig build run

Conceitos Aprendidos

  • Algoritmo de dois ponteiros para verificação de palíndromos
  • Normalização de texto (minúsculas, remoção de acentos e pontuação)
  • Tratamento básico de sequências UTF-8 de 2 bytes
  • Algoritmo de expansão a partir do centro para subpalíndromos
  • Buffers fixos para processamento sem alocação

Próximos Passos

Continue aprendendo Zig

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