O que é Enumerable e por que ele “turbinam” coleções
Em Ruby, muitas estruturas que podem ser percorridas (como Array e Hash) expõem um conjunto de operações de alto nível para transformar, filtrar, agrupar e ordenar dados. Esse conjunto é o módulo Enumerable.
A ideia central é: se um objeto consegue fornecer itens um a um (isto é, “enumerar”), você ganha uma caixa de ferramentas enorme para trabalhar com coleções de forma expressiva. Em vez de controlar índices e acumuladores manualmente, você descreve a intenção: “mapear”, “selecionar”, “agrupar”, “ordenar”.
Regra prática
- Use
mapquando quiser transformar cada item em outro. - Use
select/rejectquando quiser filtrar. - Use
partitionquando quiser separar em dois grupos (passa/não passa). - Use
group_byquando quiser agrupar por uma chave. - Use
sort/sort_byquando quiser ordenar. - Use
uniqquando quiser remover duplicatas.
map: transformar itens (um para um)
map (também conhecido como collect) percorre a coleção e devolve um novo array com o resultado do bloco para cada item.
precos = [10.0, 25.5, 7.99] descontos = precos.map { |p| (p * 0.9).round(2) } # => [9.0, 22.95, 7.19]Passo a passo mental
- Para cada
pemprecos, calculep * 0.9. - Arredonde.
- Guarde o resultado na mesma posição do novo array.
map com Hash
Ao enumerar um Hash, cada item é um par [chave, valor] (ou você pode receber |k, v| no bloco).
estoque = { "maça" => 3, "banana" => 10 } linhas = estoque.map { |produto, qtd| "#{produto}: #{qtd}" } # => ["maça: 3", "banana: 10"]select e reject: filtrar mantendo ou removendo
select mantém os itens para os quais o bloco retorna verdadeiro. reject faz o inverso: remove os itens que passam no teste.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
idades = [12, 18, 21, 16] maiores = idades.select { |i| i >= 18 } # => [18, 21] menores = idades.reject { |i| i >= 18 } # => [12, 16]Dica de legibilidade
Prefira reject quando a condição “natural” é a de remoção. Evite negações duplas:
# Menos legível ativos = usuarios.select { |u| !u[:bloqueado] } # Mais legível ativos = usuarios.reject { |u| u[:bloqueado] }partition: separar em dois grupos de uma vez
partition percorre uma vez e devolve dois arrays: o primeiro com os itens que passaram no teste, o segundo com os que não passaram.
notas = [9.5, 4.0, 7.0, 6.9] aprovados, reprovados = notas.partition { |n| n >= 7.0 } # aprovados => [9.5, 7.0] # reprovados => [4.0, 6.9]Quando você precisa dos dois lados, partition costuma ser mais eficiente e mais claro do que fazer dois filtros separados.
group_by: agrupar por uma chave
group_by cria um Hash onde cada chave é o resultado do bloco, e o valor é um array com os itens daquele grupo.
palavras = %w[ruby rust ram rails] por_inicial = palavras.group_by { |w| w[0] } # => {"r"=>["ruby", "rust", "ram", "rails"]}Exemplo realista: agrupar vendas por categoria
vendas = [ { produto: "Teclado", categoria: "Periféricos", valor: 120.0 }, { produto: "Mouse", categoria: "Periféricos", valor: 80.0 }, { produto: "SSD", categoria: "Armazenamento", valor: 350.0 } ] por_categoria = vendas.group_by { |v| v[:categoria] } # => { # "Periféricos"=>[...], # "Armazenamento"=>[...] # }Depois de agrupar, você normalmente aplica transformações dentro de cada grupo (somar valores, contar itens, etc.).
sort e sort_by: ordenação com atenção a desempenho
sort: ordena comparando itens diretamente
sort ordena usando o operador de comparação (<=>) ou um bloco que devolve -1, 0, 1.
nums = [3, 1, 10, 2] nums.sort # => [1, 2, 3, 10]nomes = ["Ana", "joão", "Bruno"] nomes.sort { |a, b| a.downcase <=> b.downcase } # => ["Ana", "Bruno", "joão"]sort_by: ordena por uma “chave” calculada (geralmente melhor)
sort_by calcula uma chave para cada item e ordena por essa chave. Em muitos casos, é mais eficiente e mais legível do que sort com bloco, porque evita recalcular a mesma transformação várias vezes durante as comparações.
pessoas = [ { nome: "Ana", idade: 29 }, { nome: "Bruno", idade: 22 }, { nome: "Carla", idade: 35 } ] pessoas.sort_by { |p| p[:idade] } # => Bruno(22), Ana(29), Carla(35)Ordenação por múltiplos critérios
Uma técnica comum é retornar um array de chaves:
itens = [ { nome: "cabo", categoria: "A", preco: 10 }, { nome: "adaptador", categoria: "A", preco: 5 }, { nome: "case", categoria: "B", preco: 20 } ] itens.sort_by { |i| [i[:categoria], i[:preco]] } # categoria A antes de B, e dentro da categoria pelo preçouniq: remover duplicatas (com e sem bloco)
uniq remove itens repetidos. Com bloco, você define a “assinatura” usada para considerar duplicado.
tags = %w[ruby ruby rails Rails] tags.uniq # => ["ruby", "rails", "Rails"] tags.uniq { |t| t.downcase } # => ["ruby", "rails"]Compondo operações: pipelines de dados
O poder do Enumerable aparece quando você encadeia operações para transformar dados brutos em uma saída final clara. Um pipeline típico tem etapas:
- Entrada: uma lista (array) de registros (hashes).
- Limpeza/normalização:
map. - Filtragem:
select/reject. - Agrupamento:
group_by. - Agregação:
mapno resultado do agrupamento. - Ordenação:
sort_by. - Formatação final:
mappara strings.
Exemplo guiado: relatório de vendas por categoria
Vamos supor uma lista de vendas com possíveis inconsistências (categoria com espaços, valores como string, itens cancelados).
vendas = [ { produto: "Teclado", categoria: " Periféricos ", valor: "120.00", cancelada: false }, { produto: "Mouse", categoria: "Periféricos", valor: "80.00", cancelada: false }, { produto: "Cabo", categoria: "periféricos", valor: "10.00", cancelada: true }, { produto: "SSD", categoria: "Armazenamento", valor: "350.00", cancelada: false } ]Passo 1: normalizar campos com map
normalizadas = vendas.map do |v| { produto: v[:produto].strip, categoria: v[:categoria].strip.downcase, valor: v[:valor].to_f, cancelada: v[:cancelada] } endPasso 2: filtrar canceladas com reject
validas = normalizadas.reject { |v| v[:cancelada] }Passo 3: agrupar por categoria com group_by
por_categoria = validas.group_by { |v| v[:categoria] }Passo 4: agregar (somar e contar) e produzir linhas
Transforme o hash de grupos em uma lista de linhas com totais:
linhas = por_categoria.map do |categoria, itens| total = itens.sum { |i| i[:valor] } { categoria: categoria, qtd: itens.size, total: total } endPasso 5: ordenar por total desc e formatar saída
ordenadas = linhas.sort_by { |l| -l[:total] } saida = ordenadas.map do |l| "#{l[:categoria]} | #{l[:qtd]} vendas | R$ #{format('%.2f', l[:total])}" end # saida agora é um array de strings pronto para imprimir ou salvarObservações de legibilidade e desempenho nesse pipeline
- Prefira
sort_bycom chave numérica (e sinal negativo para desc) em vez desortcom bloco complexo. - Evite “blocos gigantes”: quando a etapa fica longa, extraia para um método (ex.:
normaliza_venda(v)). - Quando você precisa de dois grupos (ex.: válidas e canceladas), use
partitionpara percorrer uma vez.
Padrões úteis e armadilhas comuns
select + map vs map + select
Em geral, filtre antes de transformar quando isso reduzir o volume de dados cedo (melhor para desempenho e clareza). Mas se o filtro depende de um valor derivado, transforme primeiro.
# Filtra cedo (bom quando possível) ativos = usuarios.reject { |u| u[:bloqueado] } emails = ativos.map { |u| u[:email].downcase }group_by seguido de sort_by dentro de cada grupo
por_categoria = validas.group_by { |v| v[:categoria] } por_categoria_ordenado = por_categoria.transform_values do |itens| itens.sort_by { |i| -i[:valor] } endtransform_values é útil quando você quer manter as chaves e transformar apenas os valores do hash.
uniq antes ou depois?
Se duplicatas podem inflar o trabalho, aplique uniq cedo. Se a definição de duplicata depende de normalização (ex.: case-insensitive), normalize antes e depois aplique uniq com bloco.
emails = ["Ana@Ex.com", "ana@ex.com", "bruno@ex.com"] unicos = emails.uniq { |e| e.downcase } # => ["Ana@Ex.com", "bruno@ex.com"]Exercícios (pipeline de dados)
Exercício 1: limpeza, filtro e ordenação
Dada a lista:
produtos = [ { nome: " Teclado ", preco: "120.00", estoque: 10 }, { nome: "Mouse", preco: "80.00", estoque: 0 }, { nome: "Monitor", preco: "900.00", estoque: 3 } ]- Normalize
nomecomstripeprecoparaFloat. - Filtre apenas itens com
estoque > 0. - Ordene por
precocrescente. - Gere um array de strings no formato:
"NOME - R$ PRECO"com 2 casas decimais.
Exercício 2: partition + relatórios separados
Dada a lista:
pedidos = [ { id: 1, total: 150.0, pago: true }, { id: 2, total: 90.0, pago: false }, { id: 3, total: 200.0, pago: true } ]- Use
partitionpara separar pagos e pendentes. - Para cada grupo, gere: quantidade e soma dos totais.
- Ordene os pedidos pagos por
totaldesc usandosort_by.
Exercício 3: group_by + agregação + ranking
Dada a lista:
acessos = [ { usuario: "ana", pagina: "/home" }, { usuario: "ana", pagina: "/produtos" }, { usuario: "bruno", pagina: "/home" }, { usuario: "ana", pagina: "/home" }, { usuario: "bruno", pagina: "/contato" } ]- Agrupe por
usuario. - Para cada usuário, conte quantos acessos ele fez.
- Ordene o ranking por quantidade desc.
- Gere saída final como array de strings:
"usuario: X acessos".
Exercício 4: uniq com assinatura + group_by
Dada a lista:
leads = [ { email: "Ana@Ex.com", origem: "ads" }, { email: "ana@ex.com", origem: "evento" }, { email: "bruno@ex.com", origem: "ads" } ]- Remova duplicados por email ignorando maiúsculas/minúsculas (use
uniqcom bloco). - Agrupe por
origem. - Ordene as origens por quantidade de leads desc.