Skip to content

Ruby Hashes

Hashes are another important data structure in Ruby, used to store key-value pair collections. Hashes are similar to arrays but use keys instead of indices to access elements. Ruby hashes are powerful and flexible, supporting multiple key types and rich operation methods. This chapter will introduce in detail the creation, manipulation, and processing methods of hashes in Ruby.

🎯 Hash Basics

Hash Definition

Ruby provides multiple ways to create hashes:

ruby
# Use literal syntax (Ruby 1.9+ recommended way)
person = {
  name: "Zhang San",
  age: 25,
  city: "Beijing"
}

# Traditional way using => symbol
person = {
  :name => "Zhang San",
  :age => 25,
  :city => "Beijing"
}

# Use strings as keys
person = {
  "name" => "Zhang San",
  "age" => 25,
  "city" => "Beijing"
}

# Use Hash.new to create empty hash
empty_hash = Hash.new
default_hash = Hash.new("default")  # Set default value

# Use Hash[] method to create
fruits = Hash["Apple", 5, "Banana", 3, "Orange", 8]
# Or use array pairs
pairs = [["Apple", 5], ["Banana", 3], ["Orange", 8]]
fruits = Hash[pairs]

# Use Hash.new block to set default
counter = Hash.new { |hash, key| hash[key] = 0 }
counter["Apple"] += 1
puts counter["Apple"]  # 1
puts counter["Banana"]  # 0 (auto initialized)

Hash Access

ruby
person = {
  name: "Zhang San",
  age: 25,
  city: "Beijing",
  skills: ["Ruby", "Python", "JavaScript"]
}

# Access value by key
puts person[:name]     # Zhang San
puts person[:age]      # 25
puts person[:skills]   # ["Ruby", "Python", "JavaScript"]

# Use string key to access
data = {
  "name" => "Li Si",
  "age" => 30
}
puts data["name"]  # Li Si

# Use fetch method (safer)
puts person.fetch(:name)        # Zhang San
puts person.fetch(:salary, 0)   # 0 (default value)
# person.fetch(:salary)         # Raises KeyError exception

# Use fetch with block to handle missing keys
salary = person.fetch(:salary) { |key| "#{key} not set" }
puts salary  # salary not set

# Check if key exists
puts person.key?(:name)     # true
puts person.key?(:salary)   # false
puts person.has_key?(:name) # true (alias for key?)

puts person.value?("Zhang San")   # true
puts person.has_value?("Zhang San") # true (alias for value?)

📏 Hash Properties

Hash Size and Comparison

ruby
person = {
  name: "Zhang San",
  age: 25,
  city: "Beijing"
}

# Get hash size
puts person.length  # 3
puts person.size    # 3 (alias for length)
puts person.count   # 3

# Check if hash is empty
puts person.empty?  # false
puts Hash.new.empty?  # true

# Compare hashes
hash1 = {a: 1, b: 2}
hash2 = {a: 1, b: 2}
hash3 = {b: 2, a: 1}  # Different order but same content

puts hash1 == hash2  # true
puts hash1 == hash3  # true (hash comparison doesn't consider key order)
puts hash1.eql?(hash2)  # true

# Object identity comparison
puts hash1.equal?(hash2)  # false
puts hash1.equal?(hash1)  # true

➕ Hash Operations

Adding and Modifying Key-Value Pairs

ruby
person = {
  name: "Zhang San",
  age: 25
}

# Add new key-value pair
person[:city] = "Beijing"
person[:skills] = ["Ruby", "Python"]

# Use store method
person.store(:email, "zhangsan@example.com")

# Batch update
person.merge!({
  age: 26,
  department: "Engineering"
})

puts person.inspect
# {:name=>"Zhang San", :age=>26, :city=>"Beijing", :skills=>["Ruby", "Python"], :email=>"zhangsan@example.com", :department=>"Engineering"}

