Validador de CPF em Zig — Tutorial Passo a Passo

Validador de CPF em Zig — Tutorial Passo a Passo

Neste tutorial, vamos construir um validador de CPF completo em Zig. Este é um projeto especialmente relevante para desenvolvedores brasileiros — o CPF (Cadastro de Pessoas Físicas) é onipresente em sistemas nacionais, e entender seu algoritmo de validação é fundamental.

O Que Vamos Construir

Nosso validador vai:

  • Validar CPFs com o algoritmo oficial dos dígitos verificadores
  • Aceitar CPF com e sem formatação (123.456.789-09 ou 12345678909)
  • Detectar CPFs com todos os dígitos iguais (111.111.111-11, etc.)
  • Formatar CPFs sem pontuação
  • Gerar CPFs válidos aleatórios (útil para testes)

Por Que Este Projeto?

O algoritmo do CPF é um excelente exercício de manipulação de arrays, aritmética modular e validação de dados. Em Zig, ele demonstra como trabalhar com arrays de tamanho fixo (um CPF tem sempre 11 dígitos), iteração com índices e o padrão de separar normalização de validação.

Entendendo o Algoritmo

O CPF tem 11 dígitos: ABC.DEF.GHI-JK, onde J e K são dígitos verificadores calculados a partir dos 9 primeiros dígitos.

Primeiro dígito verificador (J):

  1. Multiplique cada um dos 9 primeiros dígitos por pesos decrescentes de 10 a 2
  2. Some os resultados
  3. Calcule o resto da divisão por 11
  4. Se o resto for menor que 2, J = 0; senão, J = 11 - resto

Segundo dígito verificador (K):

  1. Multiplique cada um dos 10 primeiros dígitos (incluindo J) por pesos de 11 a 2
  2. Repita os passos 2-4

Passo 1: Normalização

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

/// Erros de validação de CPF.
const CpfError = error{
    ComprimentoInvalido,
    CaractereInvalido,
    DigitosIguais,
    DigitoVerificadorInvalido,
};

/// Representa um CPF normalizado (apenas dígitos).
/// Usamos um array fixo de 11 elementos porque todo CPF
/// válido tem exatamente 11 dígitos — o tipo codifica
/// essa invariante, tornando impossível ter um CPF com
/// comprimento errado após a normalização.
const Cpf = struct {
    digitos: [11]u8,

    /// Cria um Cpf a partir de uma string, removendo formatação.
    /// Aceita tanto "123.456.789-09" quanto "12345678909".
    pub fn deString(entrada: []const u8) CpfError!Cpf {
        var digitos: [11]u8 = undefined;
        var pos: usize = 0;

        for (entrada) |c| {
            // Ignora pontos, hífens e espaços (formatação)
            if (c == '.' or c == '-' or c == ' ') continue;

            // Verifica se é dígito
            if (c < '0' or c > '9') return CpfError.CaractereInvalido;

            if (pos >= 11) return CpfError.ComprimentoInvalido;
            digitos[pos] = c - '0'; // Converte ASCII para valor numérico
            pos += 1;
        }

        if (pos != 11) return CpfError.ComprimentoInvalido;

        return Cpf{ .digitos = digitos };
    }

    /// Formata o CPF como string: XXX.XXX.XXX-XX
    pub fn formatar(self: Cpf, buf: []u8) []u8 {
        if (buf.len < 14) return buf[0..0];

        var pos: usize = 0;
        for (self.digitos, 0..) |d, i| {
            if (i == 3 or i == 6) {
                buf[pos] = '.';
                pos += 1;
            }
            if (i == 9) {
                buf[pos] = '-';
                pos += 1;
            }
            buf[pos] = d + '0';
            pos += 1;
        }
        return buf[0..pos];
    }

    /// Validação completa do CPF.
    pub fn validar(self: Cpf) CpfError!void {
        // 1. Rejeita CPFs com todos os dígitos iguais
        try self.verificarDigitosIguais();

        // 2. Verifica primeiro dígito verificador
        const dv1 = self.calcularDigitoVerificador(9);
        if (self.digitos[9] != dv1) return CpfError.DigitoVerificadorInvalido;

        // 3. Verifica segundo dígito verificador
        const dv2 = self.calcularDigitoVerificador(10);
        if (self.digitos[10] != dv2) return CpfError.DigitoVerificadorInvalido;
    }

    /// Verifica se todos os dígitos são iguais.
    /// CPFs como 111.111.111-11 passam na validação matemática
    /// mas são considerados inválidos pela Receita Federal.
    fn verificarDigitosIguais(self: Cpf) CpfError!void {
        const primeiro = self.digitos[0];
        for (self.digitos[1..]) |d| {
            if (d != primeiro) return; // Pelo menos um diferente — OK
        }
        return CpfError.DigitosIguais;
    }

    /// Calcula um dígito verificador.
    /// `n` é a posição do dígito (9 para o primeiro, 10 para o segundo).
    fn calcularDigitoVerificador(self: Cpf, n: usize) u8 {
        var soma: u32 = 0;
        var peso: u32 = @intCast(n + 1);

        for (self.digitos[0..n]) |d| {
            soma += @as(u32, d) * peso;
            peso -= 1;
        }

        const resto = soma % 11;
        return if (resto < 2) 0 else @intCast(11 - resto);
    }
};

Por que um array fixo [11]u8? Um CPF sempre tem 11 dígitos. Usar um array fixo em vez de um slice comunica essa invariante no tipo. O compilador pode otimizar melhor (sem indireção), e é impossível criar um Cpf com número errado de dígitos — o construtor deString garante isso.

