iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Hashes

A Ruby hash is an unordered (well, insertion-ordered since 1.9) key/value collection. Rich API: each, map, merge, dig, transform_keys. Symbols are the canonical key type.

Create, access, transform, deep merge

EXAMPLE
# 1) Create
user = { name: 'Ada', email: 'a@x.com', age: 32 }
empty = {}

# String keys (older syntax)
old = { 'name' => 'Ada', 'email' => 'a@x.com' }

# Mixed
mixed = { name: 'Ada', 'phone' => '555-1234' }

# Shorthand (Ruby 3.1+) — variable name matches key
name = 'Ada'; email = 'a@x.com'
short = { name:, email: }              # { name: 'Ada', email: 'a@x.com' }

# 2) Access
user[:name]                            # 'Ada'
user[:missing]                         # nil
user.fetch(:name)                      # 'Ada' (raises KeyError if missing)
user.fetch(:missing, 'default')        # 'default'
user.fetch(:missing) { |k| "no #{k}" } # 'no missing'

# 3) Modify
user[:role] = 'admin'                  # add
user[:age] = 33                        # update
user.delete(:age)                      # remove + return value
user.delete_if { |_, v| v.nil? }       # conditional delete

# 4) Iterate
user.each { |k, v| puts "#{k}: #{v}" }
user.each_pair { |k, v| puts "#{k}: #{v}" }
user.each_key  { |k| puts k }
user.each_value { |v| puts v }

# 5) Transform
user.map { |k, v| [k, v.to_s] }.to_h    # all values to strings
user.transform_values(&:to_s)            # cleaner — Ruby 2.4+
user.transform_keys(&:to_s)              # symbol keys → string keys
user.transform_keys { |k| k.upcase }     # custom

# Filter
user.select { |k, v| v.is_a?(String) }   # keep where block true
user.reject { |_, v| v.nil? }            # drop where block true
user.filter_map { |k, v| v if v.is_a?(String) }   # filter + map

# Reduce
user.reduce(0) { |sum, (_, v)| sum + v.to_s.length }
user.sum { |k, v| v.to_s.length }       # Ruby 2.4+

# 6) Keys + values
user.keys                               # [:name, :email, :age]
user.values                             # ['Ada', 'a@x.com', 32]
user.size                                # 3
user.length                              # alias for size
user.empty?                              # false
user.any?                                # true
user.any? { |_, v| v.is_a?(Integer) }   # true
user.all? { |_, v| !v.nil? }            # true

# 7) Membership
user.key?(:name)                        # true
user.has_key?(:name)                    # alias
user.include?(:name)                    # alias
user.value?(32)                         # true

# 8) Merge
a = { x: 1, y: 2 }
b = { y: 20, z: 3 }

a.merge(b)                              # { x: 1, y: 20, z: 3 } — b wins
b.merge(a)                              # { y: 2, z: 3, x: 1 }  — a wins

# Block to resolve conflicts
a.merge(b) { |key, va, vb| va + vb }    # { x: 1, y: 22, z: 3 }

# In-place
a.merge!(b)                             # mutates a
a.update(b)                             # alias

# 9) Deep merge (Rails / ActiveSupport)
require 'active_support/core_ext/hash/deep_merge'

a = { user: { name: 'Ada', age: 32 } }
b = { user: { email: 'a@x.com' } }
a.deep_merge(b)
# => { user: { name: 'Ada', age: 32, email: 'a@x.com' } }

# 10) dig — nested access without nil checks
user = { profile: { address: { city: 'Sydney' } } }
user.dig(:profile, :address, :city)     # 'Sydney'
user.dig(:profile, :missing, :city)     # nil — no error

# 11) slice + except
user = { name: 'Ada', age: 32, email: 'a@x.com', role: 'admin' }
user.slice(:name, :email)               # { name: 'Ada', email: 'a@x.com' }
user.except(:role, :age)                # { name: 'Ada', email: 'a@x.com' } (Rails / Ruby 3+)

# 12) Default values
h = Hash.new(0)
h[:missing]                             # 0
h[:foo] += 1                            # 1
h[:foo] += 1                            # 2

# Or with a block (different defaults per key)
h = Hash.new { |hash, key| hash[key] = [] }
h[:items] << 'a'                        # creates empty array, appends
h[:items] << 'b'
h                                       # { items: ['a', 'b'] }