# Use merge to create new hash without modifying original
new_person = person.merge({title: "Engineer"})
puts person.key?(:title)   # false (original hash unchanged)
puts new_person.key?(:title)  # true (new hash has title key)

Deleting Key-Value Pairs

ruby
person = {
  name: "Zhang San",
  age: 25,
  city: "Beijing",
  salary: 10000,
  department: "Engineering"
}

# Delete specified key
deleted_value = person.delete(:salary)
puts deleted_value  # 10000
puts person.key?(:salary)  # false

# Delete key-value pairs satisfying condition
person.delete_if { |key, value| key == :department }
puts person.key?(:department)  # false

# Keep key-value pairs satisfying condition
filtered_person = person.keep_if { |key, value| key != :age }
puts person  # {:name=>"Zhang San", :city=>"Beijing"} (original hash modified)

# Clear hash
person.clear
puts person.inspect  # {}

Update and Replace

ruby
# Use update method (alias for merge!)
original = {a: 1, b: 2}
additional = {b: 3, c: 4}
original.update(additional)
puts original  # {:a=>1, :b=>3, :c=>4} (b's value updated)

# Use block to handle duplicate keys
hash1 = {a: 1, b: 2}
hash2 = {b: 3, c: 4}
result = hash1.merge(hash2) { |key, old_val, new_val| old_val + new_val }
puts result  # {:a=>1, :b=>5, :c=>4} (b's values added)

# Conditional update
person = {name: "Zhang San", age: 25}
person[:age] = 26 if person[:age] < 30
puts person[:age]  # 26

🔍 Hash Search and Filter

Find Key-Value Pairs

ruby
scores = {
  "Zhang San" => 85,
  "Li Si" => 92,
  "Wang Wu" => 78,
  "Zhao Liu" => 96
}

# Find first key-value pair satisfying condition
high_scorer = scores.find { |name, score| score > 90 }
puts high_scorer  # ["Li Si", 92]

# Find all key-value pairs satisfying condition
high_scorers = scores.select { |name, score| score > 85 }
puts high_scorers  # {"Li Si"=>92, "Zhao Liu"=>96}

# Find key-value pairs not satisfying condition
low_scorers = scores.reject { |name, score| score > 85 }
puts low_scorers  # {"Zhang San"=>85, "Wang Wu"=>78}

# Find keys or values
names_with_high_scores = scores.keys.select { |name| scores[name] > 85 }
puts names_with_high_scores  # ["Li Si", "Zhao Liu"]

scores_above_threshold = scores.values.select { |score| score > 85 }
puts scores_above_threshold  # [92, 96]

Hash Filtering and Grouping

ruby
# Use slice to get sub-hash of specified keys
person = {
  name: "Zhang San",
  age: 25,
  city: "Beijing",
  salary: 10000,
  department: "Engineering"
}

basic_info = person.slice(:name, :age, :city)
puts basic_info  # {:name=>"Zhang San", :age=>25, :city=>"Beijing"}

# Use except to exclude specified keys
detailed_info = person.except(:salary, :department)
puts detailed_info  # {:name=>"Zhang San", :age=>25, :city=>"Beijing"}

# Grouping
students = [
  {name: "Zhang San", grade: "A", subject: "Math"},
  {name: "Li Si", grade: "B", subject: "Math"},
  {name: "Wang Wu", grade: "A", subject: "English"},
  {name: "Zhao Liu", grade: "C", subject: "English"}
]

grouped_by_grade = students.group_by { |student| student[:grade] }
puts grouped_by_grade
# {"A"=>[{:name=>"Zhang San", :grade=>"A", :subject=>"Math"}, {:name=>"Wang Wu", :grade=>"A", :subject=>"English"}],
#  "B"=>[{:name=>"Li Si", :grade=>"B", :subject=>"Math"}],
#  "C"=>[{:name=>"Zhao Liu", :grade=>"C", :subject=>"English"}]}

