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
- Zig 0.13+ instalado (guia de instalação)
- Familiaridade com tipos básicos de Zig (fundamentos)
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
- Aprenda sobre formatação de números em Zig
- Explore como salvar dados em arquivo para histórico persistente
- Construa o próximo projeto: Gerador de Lorem Ipsum