Inicio / Ruby / Ruby: Lenguaje Elegante y Expresivo / Enumerables y Colecciones Avanzadas

Enumerables y Colecciones Avanzadas

Enumerable a fondo, transformación, filtrado, reducción, lazy enumerators y Set.

Intermedio
🔒 Solo lectura
📖

Estás en modo lectura

Puedes leer toda la lección, pero para marcar progreso, hacer ejercicios y ganar XP necesitas una cuenta Pro.

Desbloquear por $9/mes

Enumerables y Colecciones Avanzadas

El módulo Enumerable es una de las joyas de Ruby. Proporciona métodos de iteración y transformación potentes.


Enumerable en profundidad

Cualquier clase que implemente each e incluya Enumerable obtiene +50 métodos:

class Rango
  include Enumerable

  def initialize(desde, hasta)
    @desde = desde
    @hasta = hasta
  end

  def each
    current = @desde
    while current <= @hasta
      yield current
      current += 1
    end
  end
end

r = Rango.new(1, 5)
puts r.map { |n| n * 2 }.inspect    # [2, 4, 6, 8, 10]
puts r.select(&:odd?).inspect        # [1, 3, 5]
puts r.reduce(:+)                    # 15

Métodos de transformación

numeros = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# map / collect — transformar cada elemento
cuadrados = numeros.map { |n| n ** 2 }
# [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

# flat_map — map + flatten
oraciones = ["hola mundo", "ruby es genial"]
palabras = oraciones.flat_map { |s| s.split }
# ["hola", "mundo", "ruby", "es", "genial"]

# filter_map (Ruby 2.7+) — map + compact
emails = ["ana@test.com", "", "bob@test.com", nil, ""]
validos = emails.filter_map { |e| e&.strip unless e.nil? || e.empty? }
# ["ana@test.com", "bob@test.com"]

# zip — combinar arrays
nombres = ["Ana", "Bob", "Carlos"]
edades = [28, 35, 42]
nombres.zip(edades)
# [["Ana", 28], ["Bob", 35], ["Carlos", 42]]

# each_slice — dividir en grupos
(1..10).each_slice(3).to_a
# [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]]

# each_cons — ventana deslizante
[1, 2, 3, 4, 5].each_cons(3).to_a
# [[1, 2, 3], [2, 3, 4], [3, 4, 5]]

Métodos de filtrado

numeros = [15, 3, 42, 7, 28, 91, 6, 53]

# select / filter
pares = numeros.select(&:even?)          # [42, 28, 6]

# reject
impares = numeros.reject(&:even?)        # [15, 3, 7, 91, 53]

# partition — divide en dos arrays
pares, impares = numeros.partition(&:even?)
# pares: [42, 28, 6], impares: [15, 3, 7, 91, 53]

# find / detect — primer match
primero = numeros.find { |n| n > 20 }    # 42

# find_index
indice = numeros.find_index { |n| n > 20 }  # 2

# take_while / drop_while
[1, 3, 5, 2, 4].take_while(&:odd?)      # [1, 3, 5]
[1, 3, 5, 2, 4].drop_while(&:odd?)      # [2, 4]

# grep — filtrar por patrón (usa ===)
["ruby", "rails", "python", "react"].grep(/r/)
# ["ruby", "rails", "react"]

[1, "dos", 3, "cuatro"].grep(Integer)    # [1, 3]
[1, "dos", 3, "cuatro"].grep(String)     # ["dos", "cuatro"]

Métodos de reducción

numeros = [1, 2, 3, 4, 5]

# reduce / inject
suma = numeros.reduce(0) { |acc, n| acc + n }   # 15
prod = numeros.reduce(1) { |acc, n| acc * n }   # 120

# Con symbol
suma = numeros.reduce(:+)    # 15
prod = numeros.reduce(:*)    # 120

# Construir un hash con reduce
palabras = ["ruby", "es", "genial", "ruby", "es"]
frecuencia = palabras.reduce(Hash.new(0)) do |hash, palabra|
  hash[palabra] += 1
  hash
end
# {"ruby"=>2, "es"=>2, "genial"=>1}

# tally (Ruby 2.7+) — atajo para frecuencia
frecuencia = palabras.tally
# {"ruby"=>2, "es"=>2, "genial"=>1}

# each_with_object — como reduce pero más claro
frecuencia = palabras.each_with_object(Hash.new(0)) do |palabra, hash|
  hash[palabra] += 1
end

Métodos de verificación

numeros = [2, 4, 6, 8]