# Group by key
data = {a1: 1, a2: 2, b1: 3, b2: 4}
grouped_by_prefix = data.group_by { |key, value| key.to_s[0] }
puts grouped_by_prefix
# {"a"=>[[:a1, 1], [:a2, 2]], "b"=>[[:b1, 3], [:b2, 4]]}

🔧 Hash Transformation

Mapping and Conversion

ruby
prices = {
  "Apple" => 5.0,
  "Banana" => 3.0,
  "Orange" => 4.0
}

# Transform keys
symbol_keys = prices.transform_keys { |key| key.to_sym }
puts symbol_keys  # {:Apple=>5.0, :Banana=>3.0, :Orange=>4.0}

# Transform values
discount_prices = prices.transform_values { |price| price * 0.9 }
puts discount_prices  # {"Apple"=>4.5, "Banana"=>2.7, "Orange"=>3.6}

# Transform both keys and values
formatted_data = prices.transform_keys(&:upcase).transform_values { |price| "$#{price}" }
puts formatted_data  # {"Apple"=>"$5.0", "Banana"=>"$3.0", "Orange"=>"$4.0"}

# In-place transformation
prices.transform_keys! { |key| key.to_sym }
puts prices  # {:Apple=>5.0, :Banana=>3.0, :Orange=>4.0}

Sorting and Reversing

ruby
# Sort by key
data = {c: 3, a: 1, b: 2}
sorted_by_key = data.sort
puts sorted_by_key.inspect  # [[:a, 1], [:b, 2], [:c, 3]]

sorted_hash = data.sort.to_h
puts sorted_hash  # {:a=>1, :b=>2, :c=>3}

# Sort by value
sorted_by_value = data.sort_by { |key, value| value }
puts sorted_by_value.inspect  # [[:a, 1], [:b, 2], [:c, 3]]

# Sort by key in descending order
desc_sorted = data.sort { |a, b| b[0] <=> a[0] }
puts desc_sorted.to_h  # {:c=>3, :b=>2, :a=>1}

# Invert key-value pairs
inverted = data.invert
puts inverted  # {1=>:a, 2=>:b, 3=>:c}
# Note: If values have duplicates, data will be lost after inversion

🔁 Hash Iteration

Basic Iteration

ruby
person = {
  name: "Zhang San",
  age: 25,
  city: "Beijing"
}

# Iterate key-value pairs
person.each { |key, value| puts "#{key}: #{value}" }
# name: Zhang San
# age: 25
# city: Beijing

# Iterate only keys
person.each_key { |key| puts "Key: #{key}" }
# Key: name
# Key: age
# Key: city

# Iterate only values
person.each_value { |value| puts "Value: #{value}" }
# Value: Zhang San
# Value: 25
# Value: Beijing

# Use each_pair (alias for each)
person.each_pair { |key, value| puts "#{key} => #{value}" }

Advanced Iteration

ruby
scores = {
  "Zhang San" => 85,
  "Li Si" => 92,
  "Wang Wu" => 78,
  "Zhao Liu" => 96
}

# Use map to process key-value pairs
grade_messages = scores.map { |name, score| "#{name}'s score is #{score} points" }
puts grade_messages.inspect
# ["Zhang San's score is 85 points", "Li Si's score is 92 points", "Wang Wu's score is 78 points", "Zhao Liu's score is 96 points"]

# Use with_object to accumulate results
total_and_count = scores.each_with_object({sum: 0, count: 0}) do |(name, score), acc|
  acc[:sum] += score
  acc[:count] += 1
end
average = total_and_count[:sum].to_f / total_and_count[:count]
puts "Average score: #{average}"  # Average score: 87.75

# Use reduce for accumulation
sum = scores.reduce(0) { |total, (name, score)| total + score }
puts "Total score: #{sum}"  # Total score: 351

🎯 Hash Practice Examples

Configuration Manager

