CrackedRuby logo

CrackedRuby

Hash Iteration

Complete guide to iterating over Hash objects in Ruby, covering core methods, advanced patterns, performance considerations, and common edge cases.

Core Built-in Classes Hash Class
2.5.3

Overview

Hash iteration in Ruby provides multiple methods for traversing key-value pairs, keys, or values in Hash objects. Ruby implements hash iteration through the Enumerable module mixed into Hash, along with hash-specific iteration methods that handle the dual nature of hash elements.

The Hash class includes dedicated iteration methods that return different aspects of the data structure. The #each method yields both key and value to the block, while #each_key and #each_value yield only keys or values respectively. Ruby maintains insertion order for hashes, ensuring iteration occurs in the sequence elements were added.

Hash iteration methods return enumerators when called without blocks, enabling method chaining with other Enumerable methods. The iteration process handles edge cases like empty hashes, nil values, and nested structures consistently across all methods.

hash = { name: "Alice", age: 30, city: "Boston" }

# Basic iteration over key-value pairs
hash.each { |key, value| puts "#{key}: #{value}" }
# name: Alice
# age: 30
# city: Boston

# Iteration returns enumerator without block
enumerator = hash.each
enumerator.next # => [:name, "Alice"]

Ruby's hash iteration integrates with the broader Enumerable ecosystem, allowing transformation operations like #map, #select, and #reject to work seamlessly with hash data. The iteration methods preserve the hash's internal structure while providing flexible access patterns for different programming scenarios.

Basic Usage

The #each method forms the foundation of hash iteration, yielding key-value pairs to the provided block. The method accepts blocks with one or two parameters - single parameter blocks receive a two-element array, while two-parameter blocks receive separate key and value arguments.

user_data = { id: 1001, username: "jdoe", email: "john@example.com", active: true }

# Two-parameter block syntax
user_data.each do |key, value|
  puts "Field #{key} has value: #{value}"
end

# Single-parameter block receives array
user_data.each do |pair|
  puts "Pair: #{pair.inspect}" # => [:id, 1001], [:username, "jdoe"], etc.
end

The #each_key method iterates over hash keys exclusively, passing only the key to the block. This method proves useful when key processing doesn't require corresponding values, such as validation or key transformation scenarios.

config = {
  database_host: "localhost",
  database_port: 5432,
  cache_enabled: true,
  log_level: "info"
}

# Process only keys
config.each_key do |setting|
  puts "Configuration setting: #{setting}"
end

# Chain with other enumerable methods
database_keys = config.each_key.select { |key| key.to_s.start_with?("database") }
# => [:database_host, :database_port]

The #each_value method iterates over hash values without providing keys to the block. This method works well for value-based operations like calculations, validation, or transformation where keys are irrelevant.

scores = { alice: 95, bob: 87, charlie: 92, diana: 98 }

# Calculate statistics from values only
total = 0
scores.each_value { |score| total += score }
average = total / scores.size # => 93

# Find maximum value
max_score = scores.each_value.max # => 98

Hash iteration methods return the original hash, enabling method chaining with other hash operations. The methods maintain consistent behavior across different Ruby versions and handle edge cases like empty hashes gracefully.

data = { a: 1, b: 2, c: 3 }

# Method chaining with iteration
result = data.each { |k, v| puts "#{k}=#{v}" }
             .merge(d: 4)
             .each_key { |key| puts "Key: #{key}" }

# Empty hash iteration
empty_hash = {}
empty_hash.each { |k, v| puts "Never executed" } # No output, returns {}

The iteration order follows insertion sequence, making hash iteration predictable for operations that depend on element ordering. This behavior remains consistent across hash modifications that don't involve key removal and re-addition.

Advanced Usage

Hash iteration combines with transformation methods to create complex data processing pipelines. The #each_with_index method provides access to both hash elements and their iteration position, enabling index-based logic within hash processing.

product_inventory = {
  laptops: { count: 15, price: 1200 },
  phones: { count: 8, price: 800 },
  tablets: { count: 12, price: 600 },
  watches: { count: 25, price: 300 }
}

# Complex transformation with index tracking
inventory_report = {}
product_inventory.each_with_index do |(product, details), index|
  total_value = details[:count] * details[:price]
  inventory_report[product] = {
    position: index + 1,
    stock: details[:count],
    unit_price: details[:price],
    total_value: total_value,
    category: index < 2 ? "high_value" : "standard"
  }
end

puts inventory_report[:laptops]
# => {:position=>1, :stock=>15, :unit_price=>1200, :total_value=>18000, :category=>"high_value"}

Nested hash iteration requires careful handling of multiple iteration levels. Ruby provides several approaches for traversing deeply nested hash structures, from recursive iteration to flattening strategies.

user_permissions = {
  admin: {
    system: [:read, :write, :execute],
    users: [:create, :read, :update, :delete],
    reports: [:generate, :export, :schedule]
  },
  editor: {
    content: [:create, :read, :update],
    media: [:upload, :organize],
    reports: [:generate]
  },
  viewer: {
    content: [:read],
    reports: [:view]
  }
}

