Ruby do Zero: Enumerables e Coleções — map, select, group_by e sort

Capítulo 11

Tempo estimado de leitura: 8 minutos

+ Exercício

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 map quando quiser transformar cada item em outro.
  • Use select/reject quando quiser filtrar.
  • Use partition quando quiser separar em dois grupos (passa/não passa).
  • Use group_by quando quiser agrupar por uma chave.
  • Use sort/sort_by quando quiser ordenar.
  • Use uniq quando 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 p em precos, calcule p * 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.

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

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ço

uniq: 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: map no resultado do agrupamento.
  • Ordenação: sort_by.
  • Formatação final: map para 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]  } end

Passo 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 } end

Passo 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 salvar

Observações de legibilidade e desempenho nesse pipeline

  • Prefira sort_by com chave numérica (e sinal negativo para desc) em vez de sort com 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 partition para 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] } end

transform_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 nome com strip e preco para Float.
  • Filtre apenas itens com estoque > 0.
  • Ordene por preco crescente.
  • 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 partition para separar pagos e pendentes.
  • Para cada grupo, gere: quantidade e soma dos totais.
  • Ordene os pedidos pagos por total desc usando sort_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 uniq com bloco).
  • Agrupe por origem.
  • Ordene as origens por quantidade de leads desc.

Agora responda o exercício sobre o conteúdo:

Em um pipeline de dados, quando você precisa obter ao mesmo tempo os itens que passam e os que não passam em uma condição, qual método do Enumerable é mais indicado por percorrer a coleção uma única vez e retornar dois arrays?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

partition percorre a coleção uma vez e retorna dois arrays: o primeiro com os itens que passaram no teste e o segundo com os que falharam. É mais claro e eficiente do que fazer select e reject separados quando você precisa dos dois lados.

Próximo capitúlo

Ruby do Zero: Métodos — Parâmetros, Retorno, Argumentos Nomeados e Escopo

Arrow Right Icon
Capa do Ebook gratuito Ruby do Zero: Fundamentos, Coleções, Blocos e Organização de Código
52%

Ruby do Zero: Fundamentos, Coleções, Blocos e Organização de Código

Novo curso

21 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.