ruby
class ConfigManager
  def initialize(defaults = {})
    @config = defaults.dup
  end
  
  # Get config value
  def get(key, default = nil)
    @config.key?(key) ? @config[key] : default
  end
  
  # Set config value
  def set(key, value)
    @config[key] = value
  end
  
  # Batch set config
  def update(config_hash)
    @config.merge!(config_hash)
  end
  
  # Delete config item
  def remove(key)
    @config.delete(key)
  end
  
  # Check if config item exists
  def exists?(key)
    @config.key?(key)
  end
  
  # Get all config items
  def all
    @config.dup
  end
  
  # Reset config
  def reset(defaults = {})
    @config = defaults.dup
  end
  
  # Deep merge config (handle nested hashes)
  def deep_merge(other_hash)
    deep_merge_hash(@config, other_hash)
  end
  
  private
  
  def deep_merge_hash(hash1, hash2)
    hash2.each do |key, value|
      if hash1.key?(key) && hash1[key].is_a?(Hash) && value.is_a?(Hash)
        hash1[key] = deep_merge_hash(hash1[key], value)
      else
        hash1[key] = value
      end
    end
    hash1
  end
end

# Use configuration manager
defaults = {
  database: {
    host: "localhost",
    port: 5432,
    username: "user"
  },
  logging: {
    level: "info",
    file: "app.log"
  }
}

config = ConfigManager.new(defaults)
puts config.get(:database)  # {:host=>"localhost", :port=>5432, :username=>"user"}

# Update config
config.update({
  database: {
    host: "production.db.com",
    password: "secret"
  },
  cache: {
    enabled: true,
    ttl: 3600
  }
})

puts config.get(:database)
# {:host=>"production.db.com", :port=>5432, :username=>"user", :password=>"secret"}

Data Statistical Analyzer

ruby
class DataAnalyzer
  def initialize(data)
    @data = data
  end
  
  # Calculate frequency distribution
  def frequency
    freq = Hash.new(0)
    @data.each { |item| freq[item] += 1 }
    freq
  end
  
  # Calculate percentage distribution
  def percentage
    freq = frequency
    total = @data.length
    freq.transform_values { |count| (count.to_f / total * 100).round(2) }
  end
  
  # Find most common elements
  def most_common(n = 1)
    frequency.sort_by { |key, value| -value }.first(n).to_h
  end
  
  # Find least common elements
  def least_common(n = 1)
    frequency.sort_by { |key, value| value }.first(n).to_h
  end
  
  # Statistical analysis of numeric data
  def numeric_stats
    return {} unless @data.all? { |item| item.is_a?(Numeric) }
    
    {
      count: @data.length,
      sum: @data.sum,
      mean: @data.sum.to_f / @data.length,
      min: @data.min,
      max: @data.max,
      range: @data.max - @data.min
    }
  end
  
  # Grouped statistics
  def group_by(&block)
    @data.group_by(&block)
  end
  
  # Conditional filtering
  def filter(&block)
    @data.select(&block)
  end
end

# Use data analyzer
# String data analysis
words = ["apple", "banana", "apple", "cherry", "banana", "apple", "date"]
analyzer = DataAnalyzer.new(words)

puts "Frequency distribution: #{analyzer.frequency}"
# Frequency distribution: {"apple"=>3, "banana"=>2, "cherry"=>1, "date"=>1}

puts "Percentage distribution: #{analyzer.percentage}"
# Percentage distribution: {"apple"=>42.86, "banana"=>28.57, "cherry"=>14.29, "date"=>14.29}

puts "Most common 3: #{analyzer.most_common(3)}"
# Most common 3: {"apple"=>3, "banana"=>2, "cherry"=>1}

# Numeric data analysis
scores = [85, 92, 78, 96, 88, 91, 83, 89]
score_analyzer = DataAnalyzer.new(scores)

puts "Numeric statistics: #{score_analyzer.numeric_stats}"
# Numeric statistics: {:count=>8, :sum=>702, :mean=>87.75, :min=>78, :max=>96, :range=>18}

📚 Next Steps

After mastering Ruby hash operations, it is recommended to continue learning:

Continue your Ruby learning journey!

Content is for learning and research only.