# Recursive nested iteration
def flatten_permissions(perms, role_prefix = "")
  result = []
  perms.each do |key, value|
    current_path = role_prefix.empty? ? key.to_s : "#{role_prefix}.#{key}"
    if value.is_a?(Hash)
      result.concat(flatten_permissions(value, current_path))
    elsif value.is_a?(Array)
      value.each { |permission| result << "#{current_path}:#{permission}" }
    else
      result << "#{current_path}:#{value}"
    end
  end
  result
end

all_permissions = {}
user_permissions.each do |role, permissions|
  all_permissions[role] = flatten_permissions(permissions)
end

puts all_permissions[:admin].first(3)
# => ["system:read", "system:write", "system:execute"]

Hash iteration supports lazy evaluation through enumerator chaining, enabling memory-efficient processing of large hash datasets. The lazy evaluation defers computation until results are actually needed.

large_dataset = (1..1000).each_with_object({}) do |i, hash|
  hash["key_#{i}"] = { value: i * 2, category: i % 10 }
end

# Lazy evaluation chain
filtered_results = large_dataset.lazy
  .each
  .select { |key, data| data[:category] == 0 }
  .map { |key, data| [key, data[:value]] }
  .first(5)

puts filtered_results
# => [["key_10", 20], ["key_20", 40], ["key_30", 60], ["key_40", 80], ["key_50", 100]]

Custom iteration patterns can be implemented using #each as a foundation, creating domain-specific iteration methods that encapsulate complex traversal logic.

class ConfigHash < Hash
  def each_required
    return enum_for(:each_required) unless block_given?
    
    each do |key, value|
      if key.to_s.end_with?('_required') || value.is_a?(Hash) && value[:required]
        yield(key, value)
      end
    end
    self
  end
  
  def each_nested_key(separator = '.')
    return enum_for(:each_nested_key, separator) unless block_given?
    
    flatten_keys = lambda do |hash, prefix = ''|
      hash.each do |key, value|
        full_key = prefix.empty? ? key.to_s : "#{prefix}#{separator}#{key}"
        if value.is_a?(Hash)
          flatten_keys.call(value, full_key)
        else
          yield(full_key, value)
        end
      end
    end
    
    flatten_keys.call(self)
    self
  end
end

config = ConfigHash.new
config.merge!({
  database: { host: 'localhost', port_required: 5432, credentials: { user: 'app', password_required: 'secret' } },
  cache_required: { enabled: true, ttl: 3600 }
})

config.each_required { |key, value| puts "Required: #{key} = #{value}" }
# Required: cache_required = {:enabled=>true, :ttl=>3600}
# Required: port_required = 5432

config.each_nested_key { |key, value| puts "#{key} => #{value}" }
# database.host => localhost
# database.port_required => 5432
# database.credentials.user => app
# database.credentials.password_required => secret
# cache_required.enabled => true
# cache_required.ttl => 3600

Common Pitfalls

Hash modification during iteration creates unpredictable behavior that can lead to skipped elements, infinite loops, or runtime errors. Ruby's hash implementation doesn't provide protection against concurrent modification, making this a frequent source of bugs.

# Dangerous: modifying hash during iteration
user_scores = { alice: 85, bob: 92, charlie: 78, diana: 95, eve: 88 }

# WRONG: This can skip elements or cause errors
user_scores.each do |name, score|
  if score < 80
    user_scores.delete(name) # Modifies hash during iteration
  end
end

# CORRECT: Collect keys to remove first
to_remove = []
user_scores.each { |name, score| to_remove << name if score < 80 }
to_remove.each { |name| user_scores.delete(name) }

# ALTERNATIVE: Use reject! which handles modification safely
user_scores.reject! { |name, score| score < 80 }

Block parameter mismatches occur when developers confuse single-parameter and two-parameter block syntax. This leads to unexpected array assignments instead of key-value destructuring.

data = { name: "John", age: 30, city: "Boston" }

# WRONG: Single parameter receives array, not key
data.each do |key|
  puts "Key: #{key}" # => Key: [:name, "John"], Key: [:age, 30], etc.
end

# CORRECT: Use two parameters for key-value pairs
data.each do |key, value|
  puts "#{key}: #{value}" # => name: John, age: 30, etc.
end

# CORRECT: Single parameter with explicit array handling
data.each do |pair|
  key, value = pair
  puts "#{key}: #{value}"
end

Nil value handling in hash iteration can cause unexpected behavior when values are nil or when keys don't exist. Ruby treats nil as a valid hash value, which can interfere with logic that assumes falsy values indicate missing data.

user_data = { name: "Alice", age: nil, city: "", country: false }

# WRONG: This skips nil and false values unintentionally  
user_data.each do |key, value|
  next unless value # Skips age, country
  puts "#{key}: #{value}"
end

# CORRECT: Explicitly check for nil if that's the intent
user_data.each do |key, value|
  next if value.nil?
  puts "#{key}: #{value}"
end