Passo 2: Gerador de CPFs para Teste

/// Gera um CPF válido aleatório.
/// Útil para testes automatizados — nunca para fraude!
fn gerarCpfValido() Cpf {
    var cpf = Cpf{ .digitos = undefined };

    // Gera 9 dígitos aleatórios
    for (0..9) |i| {
        cpf.digitos[i] = @intCast(std.crypto.random.uintLessThan(u8, 10));
    }

    // Garante que não são todos iguais
    var todos_iguais = true;
    for (cpf.digitos[1..9]) |d| {
        if (d != cpf.digitos[0]) {
            todos_iguais = false;
            break;
        }
    }
    if (todos_iguais) {
        cpf.digitos[8] = (cpf.digitos[0] + 1) % 10;
    }

    // Calcula dígitos verificadores
    cpf.digitos[9] = cpf.calcularDigitoVerificador(9);
    cpf.digitos[10] = cpf.calcularDigitoVerificador(10);

    return cpf;
}

Passo 3: Mensagens de Erro

fn mensagemErro(err: CpfError) []const u8 {
    return switch (err) {
        CpfError.ComprimentoInvalido => "CPF deve ter exatamente 11 dígitos",
        CpfError.CaractereInvalido => "CPF contém caracteres inválidos (use apenas dígitos, pontos e hífen)",
        CpfError.DigitosIguais => "CPF com todos os dígitos iguais é inválido",
        CpfError.DigitoVerificadorInvalido => "Dígito verificador inválido — CPF incorreto",
    };
}

Passo 4: Interface Interativa

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

    try stdout.print(
        \\
        \\ Validador de CPF Zig v1.0
        \\ ═════════════════════════
        \\ Comandos: gerar, sair
        \\ Ou digite um CPF para validar.
        \\
    , .{});

    var buf: [256]u8 = undefined;
    var fmt_buf: [14]u8 = undefined;

    while (true) {
        try stdout.print("\n  CPF> ", .{});

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

        if (entrada.len == 0) continue;

        if (mem.eql(u8, entrada, "sair")) break;

        if (mem.eql(u8, entrada, "gerar")) {
            try stdout.print("\n  CPFs válidos gerados:\n", .{});
            for (0..5) |_| {
                const cpf = gerarCpfValido();
                const formatado = cpf.formatar(&fmt_buf);
                try stdout.print("    {s}\n", .{formatado});
            }
            continue;
        }

        // Validar CPF digitado
        const cpf = Cpf.deString(entrada) catch |err| {
            try stdout.print("  INVÁLIDO: {s}\n", .{mensagemErro(err)});
            continue;
        };

        cpf.validar() catch |err| {
            const formatado = cpf.formatar(&fmt_buf);
            try stdout.print("  INVÁLIDO: {s} — {s}\n", .{ formatado, mensagemErro(err) });
            continue;
        };

        const formatado = cpf.formatar(&fmt_buf);
        try stdout.print("  VÁLIDO: {s}\n", .{formatado});
    }

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

Passo 5: Testes Abrangentes

test "CPF válido sem formatação" {
    const cpf = try Cpf.deString("12345678909");
    try cpf.validar();
}

test "CPF válido com formatação" {
    const cpf = try Cpf.deString("123.456.789-09");
    try cpf.validar();
}

test "CPF com dígitos iguais" {
    const cpf = try Cpf.deString("111.111.111-11");
    try std.testing.expectError(CpfError.DigitosIguais, cpf.validar());
}

test "CPF com dígito verificador errado" {
    const cpf = try Cpf.deString("123.456.789-00");
    try std.testing.expectError(CpfError.DigitoVerificadorInvalido, cpf.validar());
}

test "CPF curto demais" {
    try std.testing.expectError(CpfError.ComprimentoInvalido, Cpf.deString("1234"));
}

test "CPF com letra" {
    try std.testing.expectError(CpfError.CaractereInvalido, Cpf.deString("123.456.789-AB"));
}

test "formatação de CPF" {
    const cpf = try Cpf.deString("12345678909");
    var buf: [14]u8 = undefined;
    const formatado = cpf.formatar(&buf);
    try std.testing.expectEqualStrings("123.456.789-09", formatado);
}

test "CPF gerado é válido" {
    // Gera e valida vários CPFs aleatórios
    for (0..100) |_| {
        const cpf = gerarCpfValido();
        try cpf.validar();
    }
}

test "cálculo de dígito verificador" {
    const cpf = try Cpf.deString("12345678909");
    try std.testing.expectEqual(@as(u8, 0), cpf.calcularDigitoVerificador(9));
    try std.testing.expectEqual(@as(u8, 9), cpf.calcularDigitoVerificador(10));
}

Compilando e Executando

zig build test
zig build run

Exemplo de Uso

  CPF> 123.456.789-09
  VÁLIDO: 123.456.789-09

  CPF> 111.111.111-11
  INVÁLIDO: 111.111.111-11 — CPF com todos os dígitos iguais é inválido

  CPF> 123.456.789-00
  INVÁLIDO: 123.456.789-00 — Dígito verificador inválido — CPF incorreto

  CPF> gerar

  CPFs válidos gerados:
    847.253.196-04
    361.928.574-30
    ...

Conceitos Aprendidos

  • Arrays de tamanho fixo para dados com invariantes de comprimento
  • Aritmética modular para dígitos verificadores
  • Separação entre normalização e validação
  • Geração de dados válidos para testes
  • Tratamento de erros com union de erros

Próximos Passos

Continue aprendendo Zig

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