# Or default proc that sets dynamically
h = Hash.new { |hash, key| key.to_s.upcase }
h[:hello]                               # 'HELLO'

# 13) Group + count
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
numbers.group_by(&:even?)               # { false: [1, 3, ...], true: [2, 4, ...] }
numbers.tally                           # { 1 => 1, 2 => 1, ... } — counts

['a', 'b', 'a', 'c', 'a'].tally         # { 'a' => 3, 'b' => 1, 'c' => 1 }

# 14) Convert between arrays and hashes
h = { a: 1, b: 2 }
h.to_a                                   # [[:a, 1], [:b, 2]]
h.flatten                                # [:a, 1, :b, 2]
h.flatten(2)                             # [:a, 1, :b, 2]

# From an array
[[:a, 1], [:b, 2]].to_h                 # { a: 1, b: 2 }
[:a, :b].zip([1, 2]).to_h               # { a: 1, b: 2 }

# 15) Iterating with index
user.each_with_index do |(k, v), i|
    puts "#{i}: #{k} = #{v}"
end

# 16) Sorting
user.sort_by { |k, v| v.to_s }          # array of [k, v] pairs
user.min_by  { |_, v| v.to_s.length }   # smallest
user.max_by  { |_, v| v.to_s.length }   # largest

# 17) Nested hash patterns
body = {
    user: {
        id: 42,
        profile: {
            name: 'Ada',
            roles: ['admin', 'user'],
        },
    },
    posts: [
        { id: 1, title: 'Hi' },
        { id: 2, title: 'Hello' },
    ],
}

body.dig(:user, :profile, :name)        # 'Ada'
body.dig(:user, :profile, :roles, 0)    # 'admin'
body.dig(:posts, 0, :title)             # 'Hi'

# 18) Symbol vs string keys — convention
# - Symbol keys for code-side data (configs, options)
# - String keys for external data (JSON, params, headers)
# - HashWithIndifferentAccess (Rails) when input may be either:
require 'active_support/core_ext/hash/indifferent_access'
h = { 'name' => 'Ada' }.with_indifferent_access
h[:name]                                 # 'Ada'
h['name']                                # 'Ada'

# 19) Pattern matching (Ruby 3+)
case user
in { name: String => name, email: String => email }
    puts "#{name} <#{email}>"
in { name: }
    puts "name only: #{name}"
in {}
    puts "empty"
end

# Array of hashes
case rows
in [{ id: Integer => first_id, * }, *_]
    puts "first id: #{first_id}"
end

# 20) JSON serialisation
require 'json'
user.to_json                            # '{"name":"Ada","email":"a@x.com","age":32}'
JSON.parse('{"name":"Ada"}')            # { 'name' => 'Ada' } — STRING keys
JSON.parse('{"name":"Ada"}', symbolize_names: true)   # { name: 'Ada' }

# 21) Hash#freeze — immutable
const = { code: 200, status: 'OK' }.freeze
# const[:code] = 500  → FrozenError

# 22) Common bugs
#   • String keys vs symbol keys mismatch → undefined access
#   • Mutating during iteration → undefined behaviour
#   • Default values shared by reference: Hash.new([]) — same array! Use Hash.new { |h, k| h[k] = [] }
#   • h[key] += 1 fails for missing keys → use Hash.new(0)
#   • Comparing hashes with == checks each key/value (NOT reference)

# 23) Performance
# - Hash lookup O(1) average
# - Insertion order preserved (since 1.9)
# - Use symbols (or integers) for hot-path keys — strings allocate
# - For frozen string keys, freeze them via String#freeze or use 'literal'.freeze

# 24) Best practices
#   ✅ Symbols for in-code data; strings for external
#   ✅ Use fetch with defaults when key is required
#   ✅ dig for nested access
#   ✅ transform_keys / transform_values for type conversions
#   ✅ Pattern matching for structured destructuring (Ruby 3+)
#   ✅ Freeze constants intended as immutable

Why it matters

Symbols for code keys, strings for external input. dig, fetch, transform_keys/values, slice/except, and tally cover almost every hash transformation cleanly.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
user = { name: 'Ada', age: 36 }
puts user[:name]
user[:role] = 'admin'
user.each { |k, v| puts "#{k} = #{v}" }
Try it Yourself »

Discussion

Loading…