Calculadora de IMC em Zig — Tutorial Passo a Passo

Calculadora de IMC em Zig — Tutorial Passo a Passo

Neste tutorial, vamos construir uma calculadora de Índice de Massa Corporal (IMC) interativa no terminal. O IMC é uma fórmula simples (peso / altura^2), mas ao redor dela construiremos um sistema completo com classificações, validação de entrada e histórico de medições.

O Que Vamos Construir

Nossa calculadora vai:

  • Calcular o IMC a partir de peso (kg) e altura (m)
  • Classificar o resultado segundo a tabela da OMS
  • Exibir o peso ideal para a altura informada
  • Manter um histórico de medições da sessão
  • Validar entradas com mensagens claras de erro

Por Que Este Projeto?

Este projeto é excelente para iniciantes porque combina parsing de floats, enums com lógica de negócio, formatação de saída e validação de dados. A fórmula é simples, mas a experiência do usuário requer cuidado — e isso é o que diferencia um programa funcional de um programa bem feito.

Pré-requisitos

Passo 1: Estrutura do Projeto

mkdir calculadora-imc
cd calculadora-imc
zig init

Passo 2: Definindo Classificações

A OMS define faixas de classificação para o IMC. Modelamos isso com um enum.

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

/// Classificação do IMC segundo a Organização Mundial da Saúde.
/// Cada faixa tem limites inferior e superior, permitindo
/// verificar em qual categoria um valor se encaixa.
const ClassificacaoIMC = enum {
    abaixo_peso,
    peso_normal,
    sobrepeso,
    obesidade_grau1,
    obesidade_grau2,
    obesidade_grau3,

    /// Determina a classificação a partir do valor do IMC.
    pub fn deValor(imc: f64) ClassificacaoIMC {
        if (imc < 18.5) return .abaixo_peso;
        if (imc < 25.0) return .peso_normal;
        if (imc < 30.0) return .sobrepeso;
        if (imc < 35.0) return .obesidade_grau1;
        if (imc < 40.0) return .obesidade_grau2;
        return .obesidade_grau3;
    }

    /// Nome da classificação em português.
    pub fn nome(self: ClassificacaoIMC) []const u8 {
        return switch (self) {
            .abaixo_peso => "Abaixo do peso",
            .peso_normal => "Peso normal",
            .sobrepeso => "Sobrepeso",
            .obesidade_grau1 => "Obesidade Grau I",
            .obesidade_grau2 => "Obesidade Grau II",
            .obesidade_grau3 => "Obesidade Grau III",
        };
    }

    /// Cor ANSI para exibição visual no terminal.
    pub fn cor(self: ClassificacaoIMC) []const u8 {
        return switch (self) {
            .abaixo_peso => "\x1b[33m",     // amarelo
            .peso_normal => "\x1b[32m",      // verde
            .sobrepeso => "\x1b[33m",        // amarelo
            .obesidade_grau1 => "\x1b[31m",  // vermelho
            .obesidade_grau2 => "\x1b[31m",  // vermelho
            .obesidade_grau3 => "\x1b[35m",  // magenta
        };
    }

    /// Dica de saúde associada à classificação.
    pub fn dica(self: ClassificacaoIMC) []const u8 {
        return switch (self) {
            .abaixo_peso => "Consulte um nutricionista. Ganho de peso saudavel envolve dieta equilibrada.",
            .peso_normal => "Otimo! Mantenha habitos saudaveis com alimentacao balanceada e exercicios.",
            .sobrepeso => "Atencao: pequenas mudancas na dieta e atividade fisica podem ajudar.",
            .obesidade_grau1 => "Recomenda-se acompanhamento medico e mudancas no estilo de vida.",
            .obesidade_grau2 => "Procure orientacao medica. Tratamento multidisciplinar e importante.",
            .obesidade_grau3 => "Busque acompanhamento medico urgente. Tratamento especializado e essencial.",
        };
    }

    /// Faixa de IMC da classificação.
    pub fn faixa(self: ClassificacaoIMC) struct { min: f64, max: f64 } {
        return switch (self) {
            .abaixo_peso => .{ .min = 0.0, .max = 18.49 },
            .peso_normal => .{ .min = 18.5, .max = 24.99 },
            .sobrepeso => .{ .min = 25.0, .max = 29.99 },
            .obesidade_grau1 => .{ .min = 30.0, .max = 34.99 },
            .obesidade_grau2 => .{ .min = 35.0, .max = 39.99 },
            .obesidade_grau3 => .{ .min = 40.0, .max = 100.0 },
        };
    }
};

