Conversor de Temperatura em Zig — Tutorial Passo a Passo
Neste tutorial, vamos construir um conversor de temperatura que converte entre Celsius, Fahrenheit e Kelvin. É um projeto simples, mas nos permite explorar o uso de enums, funções puras e formatação numérica em Zig.
O Que Vamos Construir
Nosso conversor vai:
- Converter entre Celsius, Fahrenheit e Kelvin (todas as combinações)
- Oferecer modo interativo e modo de linha de comando
- Exibir uma tabela de conversão para referência
- Validar entradas e tratar erros
Por Que Este Projeto?
Conversão de temperatura é fundamentalmente sobre funções puras — funções que sempre retornam o mesmo resultado para a mesma entrada, sem efeitos colaterais. Zig incentiva esse estilo funcional, e este projeto demonstra como organizar código em torno de funções puras que são fáceis de testar.
Pré-requisitos
- Zig 0.13+ instalado
- Conhecimentos básicos de Zig (fundamentos)
Passo 1: Definindo as Escalas
const std = @import("std");
const io = std.io;
const fmt = std.fmt;
const mem = std.mem;
/// Escalas de temperatura suportadas.
/// Um enum é a escolha natural aqui: o conjunto de escalas
/// é fixo, e cada uma tem um nome e símbolo associado.
const Escala = enum {
celsius,
fahrenheit,
kelvin,
/// Retorna o símbolo da escala para exibição.
pub fn simbolo(self: Escala) []const u8 {
return switch (self) {
.celsius => "°C",
.fahrenheit => "°F",
.kelvin => "K",
};
}
/// Retorna o nome completo da escala.
pub fn nome(self: Escala) []const u8 {
return switch (self) {
.celsius => "Celsius",
.fahrenheit => "Fahrenheit",
.kelvin => "Kelvin",
};
}
/// Tenta converter uma string em uma Escala.
pub fn deString(s: []const u8) ?Escala {
const lower = blk: {
var buf: [16]u8 = undefined;
const len = @min(s.len, 16);
for (s[0..len], 0..) |c, i| {
buf[i] = std.ascii.toLower(c);
}
break :blk buf[0..len];
};
if (mem.eql(u8, lower, "c") or mem.eql(u8, lower, "celsius")) return .celsius;
if (mem.eql(u8, lower, "f") or mem.eql(u8, lower, "fahrenheit")) return .fahrenheit;
if (mem.eql(u8, lower, "k") or mem.eql(u8, lower, "kelvin")) return .kelvin;
return null;
}
};
Passo 2: Funções de Conversão
O coração do conversor são funções puras de conversão. A estratégia é converter tudo para Celsius primeiro, e depois para a escala de destino. Isso reduz o número de funções necessárias de 6 (uma para cada par) para 4.
/// Converte qualquer temperatura para Celsius.
/// Decisão de design: centralizar a conversão via Celsius reduz
/// a complexidade de O(n²) para O(n) em número de funções.
fn paraCelsius(valor: f64, de: Escala) f64 {
return switch (de) {
.celsius => valor,
.fahrenheit => (valor - 32.0) * 5.0 / 9.0,
.kelvin => valor - 273.15,
};
}
/// Converte de Celsius para qualquer outra escala.
fn deCelsius(celsius: f64, para: Escala) f64 {
return switch (para) {
.celsius => celsius,
.fahrenheit => celsius * 9.0 / 5.0 + 32.0,
.kelvin => celsius + 273.15,
};
}
/// Função principal de conversão: de qualquer escala para qualquer escala.
/// Esta é a API pública — os detalhes da conversão via Celsius
/// ficam encapsulados nas funções auxiliares.
fn converter(valor: f64, de: Escala, para: Escala) f64 {
if (de == para) return valor;
const celsius = paraCelsius(valor, de);
return deCelsius(celsius, para);
}
/// Verifica se a temperatura é fisicamente válida
/// (acima do zero absoluto).
fn temperaturaValida(valor: f64, escala: Escala) bool {
return switch (escala) {
.celsius => valor >= -273.15,
.fahrenheit => valor >= -459.67,
.kelvin => valor >= 0,
};
}
Por que converter via Celsius? Se temos N escalas, precisaríamos de N*(N-1) funções de conversão direta. Usando Celsius como intermediário, precisamos de apenas 2*N funções. Com 3 escalas a diferença é pequena (6 vs 6), mas a arquitetura escala melhor — se adicionarmos Rankine ou Réaumur, basta escrever duas funções novas.
Passo 3: Exibição de Tabela
/// Gera uma tabela de conversão para referência rápida.
fn exibirTabela(writer: anytype) !void {
try writer.print("\n ╔═══════════╦═══════════╦═══════════╗\n", .{});
try writer.print(" ║ Celsius ║ Fahrenheit║ Kelvin ║\n", .{});
try writer.print(" ╠═══════════╬═══════════╬═══════════╣\n", .{});
const valores = [_]f64{ -40, -20, 0, 10, 20, 25, 30, 37, 100, 200 };
for (valores) |c| {
const f = converter(c, .celsius, .fahrenheit);
const k = converter(c, .celsius, .kelvin);
try writer.print(" ║ {d:>8.1} ║ {d:>8.1} ║ {d:>8.1} ║\n", .{ c, f, k });
}
try writer.print(" ╚═══════════╩═══════════╩═══════════╝\n", .{});
}
Passo 4: Interface Interativa
/// Lê um número de ponto flutuante do stdin.
fn lerNumero(reader: anytype, writer: anytype, prompt: []const u8, buf: []u8) ?f64 {
writer.print("{s}", .{prompt}) catch return null;
const linha = reader.readUntilDelimiterOrEof(buf, '\n') catch return null orelse return null;
const limpa = mem.trim(u8, linha, " \t\r\n");
return fmt.parseFloat(f64, limpa) catch {
writer.print(" Número inválido.\n", .{}) catch {};
return null;
};
}
/// Lê uma escala de temperatura do stdin.
fn lerEscala(reader: anytype, writer: anytype, prompt: []const u8, buf: []u8) ?Escala {
writer.print("{s}", .{prompt}) catch return null;
const linha = reader.readUntilDelimiterOrEof(buf, '\n') catch return null orelse return null;
const limpa = mem.trim(u8, linha, " \t\r\n");
return Escala.deString(limpa) orelse {
writer.print(" Escala inválida. Use: C, F ou K\n", .{}) catch {};
return null;
};
}
pub fn main() !void {
const stdout = io.getStdOut().writer();
const stdin = io.getStdIn().reader();
try stdout.print(
\\
\\ Conversor de Temperatura Zig v1.0
\\ ══════════════════════════════════
\\ Escalas: Celsius (C), Fahrenheit (F), Kelvin (K)
\\ Comandos: tabela, sair
\\
, .{});
var buf: [256]u8 = undefined;
while (true) {
try stdout.print("\n", .{});
// Ler escala de origem
const escala_de = lerEscala(stdin, stdout, " Escala de origem (C/F/K): ", &buf) orelse continue;
// Ler valor
const valor = lerNumero(stdin, stdout, " Temperatura: ", &buf) orelse continue;
// Validar temperatura
if (!temperaturaValida(valor, escala_de)) {
try stdout.print(" Temperatura abaixo do zero absoluto!\n", .{});
continue;
}
// Ler escala de destino
const escala_para = lerEscala(stdin, stdout, " Escala de destino (C/F/K): ", &buf) orelse continue;
// Converter e exibir
const resultado = converter(valor, escala_de, escala_para);
try stdout.print(
"\n {d:.2} {s} = {d:.2} {s}\n",
.{ valor, escala_de.simbolo(), resultado, escala_para.simbolo() },
);
}
}
Passo 5: Testes Abrangentes
Funções puras são um prazer de testar — não há estado para configurar nem efeitos colaterais para limpar.
test "celsius para fahrenheit" {
try std.testing.expectApproxEqAbs(@as(f64, 32.0), converter(0, .celsius, .fahrenheit), 0.01);
try std.testing.expectApproxEqAbs(@as(f64, 212.0), converter(100, .celsius, .fahrenheit), 0.01);
try std.testing.expectApproxEqAbs(@as(f64, -40.0), converter(-40, .celsius, .fahrenheit), 0.01);
}
test "fahrenheit para celsius" {
try std.testing.expectApproxEqAbs(@as(f64, 0.0), converter(32, .fahrenheit, .celsius), 0.01);
try std.testing.expectApproxEqAbs(@as(f64, 100.0), converter(212, .fahrenheit, .celsius), 0.01);
}
test "celsius para kelvin" {
try std.testing.expectApproxEqAbs(@as(f64, 273.15), converter(0, .celsius, .kelvin), 0.01);
try std.testing.expectApproxEqAbs(@as(f64, 373.15), converter(100, .celsius, .kelvin), 0.01);
}
test "kelvin para celsius" {
try std.testing.expectApproxEqAbs(@as(f64, 0.0), converter(273.15, .kelvin, .celsius), 0.01);
try std.testing.expectApproxEqAbs(@as(f64, -273.15), converter(0, .kelvin, .celsius), 0.01);
}
test "mesma escala retorna valor inalterado" {
try std.testing.expectEqual(@as(f64, 42.0), converter(42, .celsius, .celsius));
try std.testing.expectEqual(@as(f64, 100.0), converter(100, .fahrenheit, .fahrenheit));
}
test "validação de temperatura" {
try std.testing.expect(temperaturaValida(0, .celsius));
try std.testing.expect(!temperaturaValida(-300, .celsius));
try std.testing.expect(temperaturaValida(0, .kelvin));
try std.testing.expect(!temperaturaValida(-1, .kelvin));
}
test "parse de escala" {
try std.testing.expectEqual(Escala.celsius, Escala.deString("C").?);
try std.testing.expectEqual(Escala.fahrenheit, Escala.deString("f").?);
try std.testing.expectEqual(Escala.kelvin, Escala.deString("kelvin").?);
try std.testing.expect(Escala.deString("X") == null);
}
Compilando e Executando
zig build test # Executa os testes
zig build run # Compila e executa
Conceitos Aprendidos
- Funções puras e composição funcional
- Enums com métodos associados
- Formatação numérica com
std.fmt - Organização de código via centralização (conversão via Celsius)
- Validação de dados de entrada
Próximos Passos
- Explore formatação numérica para mais opções de exibição
- Adicione suporte a argumentos de linha de comando com std.process.args
- Construa o próximo projeto: Gerador de Senhas