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
- Aprenda sobre formatação numérica avançada
- Para taxas em tempo real, explore requisições HTTP
- Construa o próximo projeto: Quiz no Terminal