Conversor de Temperatura em Zig — Tutorial Passo a Passo

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

Continue aprendendo Zig

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