Decisão de design: Colocamos todas as informações relacionadas à classificação dentro do enum. Isso segue o princípio de coesão — tudo sobre classificações fica junto. Se precisarmos adicionar uma nova classificação, só alteramos um lugar.

Passo 3: Cálculos

/// Erros possíveis durante o cálculo.
const IMCError = error{
    PesoInvalido,
    AlturaInvalida,
    ValorForaDoIntervalo,
};

/// Resultado completo de um cálculo de IMC.
const ResultadoIMC = struct {
    imc: f64,
    classificacao: ClassificacaoIMC,
    peso: f64,
    altura: f64,
    peso_ideal_min: f64,
    peso_ideal_max: f64,

    /// Calcula a diferença para o peso ideal mais próximo.
    /// Positivo = precisa perder, Negativo = precisa ganhar.
    pub fn diferencaPesoIdeal(self: *const ResultadoIMC) f64 {
        if (self.peso < self.peso_ideal_min) {
            return self.peso - self.peso_ideal_min; // negativo
        } else if (self.peso > self.peso_ideal_max) {
            return self.peso - self.peso_ideal_max; // positivo
        }
        return 0.0; // dentro do ideal
    }
};

/// Calcula o IMC e todas as informações derivadas.
/// Fórmula: IMC = peso(kg) / altura(m)²
///
/// Validamos os valores de entrada porque dados de saúde
/// precisam ser tratados com cuidado — valores absurdos
/// podem gerar resultados enganosos.
fn calcularIMC(peso: f64, altura: f64) IMCError!ResultadoIMC {
    // Validação de peso (0.5 kg a 500 kg cobre todos os casos reais)
    if (peso <= 0.0 or peso > 500.0) return IMCError.PesoInvalido;

    // Validação de altura (0.3 m a 2.8 m cobre todos os casos reais)
    if (altura <= 0.0 or altura > 3.0) return IMCError.AlturaInvalida;

    const imc = peso / (altura * altura);

    // Sanity check do resultado
    if (imc < 5.0 or imc > 100.0) return IMCError.ValorForaDoIntervalo;

    // Peso ideal: IMC entre 18.5 e 24.9
    const altura2 = altura * altura;

    return ResultadoIMC{
        .imc = imc,
        .classificacao = ClassificacaoIMC.deValor(imc),
        .peso = peso,
        .altura = altura,
        .peso_ideal_min = 18.5 * altura2,
        .peso_ideal_max = 24.9 * altura2,
    };
}

Passo 4: Histórico de Medições

/// Registro de uma medição no histórico.
const Medicao = struct {
    peso: f64,
    altura: f64,
    imc: f64,
    classificacao: ClassificacaoIMC,
};

/// Histórico de medições da sessão.
const Historico = struct {
    medicoes: [50]Medicao,
    quantidade: usize,

    pub fn init() Historico {
        return .{
            .medicoes = undefined,
            .quantidade = 0,
        };
    }

    pub fn adicionar(self: *Historico, resultado: ResultadoIMC) void {
        if (self.quantidade >= 50) return;
        self.medicoes[self.quantidade] = .{
            .peso = resultado.peso,
            .altura = resultado.altura,
            .imc = resultado.imc,
            .classificacao = resultado.classificacao,
        };
        self.quantidade += 1;
    }

    pub fn exibir(self: *const Historico, writer: anytype) !void {
        if (self.quantidade == 0) {
            try writer.print("  Nenhuma medicao registrada.\n", .{});
            return;
        }

        try writer.print("\n  {s:<6} {s:<8} {s:<8} {s}\n", .{ "Num", "Peso", "Altura", "IMC / Classificacao" });
        try writer.print("  {s}\n", .{"-" ** 50});

        var i: usize = 0;
        while (i < self.quantidade) : (i += 1) {
            const m = self.medicoes[i];
            const reset = "\x1b[0m";
            try writer.print("  {d:<6} {d:<8.1} {d:<8.2} {s}{d:.1} - {s}{s}\n", .{
                i + 1, m.peso, m.altura,
                m.classificacao.cor(), m.imc, m.classificacao.nome(), reset,
            });
        }
    }
};

Passo 5: Exibição dos Resultados

