Conversor de Moedas em Zig — Tutorial Passo a Passo

Conversor de Moedas em Zig — Tutorial Passo a Passo

Neste tutorial, vamos construir um conversor de moedas que suporta múltiplas moedas internacionais. Este projeto demonstra como trabalhar com dados tabulares, HashMaps e formatação financeira em Zig.

O Que Vamos Construir

Nosso conversor vai:

  • Converter entre BRL, USD, EUR, GBP, JPY e outras moedas
  • Usar taxas de câmbio configuráveis (via constantes ou arquivo)
  • Exibir tabela de cotações
  • Manter histórico de conversões da sessão
  • Formatar valores monetários corretamente

Por Que Este Projeto?

Trabalhar com dinheiro em programação exige cuidado: arredondamento, precisão de ponto flutuante e formatação localizada são problemas reais. Neste projeto, exploramos esses desafios usando f64 com arredondamento explícito e formatação controlada.

Passo 1: Modelagem de Moedas

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

/// Informações sobre uma moeda.
const InfoMoeda = struct {
    codigo: []const u8,
    nome: []const u8,
    simbolo: []const u8,
    taxa_para_usd: f64, // Quanto 1 unidade vale em USD
};

/// Tabela de moedas suportadas com taxas de referência.
/// As taxas são relativas ao USD (dólar americano) como base.
/// Decisão: usar USD como base é padrão no mercado financeiro
/// e simplifica a conversão entre qualquer par de moedas.
const moedas = [_]InfoMoeda{
    .{ .codigo = "USD", .nome = "Dólar Americano", .simbolo = "$", .taxa_para_usd = 1.0 },
    .{ .codigo = "BRL", .nome = "Real Brasileiro", .simbolo = "R$", .taxa_para_usd = 0.1818 },
    .{ .codigo = "EUR", .nome = "Euro", .simbolo = "€", .taxa_para_usd = 1.0870 },
    .{ .codigo = "GBP", .nome = "Libra Esterlina", .simbolo = "£", .taxa_para_usd = 1.2650 },
    .{ .codigo = "JPY", .nome = "Iene Japonês", .simbolo = "¥", .taxa_para_usd = 0.00667 },
    .{ .codigo = "ARS", .nome = "Peso Argentino", .simbolo = "AR$", .taxa_para_usd = 0.00094 },
    .{ .codigo = "CNY", .nome = "Yuan Chinês", .simbolo = "¥", .taxa_para_usd = 0.1389 },
    .{ .codigo = "CAD", .nome = "Dólar Canadense", .simbolo = "C$", .taxa_para_usd = 0.7407 },
    .{ .codigo = "CHF", .nome = "Franco Suíço", .simbolo = "CHF", .taxa_para_usd = 1.1236 },
    .{ .codigo = "AUD", .nome = "Dólar Australiano", .simbolo = "A$", .taxa_para_usd = 0.6494 },
};

/// Busca informações de uma moeda pelo código.
fn buscarMoeda(codigo: []const u8) ?InfoMoeda {
    for (moedas) |m| {
        if (std.ascii.eqlIgnoreCase(m.codigo, codigo)) return m;
    }
    return null;
}

Passo 2: Motor de Conversão

/// Converte um valor de uma moeda para outra.
/// A conversão passa pelo USD como moeda intermediária:
/// valor_origem -> USD -> valor_destino
///
/// Decisão: arredondamos para 2 casas decimais no final
/// porque é o padrão para a maioria das moedas. O JPY seria
/// melhor com 0 casas, mas simplificamos para este tutorial.
fn converter(valor: f64, de: InfoMoeda, para: InfoMoeda) f64 {
    const em_usd = valor * de.taxa_para_usd;
    const resultado = em_usd / para.taxa_para_usd;
    return arredondar(resultado, 2);
}

/// Arredonda para N casas decimais.
fn arredondar(valor: f64, casas: u8) f64 {
    const fator = std.math.pow(f64, 10.0, @floatFromInt(casas));
    return @round(valor * fator) / fator;
}

/// Exibe tabela de cotações.
fn exibirCotacoes(writer: anytype) !void {
    try writer.print("\n  ╔════════╦══════════════════════╦════════════════╗\n", .{});
    try writer.print("  ║ Código ║ Moeda                ║ 1 USD =        ║\n", .{});
    try writer.print("  ╠════════╬══════════════════════╬════════════════╣\n", .{});

    for (moedas) |m| {
        if (m.taxa_para_usd > 0) {
            const cotacao = 1.0 / m.taxa_para_usd;
            try writer.print("  ║ {s:<6} ║ {s:<20} ║ {s} {d:>9.2} ║\n", .{
                m.codigo, m.nome, m.simbolo, arredondar(cotacao, 2),
            });
        }
    }

    try writer.print("  ╚════════╩══════════════════════╩════════════════╝\n", .{});
}

Passo 3: Histórico de Conversões

const RegistroConversao = struct {
    texto: [128]u8,
    texto_len: usize,
};

