Hash Tricks: Counting, Grouping, and Auto-Vivifying Like Perl
Hashes are the workhorse of text processing. Counting words, grouping log entries, building lookup tables. Perl spoiled us with auto-vivification where$hash{a}{b}{c} = 1 just works. Ruby needs a little setup, but once you know the tricks, it's just as powerful.
Part 1: Default Values for Counting
This is safe because 0 is immutable. The same value is returned for every missing key, but since you're replacing it with# Counting hash - default 0 counts = Hash.new(0) words.each { |w| counts[w] += 1 } # Perl: $counts{$word}++ (just works in Perl)
+=, it works fine.
Part 2: The Mutable Default Trap
Every key gets the same array object. Pushing to one pushes to all. This bug will cost you an hour of your life.# DON'T DO THIS - all keys share the same array object h = Hash.new([]) h[:a] << "item" h[:b] # => ["item"] - OOPS! Same array!
Part 3: Block Form (Safe for Mutables)
The block runs fresh for each new key. Problem solved.# Creates a NEW array for each missing key parties = Hash.new { |hash, key| hash[key] = [] } parties["Summer"] << "Joe" parties["Winter"] << "Jane" parties["Summer"] # => ["Joe"] - correct!
Part 4: Auto-Vivifying Nested Hashes
Perl does this natively. Ruby needs a one-liner trick:Why it works: the block sets each missing key to a new hash that uses the same block as its own default_proc. It's recursive default creation. Pure evil. Pure utility.# The magic one-liner def autoviv Hash.new { |h, k| h[k] = Hash.new(&h.default_proc) } end config = autoviv config[:database][:primary][:host] = "db1.example.com" config[:database][:primary][:port] = 5432 config[:database][:replica][:host] = "db2.example.com" # => {:database=>{:primary=>{:host=>"db1.example.com", :port=>5432}, # :replica=>{:host=>"db2.example.com"}}}
Part 5: Array to Hash Conversion
# Flat array to hash fruit = %w~apple red banana yellow~ Hash[*fruit] # => {"apple"=>"red", "banana"=>"yellow"} # Perl: my %hash = @array; (nearly identical!) # Array of pairs to hash pairs = [["apple", "red"], ["banana", "yellow"]] Hash[pairs] # => {"apple"=>"red", "banana"=>"yellow"} pairs.to_h # same thing (Ruby 2.1+)
Part 6: Zip to Hash
Combine parallel arrays into a hash:Perl equivalent:keys = [:host, :port, :user] values = ["localhost", 5432, "admin"] Hash[keys.zip(values)] # => {:host=>"localhost", :port=>5432, :user=>"admin"} # Or with to_h (Ruby 2.1+) keys.zip(values).to_h
my %hash; @hash{@keys} = @values; (hash slice assignment).
Part 7: Tally and Grouping
# Count occurrences (Ruby 2.7+) words = text.split counts = words.tally # => {"the"=>5, "quick"=>2, "brown"=>1, ...} # Group lines by first character groups = Hash.new { |h, k| h[k] = [] } ARGF.each_line do |line| groups[line[0]] << line.chomp end # Sort hash by value counts.sort_by { |k, v| -v }.each do |word, count| printf "%6d %s\n", count, word end
.tally is one of those methods that makes you wonder how you ever lived without it. One word replaces an entire counting loop.
Part 8: Useful Hash Methods
hash.key?(:foo) # key exists? (like Perl's exists) hash.value?(42) # value exists? hash.keys # array of keys hash.values # array of values hash.merge(other_hash) # combine two hashes hash.select { |k, v| v > 10 } # filter hash.reject { |k, v| v < 5 } # inverse filter hash.transform_values { |v| v.upcase } # modify all values hash.invert # swap keys and values
Created By: Wildcard Wizard. Copyright 2026