/// Exibe o resultado formatado com barra visual.
fn exibirResultado(resultado: ResultadoIMC, writer: anytype) !void {
    const reset = "\x1b[0m";
    const cor = resultado.classificacao.cor();

    try writer.print(
        \\
        \\  ==========================================
        \\           RESULTADO DO IMC
        \\  ==========================================
        \\
        \\  Peso:    {d:.1} kg
        \\  Altura:  {d:.2} m
        \\
        \\  IMC:     {s}{d:.1}{s}
        \\  Status:  {s}{s}{s}
        \\
    , .{
        resultado.peso, resultado.altura,
        cor, resultado.imc, reset,
        cor, resultado.classificacao.nome(), reset,
    });

    // Barra visual do IMC
    try writer.print("  Escala: ", .{});
    try exibirBarraIMC(resultado.imc, writer);
    try writer.print("\n", .{});

    // Peso ideal
    try writer.print(
        \\
        \\  Peso ideal para sua altura:
        \\    {d:.1} kg a {d:.1} kg
        \\
    , .{ resultado.peso_ideal_min, resultado.peso_ideal_max });

    const diff = resultado.diferencaPesoIdeal();
    if (diff > 0.1) {
        try writer.print("    Voce esta {d:.1} kg acima do peso ideal.\n", .{diff});
    } else if (diff < -0.1) {
        try writer.print("    Voce esta {d:.1} kg abaixo do peso ideal.\n", .{-diff});
    } else {
        try writer.print("    Voce esta dentro do peso ideal!\n", .{});
    }

    // Dica de saude
    try writer.print("\n  Dica: {s}\n", .{resultado.classificacao.dica()});
}

/// Exibe uma barra visual representando onde o IMC cai na escala.
fn exibirBarraIMC(imc: f64, writer: anytype) !void {
    // Escala de 15 a 45
    const min_escala: f64 = 15.0;
    const max_escala: f64 = 45.0;
    const largura: usize = 30;

    try writer.print("[", .{});

    var i: usize = 0;
    while (i < largura) : (i += 1) {
        const pos = min_escala + (max_escala - min_escala) * @as(f64, @floatFromInt(i)) / @as(f64, @floatFromInt(largura));
        const imc_pos = @as(usize, @intFromFloat((imc - min_escala) / (max_escala - min_escala) * @as(f64, @floatFromInt(largura))));

        if (i == imc_pos) {
            try writer.print("*", .{});
        } else if (pos < 18.5) {
            try writer.print("-", .{});
        } else if (pos < 25.0) {
            try writer.print("=", .{});
        } else if (pos < 30.0) {
            try writer.print("+", .{});
        } else {
            try writer.print("#", .{});
        }
    }
    try writer.print("]", .{});
    try writer.print("\n           15    18.5    25     30    40", .{});
}

/// Lê um número float do stdin com mensagem de prompt.
fn lerFloat(reader: anytype, writer: anytype, prompt: []const u8) !?f64 {
    try writer.print("{s}", .{prompt});
    var buf: [64]u8 = undefined;
    const linha = reader.readUntilDelimiterOrEof(&buf, '\n') catch return null orelse return null;
    const limpa = mem.trim(u8, linha, " \t\r\n");
    // Aceita vírgula como separador decimal
    var normalizada: [64]u8 = undefined;
    var len: usize = 0;
    for (limpa) |c| {
        if (c == ',') {
            normalizada[len] = '.';
        } else {
            normalizada[len] = c;
        }
        len += 1;
    }
    return fmt.parseFloat(f64, normalizada[0..len]) catch null;
}

Passo 6: Tabela de Referência e Loop Principal