numeros.all?(&:even?)      # true — ¿todos cumplen?
numeros.any?(&:odd?)       # false — ¿alguno cumple?
numeros.none?(&:odd?)      # true — ¿ninguno cumple?
numeros.one? { |n| n > 7 } # true — ¿exactamente uno?
numeros.count(&:even?)     # 4 — ¿cuántos cumplen?
numeros.include?(4)        # true — ¿contiene?

Métodos de ordenación

# sort
[3, 1, 4, 1, 5].sort                    # [1, 1, 3, 4, 5]
[3, 1, 4, 1, 5].sort { |a, b| b <=> a } # [5, 4, 3, 1, 1]

# sort_by (más eficiente para transformaciones)
personas = [
  { nombre: "Carlos", edad: 42 },
  { nombre: "Ana", edad: 28 },
  { nombre: "Bob", edad: 35 }
]

personas.sort_by { |p| p[:edad] }
# Ana(28), Bob(35), Carlos(42)

personas.sort_by { |p| [-p[:edad], p[:nombre]] }
# Carlos(42), Bob(35), Ana(28) — descendente por edad

# min, max, minmax
[3, 1, 4, 1, 5].min           # 1
[3, 1, 4, 1, 5].max           # 5
[3, 1, 4, 1, 5].minmax        # [1, 5]

# min_by, max_by
personas.min_by { |p| p[:edad] }   # Ana
personas.max_by { |p| p[:edad] }   # Carlos

Lazy Enumerators

Para colecciones infinitas o muy grandes:

# Sin lazy: evalúa TODO antes de tomar
# (1..Float::INFINITY).select(&:odd?).first(5)  # ¡nunca termina!

# Con lazy: evalúa bajo demanda
(1..Float::INFINITY)
  .lazy
  .select(&:odd?)
  .map { |n| n ** 2 }
  .first(5)
# [1, 9, 25, 49, 81]

# Generador Fibonacci
fib = Enumerator.new do |yielder|
  a, b = 0, 1
  loop do
    yielder.yield a
    a, b = b, a + b
  end
end

puts fib.lazy.select(&:odd?).first(10).inspect
# [1, 1, 3, 5, 13, 21, 55, 89, 233, 377]

Hash avanzado

datos = { a: 1, b: 2, c: 3, d: 4, e: 5 }

# transform_keys / transform_values
datos.transform_keys(&:to_s)         # {"a"=>1, "b"=>2, ...}
datos.transform_values { |v| v * 10 } # {a: 10, b: 20, ...}

# slice — subconjunto
datos.slice(:a, :c)                  # {a: 1, c: 3}

# except (Ruby 3.0+) — excluir keys
datos.except(:a, :b)                 # {c: 3, d: 4, e: 5}

# select / reject en hashes
datos.select { |k, v| v > 2 }       # {c: 3, d: 4, e: 5}

# group_by
personas = [
  { nombre: "Ana", dept: "IT" },
  { nombre: "Bob", dept: "HR" },
  { nombre: "Carlos", dept: "IT" }
]

por_depto = personas.group_by { |p| p[:dept] }
# {"IT" => [{nombre: "Ana"...}, {nombre: "Carlos"...}], "HR" => [...]}

# chunk — agrupa consecutivos
[1, 1, 2, 2, 2, 3, 1, 1].chunk { |n| n }.to_a
# [[1, [1, 1]], [2, [2, 2, 2]], [3, [3]], [1, [1, 1]]]

Set

require 'set'

s1 = Set.new([1, 2, 3, 4])
s2 = Set.new([3, 4, 5, 6])

s1 & s2         # Set[3, 4] (intersección)
s1 | s2         # Set[1, 2, 3, 4, 5, 6] (unión)
s1 - s2         # Set[1, 2] (diferencia)
s1 ^ s2         # Set[1, 2, 5, 6] (diferencia simétrica)

s1.add(5)        # Set[1, 2, 3, 4, 5]
s1.include?(3)   # true
s1.subset?(s2)   # false

Resumen

Método Descripción
map / flat_map Transformar elementos
select / reject Filtrar
find Primer match
reduce Acumular
tally Contar frecuencias
partition Dividir en dos
group_by Agrupar por criterio
sort_by Ordenar por transformación
lazy Evaluación diferida
grep Filtrar por patrón
🔒

Ejercicio práctico disponible

Enumerable: transformaciones avanzadas

Desbloquear ejercicios
// Enumerable: transformaciones avanzadas
// Desbloquea Pro para acceder a este ejercicio
// y ganar +50 XP al completarlo

function ejemplo() {
    // Tu código aquí...
}

¿Te gustó esta lección?

Con Pro puedes marcar progreso, hacer ejercicios, tomar quizzes, ganar XP y obtener tu constancia.

Ver planes desde $9/mes