const HistoricoConversao = struct {
    registros: [50]RegistroConversao,
    quantidade: usize,

    const Self = @This();

    pub fn init() Self {
        return Self{
            .registros = undefined,
            .quantidade = 0,
        };
    }

    pub fn adicionar(self: *Self, de: InfoMoeda, para: InfoMoeda, valor: f64, resultado: f64) void {
        if (self.quantidade >= 50) return;

        var buf: [128]u8 = undefined;
        const texto = fmt.bufPrint(&buf, "{s} {d:.2} {s} = {s} {d:.2} {s}", .{
            de.simbolo, valor, de.codigo, para.simbolo, resultado, para.codigo,
        }) catch return;

        self.registros[self.quantidade] = .{
            .texto = buf,
            .texto_len = texto.len,
        };
        self.quantidade += 1;
    }

    pub fn exibir(self: *const Self, writer: anytype) !void {
        if (self.quantidade == 0) {
            try writer.print("  (nenhuma conversão realizada)\n", .{});
            return;
        }
        for (0..self.quantidade) |i| {
            const reg = self.registros[i];
            try writer.print("  {d}. {s}\n", .{ i + 1, reg.texto[0..reg.texto_len] });
        }
    }
};

Passo 4: Interface Principal

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

    var historico = HistoricoConversao.init();
    var buf: [256]u8 = undefined;

    try stdout.print(
        \\
        \\ Conversor de Moedas Zig v1.0
        \\ ═════════════════════════════
        \\ Moedas: USD, BRL, EUR, GBP, JPY, ARS, CNY, CAD, CHF, AUD
        \\ Comandos: cotacoes, historico, sair
        \\ Formato: <valor> <moeda_origem> <moeda_destino>
        \\ Exemplo: 100 BRL USD
        \\
    , .{});

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

        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, "cotacoes")) {
            try exibirCotacoes(stdout);
            continue;
        }

        if (mem.eql(u8, entrada, "historico")) {
            try stdout.print("\n  --- Histórico ---\n", .{});
            try historico.exibir(stdout);
            continue;
        }

        // Parse: "100 BRL USD"
        var partes = mem.splitSequence(u8, entrada, " ");

        const str_valor = partes.next() orelse {
            try stdout.print("  Formato: <valor> <origem> <destino>\n", .{});
            continue;
        };

        const valor = fmt.parseFloat(f64, str_valor) catch {
            try stdout.print("  Valor inválido: {s}\n", .{str_valor});
            continue;
        };

        if (valor <= 0) {
            try stdout.print("  O valor deve ser positivo.\n", .{});
            continue;
        }

        const cod_de = partes.next() orelse {
            try stdout.print("  Falta a moeda de origem.\n", .{});
            continue;
        };

        const cod_para = partes.next() orelse {
            try stdout.print("  Falta a moeda de destino.\n", .{});
            continue;
        };

        const moeda_de = buscarMoeda(cod_de) orelse {
            try stdout.print("  Moeda desconhecida: {s}\n", .{cod_de});
            continue;
        };

        const moeda_para = buscarMoeda(cod_para) orelse {
            try stdout.print("  Moeda desconhecida: {s}\n", .{cod_para});
            continue;
        };

        const resultado = converter(valor, moeda_de, moeda_para);
        try stdout.print("\n  {s} {d:.2} {s} = {s} {d:.2} {s}\n", .{
            moeda_de.simbolo, valor, moeda_de.codigo,
            moeda_para.simbolo, resultado, moeda_para.codigo,
        });

        // Taxa implícita
        const taxa = converter(1.0, moeda_de, moeda_para);
        try stdout.print("  (Taxa: 1 {s} = {d:.4} {s})\n", .{
            moeda_de.codigo, taxa, moeda_para.codigo,
        });

        historico.adicionar(moeda_de, moeda_para, valor, resultado);
    }

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

Passo 5: Testes

test "conversão USD para USD" {
    const usd = buscarMoeda("USD").?;
    const resultado = converter(100, usd, usd);
    try std.testing.expectApproxEqAbs(@as(f64, 100.0), resultado, 0.01);
}

test "conversão bidirecional" {
    const brl = buscarMoeda("BRL").?;
    const usd = buscarMoeda("USD").?;
    const em_usd = converter(100, brl, usd);
    const volta = converter(em_usd, usd, brl);
    try std.testing.expectApproxEqAbs(@as(f64, 100.0), volta, 0.1);
}

test "buscar moeda existente" {
    try std.testing.expect(buscarMoeda("BRL") != null);
    try std.testing.expect(buscarMoeda("EUR") != null);
}

test "buscar moeda inexistente" {
    try std.testing.expect(buscarMoeda("XYZ") == null);
}

test "arredondamento" {
    try std.testing.expectApproxEqAbs(@as(f64, 3.14), arredondar(3.14159, 2), 0.001);
    try std.testing.expectApproxEqAbs(@as(f64, 3.1), arredondar(3.14159, 1), 0.01);
}

Compilando e Executando

zig build test
zig build run

Conceitos Aprendidos

  • Arrays de structs constantes como banco de dados em memória
  • Busca linear em arrays tipados
  • Formatação numérica com casas decimais fixas
  • Arredondamento explícito para valores financeiros
  • Conversão transitiva (via moeda base)

Próximos Passos

Continue aprendendo Zig

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