/// Exibe a tabela de referência da OMS.
fn exibirTabela(writer: anytype) !void {
    const reset = "\x1b[0m";
    try writer.print(
        \\
        \\  ==========================================
        \\     TABELA DE REFERENCIA - OMS
        \\  ==========================================
        \\
    , .{});

    const classificacoes = [_]ClassificacaoIMC{
        .abaixo_peso, .peso_normal, .sobrepeso,
        .obesidade_grau1, .obesidade_grau2, .obesidade_grau3,
    };

    for (classificacoes) |c| {
        const faixa = c.faixa();
        try writer.print("  {s}{s:<20}{s} IMC {d:>5.1} - {d:.1}\n", .{
            c.cor(), c.nome(), reset, faixa.min, faixa.max,
        });
    }
    try writer.print("\n", .{});
}

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

    var historico = Historico.init();

    try stdout.print(
        \\
        \\  ==========================================
        \\     CALCULADORA DE IMC - Zig
        \\  ==========================================
        \\
    , .{});

    var buf: [64]u8 = undefined;

    while (true) {
        try stdout.print(
            \\
            \\  [1] Calcular IMC
            \\  [2] Ver historico
            \\  [3] Tabela de referencia
            \\  [4] 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, "4") or mem.eql(u8, opcao, "sair")) {
            try stdout.print("\n  Ate logo! Cuide-se.\n", .{});
            break;
        }

        if (mem.eql(u8, opcao, "2")) {
            try historico.exibir(stdout);
            continue;
        }

        if (mem.eql(u8, opcao, "3")) {
            try exibirTabela(stdout);
            continue;
        }

        if (mem.eql(u8, opcao, "1")) {
            const peso = (try lerFloat(stdin, stdout, "\n  Peso (kg): ")) orelse {
                try stdout.print("  Valor invalido para peso.\n", .{});
                continue;
            };

            const altura = (try lerFloat(stdin, stdout, "  Altura (m, ex: 1.75): ")) orelse {
                try stdout.print("  Valor invalido para altura.\n", .{});
                continue;
            };

            const resultado = calcularIMC(peso, altura) catch |err| {
                switch (err) {
                    IMCError.PesoInvalido => try stdout.print("  Peso invalido. Use valores entre 0.5 e 500 kg.\n", .{}),
                    IMCError.AlturaInvalida => try stdout.print("  Altura invalida. Use valores entre 0.3 e 2.8 m.\n", .{}),
                    IMCError.ValorForaDoIntervalo => try stdout.print("  Valores resultam em IMC fora do intervalo razoavel.\n", .{}),
                }
                continue;
            };

            try exibirResultado(resultado, stdout);
            historico.adicionar(resultado);
        } else {
            try stdout.print("  Opcao invalida.\n", .{});
        }
    }
}

Passo 7: Testes

test "classificacao IMC" {
    try std.testing.expectEqual(ClassificacaoIMC.abaixo_peso, ClassificacaoIMC.deValor(17.0));
    try std.testing.expectEqual(ClassificacaoIMC.peso_normal, ClassificacaoIMC.deValor(22.0));
    try std.testing.expectEqual(ClassificacaoIMC.sobrepeso, ClassificacaoIMC.deValor(27.5));
    try std.testing.expectEqual(ClassificacaoIMC.obesidade_grau1, ClassificacaoIMC.deValor(32.0));
    try std.testing.expectEqual(ClassificacaoIMC.obesidade_grau2, ClassificacaoIMC.deValor(37.0));
    try std.testing.expectEqual(ClassificacaoIMC.obesidade_grau3, ClassificacaoIMC.deValor(42.0));
}

test "calcular IMC - peso normal" {
    const resultado = try calcularIMC(70.0, 1.75);
    try std.testing.expectApproxEqAbs(@as(f64, 22.86), resultado.imc, 0.01);
    try std.testing.expectEqual(ClassificacaoIMC.peso_normal, resultado.classificacao);
}

test "calcular IMC - peso ideal inclui o peso" {
    const resultado = try calcularIMC(70.0, 1.75);
    try std.testing.expect(resultado.peso >= resultado.peso_ideal_min);
    try std.testing.expect(resultado.peso <= resultado.peso_ideal_max);
    try std.testing.expectApproxEqAbs(@as(f64, 0.0), resultado.diferencaPesoIdeal(), 0.01);
}

test "rejeitar peso invalido" {
    try std.testing.expectError(IMCError.PesoInvalido, calcularIMC(-10.0, 1.75));
    try std.testing.expectError(IMCError.PesoInvalido, calcularIMC(0.0, 1.75));
}

test "rejeitar altura invalida" {
    try std.testing.expectError(IMCError.AlturaInvalida, calcularIMC(70.0, 0.0));
    try std.testing.expectError(IMCError.AlturaInvalida, calcularIMC(70.0, 5.0));
}

test "diferenca peso ideal - acima do peso" {
    const resultado = try calcularIMC(100.0, 1.70);
    try std.testing.expect(resultado.diferencaPesoIdeal() > 0.0);
}

test "diferenca peso ideal - abaixo do peso" {
    const resultado = try calcularIMC(45.0, 1.75);
    try std.testing.expect(resultado.diferencaPesoIdeal() < 0.0);
}

Compilando e Executando

zig build test
zig build run

Conceitos Aprendidos

  • Enums com múltiplas funções associadas para lógica de domínio
  • Parsing de floats com std.fmt.parseFloat
  • Validação de dados de entrada com erros específicos
  • Formatação de saída com cores ANSI
  • Structs para agrupar resultados calculados
  • Conversão de vírgula para ponto (localização)

Próximos Passos

Continue aprendendo Zig

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