# CORRECT: Handle different falsy values appropriately
user_data.each do |key, value|
  case value
  when nil
    puts "#{key}: not provided"
  when false
    puts "#{key}: disabled"
  when ""
    puts "#{key}: empty"
  else
    puts "#{key}: #{value}"
  end
end

Enumerator confusion arises when developers expect hash iteration methods to return arrays instead of the original hash. This leads to incorrect chaining and unexpected results in method pipelines.

scores = { math: 95, science: 87, english: 92 }

# WRONG: each returns the hash, not an array
result = scores.each { |subject, score| score > 90 } # Returns original hash
puts result.class # => Hash

# WRONG: Chaining expects array-like behavior
high_scores = scores.each { |subject, score| score > 90 }.compact # NoMethodError

# CORRECT: Use map for transformation
high_score_subjects = scores.map { |subject, score| subject if score > 90 }.compact
# => [:math, :english]

# CORRECT: Use select for filtering  
high_scores = scores.select { |subject, score| score > 90 }
# => {:math=>95, :english=>92}

# CORRECT: Use each for side effects, chain other methods
scores.each { |subject, score| puts "#{subject}: #{score}" }
      .select { |subject, score| score > 90 }
      .keys
# => [:math, :english]

Symbol versus string key iteration can create subtle bugs when hash keys have mixed types or when string interpolation creates different key types than expected.

# Mixed key types cause iteration confusion
mixed_hash = { "name" => "Alice", :age => 30, "city" => "Boston" }

# This only finds string keys
string_keys = mixed_hash.each_key.select { |key| key.is_a?(String) }
# => ["name", "city"]

# WRONG: Assuming all keys are symbols
mixed_hash.each do |key, value|
  puts key.upcase # NoMethodError if key is string
end

# CORRECT: Handle mixed key types
mixed_hash.each do |key, value|
  normalized_key = key.to_s.upcase
  puts "#{normalized_key}: #{value}"
end

Reference

Core Iteration Methods

Method Parameters Returns Description
#each(&block) Block with 1-2 params Hash Iterates over key-value pairs
#each_key(&block) Block with 1 param Hash Iterates over keys only
#each_value(&block) Block with 1 param Hash Iterates over values only
#each_pair(&block) Block with 2 params Hash Alias for #each
#each_with_index(&block) Block with 2 params Hash Iterates with index counter

Enumerable Integration Methods

Method Parameters Returns Description
#map(&block) Block with 1-2 params Array Transforms elements to array
#select(&block) Block with 1-2 params Hash Filters elements by condition
#reject(&block) Block with 1-2 params Hash Excludes elements by condition
#find(&block) Block with 1-2 params Array or nil Returns first matching pair
#any?(&block) Block with 1-2 params Boolean Tests if any element matches
#all?(&block) Block with 1-2 params Boolean Tests if all elements match

Transformation Methods

Method Parameters Returns Description
#transform_keys(&block) Block with 1 param Hash Transforms keys, preserves values
#transform_values(&block) Block with 1 param Hash Transforms values, preserves keys
#filter_map(&block) Block with 1-2 params Array Maps and filters nil results
#group_by(&block) Block with 1-2 params Hash Groups elements by block result
#partition(&block) Block with 1-2 params Array[Hash, Hash] Splits into matching/non-matching

Block Parameter Patterns

Pattern Syntax Receives Use Case
Two parameters `{ key, value ... }`
Single parameter `{ pair ... }`
Index tracking `{ (key, value), index ... }`
Destructuring `{ key, (nested_key, nested_value) ... }`

Enumerator Behavior

Without Block Returns Chainable With
hash.each Enumerator Any Enumerable method
hash.each_key Enumerator Array-producing methods
hash.each_value Enumerator Value-processing methods
hash.each_with_index Enumerator Index-aware methods

Common Return Values

Operation Type Method Examples Returns Chainable
Iteration #each, #each_key Original hash Yes
Transformation #map, #collect Array No
Filtering #select, #reject New hash Yes
Searching #find, #detect Single element/nil No
Testing #any?, #all? Boolean No

Performance Characteristics

Method Time Complexity Space Complexity Memory Usage
#each O(n) O(1) Constant
#each_key O(n) O(1) Constant
#each_value O(n) O(1) Constant
#map O(n) O(n) New array allocation
#select O(n) O(k) where k ≤ n New hash allocation
#transform_keys O(n) O(n) New hash allocation

Edge Case Handling

Scenario Behavior Safe Methods
Empty hash No block execution, returns self All iteration methods
Nil values Treated as valid values All iteration methods
Mixed key types Preserves original types Type-agnostic methods
Concurrent modification Undefined behavior #select!, #reject!
Large datasets Memory proportional to size Lazy enumerators

Error Conditions

Error Type Cause Prevention
NoMethodError Wrong parameter count in block Match block parameters to method
FrozenError Modifying frozen hash during iteration Check frozen state first
SystemStackError Infinite recursion in nested iteration Implement cycle detection
ArgumentError Invalid enumerator operations Validate enumerator state