A Guide to Ruby Collections, II: Hashes, Sets, and Ranges

Robert Qualls
Robert Qualls
Share
collections_hashes

The first article in this series focused on Array and the basics of idiomatic Ruby iteration. Array is a pretty flexible class, but there are better solutions for particular situations. This article covers some of the other collection types that ship with Ruby.

Hashes

Sometimes you need to map one value to another. For example, you might want to map a product ID to an array containing information about that product. If the product IDs were all integers, you could do this with Array, but at the risk of wasting a lot of space in between IDs. Ruby hashes function as associative arrays where keys are not limited to integers. They are similar to Python’s dictionaries.

Creation

Like arrays, hashes have both literal and constructor initialization syntax.

>> colors = {}
>> colors['red'] = 0xff0000

>> colors = Hash.new
>> colors['red'] = 0xff0000

As with arrays, it’s possible to create a hash with starting values. This is where we get to see the idiomatic => (“hash rocket”) operator.

>> colors = {
>>   'red' => 0xff0000,
>>   'blue' => 0x0000ff
>> } 
=> {"red"=>16711680, "blue"=>255}

If you try to access a hash key with no value, it will return nil. You can change this default value by passing an argument to the constructor.

>> h = Hash.new
>> h[:blah]
=> nil

>> h = Hash.new(0)
>> h[:blah]
=> 0

Note that accessing a non-existent key does not create it.

>> h = {}
>> h.size
=> 0
>> h[:blah]
>> h.size
=> 0

Deletion

If you want to remove a key-pair from a hash, you can use #delete. It might be tempting to simply set the value at the key to nil like in Lua tables, but the key will still be part of the hash and thus included in iteration.

>> colors['red'] = nil
>> colors.size
=> 2

>> colors.delete('red')
>> colors.size 
=> 1

Iteration

Hashes are iterated like Arrays, except two values are passed to blocks instead of one.

>> hash = {"Juan" => 24, "Isidora" => 35}
>> hash.each { |name, age| puts "#{name}: #{age}" }
Juan: 24
Isidora: 35

Block variables like name and age in the previous example are just placeholders. They are arbitrary and could be anything, though it is good practice to make them descriptive.

To Hash Rocket or Not to Hash Rocket

It’s popular to use symbols as the Hash keys because they are descriptive like strings but fast like integers.

>> farm_counts = {
>>   :cow => 8,
>>   :chicken => 23,
>>   :pig => 11,
>> }
=> {:cow=>8, :chicken=>23, :pig=>11}

Starting with Ruby 1.9, hashes whose keys are symbols can be built sans hash rocket (=>), looking more like JavaScript or Python.

>> farm_counts = {
>>   cow: 8,
>>   chicken: 23,
>>   pig: 11
>> }
=> {:cow=>8, :chicken=>23, :pig=>11}

Both styles are common, but one thing to keep in mind is that all other key types still use a hash rocket, so using a colon instead in one part of your code might throw newcomers off.

Keyword Arguments With Hashes

Python provides the ability to call functions using keywords arguments With keywords, it isn’t necessary to pass arguments in a specific order or pass any particular arguments at all. Although Ruby technically does not provide keyword arguments, a hash can be used to simulate them. If a hash is the last argument in a method call, the curly braces can be left off.

>> class Person
>>   attr_accessor :first, :last, :weight, :height

>>   def initialize(params = {})
>>     @first = params[:first]
>>     @last = params[:last]
>>     @weight = params[:weight]
>>     @height = params[:height]   
>>   end
>> end

>> p = Person.new(
>>   height: 170cm,
>>   weight: 72,
>>   last: 'Doe',
>>   first: 'John'
>> )

Note that params = {} isn’t strictly necessary, but it protects your code from throwing an ArgumentError if no argument is passed, and it makes the intended argument type clearer.

Smaller Hashes with Array Fields

Someone got the bright idea of making a lighter hash out of the Array class.

$ gem install arrayfields

>> require 'arrayfields'
>> h = ArrayFields.new
>> h[:lunes] = "Monday"
>> h[:martes] = "Tuesday"
>> h.fields
=> [:lunes, :martes]
>> h.values
=> ["Monday", "Tuesday"]

I’m not ultra-familiar with the arrayfields gem or how it applies across different Ruby implementations, but it’s very popular on Ruby Toolbox, and if you’re going to be serializing a lot of Hash data, it’s probably worth checking out.

Sets

If you need a collection where the order does not matter, and the elements are guaranteed to be unique, then you probably want a set.

Unlike the other collection types, you must add a require statement to make use of the Set class.

>> require 'set'

Also, unlike Array and Hash, Set does not have any kind of special literal syntax. However, you can pass an Array to Set#new.

>> s = Set.new([1,2,3])
=> #<Set: {1, 2, 3}>

Alternatively, you can use Array#to_set.

>> [1,2,3,3].to_set
=> #<Set: {1, 2, 3}>

Set uses the << operator like Array, but #add is used instead of #push.

>> s = Set.new
>> s << 1
>> s.add 2

To remove an element from a set, use the #delete method.

>> s.delete(1)
=> #<Set: {2}>

As with Array, #include? can be used for membership testing.

>> s.include? 1
=> false
>> s.include? 2
=> true

One of the useful features of Set is that it will not add elements that it already includes.

>> s = Set.new [1,2]
=> #<Set: {1, 2}> 
>> s.add 2
=> #<Set: {1, 2}>

Earlier I pointed out that Array can perform boolean operations. Naturally, Set can do these as well.

>> s1 = [1,2,3].to_set
>> s2 = [2,3,4].to_set

>> s1 & s2
=> #<Set: {2, 3}>

>> s1 | s2
=> #<Set: {1, 2, 3, 4}>

It also can do exclusive-or operations with the ^ operator, unlike Array.

>> [1,2,3] ^ [2,3,4]
=> NoMethodError: undefined method `^' for [1, 2, 3]:Array

>> s1 ^ s2
=> #<Set: {4, 1}>

Ranges

I pointed out Ranges before in part I. The Range class is a sort of quasi-collection. It can be iterated like the other collections that use Enumerable, but it’s not a container for arbitrary elements.

>> r = Range.new('a', 'c')
=> 'a'..'c'
>> r.each { |i| puts i }
a
b
c

Before, I showed that Ranges can slice arrays or produce indices for iterating through them.

>> letters = [:a,:b,:c,:d,:e]
>> letters[1..3]
=> [:b, :c, :d]

>> (1..3).map { |i| letters[i].upcase }
=> [:B, :C, :D]

In addition to slicing arrays, Ranges can simplify case statement logic.

>> def theme(year)
>>   case year
>>     when 1970..1979 then "War Bad, Black People Not Bad"
>>     when 1980..1992 then "Cocaine, Money, and The Future"
>>     when 1993..2000 then "Gillian Anderson, Sitcoms in The FriendZone, and AOL"
>>     when 2000..2013 then "RIP, Music"
>>   end
>> end

>> theme(1987)
=> "Cocaine, Money, and The Future"

There is also this stackoverflow question about generating random strings which got some answers that put ranges to good use.

>> (0...10).map{ ('a'..'z').to_a[rand(26)] }.join
=> "vphkjxysly"

Conclusion

That covers Hashes, Sets, and Ranges. It the next post, I’ll discuss Enumerable, Enumerator, and the neat things you can do with such tools.

Frequently Asked Questions about Ruby Collections

What are the key differences between arrays and hashes in Ruby?

In Ruby, both arrays and hashes are used to store data, but they do so in different ways. An array is an ordered list of elements, where each element can be accessed by its index. On the other hand, a hash is a collection of key-value pairs, where each value can be accessed by its corresponding key. This makes hashes particularly useful when you want to associate specific values with certain keys.

How can I convert an array into a hash in Ruby?

Converting an array into a hash can be done using the to_h method in Ruby. This method transforms an array of arrays into a hash, where the first element of each sub-array becomes a key and the second element becomes the corresponding value. Here’s an example:

array = [[:key1, "value1"], [:key2, "value2"]]
hash = array.to_h

What are sets in Ruby and how do they differ from arrays and hashes?

A set in Ruby is a collection of unique elements. Unlike arrays and hashes, sets do not maintain any specific order of their elements and do not allow duplicates. This makes sets particularly useful when you need to perform operations like union, intersection, and difference on collections.

How can I create a range in Ruby and what can I use it for?

A range in Ruby is a sequence of values between a start value and an end value. You can create a range using the .. or ... operators. For example, 1..5 creates a range from 1 to 5. Ranges can be used in various ways, such as in loops, to create arrays, or to check if a value falls within a certain interval.

How can I iterate over a hash in Ruby?

You can iterate over a hash in Ruby using the each method, which passes each key-value pair to a block of code. Here’s an example:

hash = {key1: "value1", key2: "value2"}
hash.each do |key, value|
puts "#{key}: #{value}"
end

How can I add elements to a hash in Ruby?

You can add elements to a hash in Ruby by using the []= operator. You simply specify the key and assign a value to it. For example:

hash = {}
hash[:key] = "value"

How can I remove elements from a hash in Ruby?

You can remove elements from a hash in Ruby using the delete method. This method removes the key-value pair with the specified key and returns the value. If the key is not found, it returns nil. Here’s an example:

hash = {key: "value"}
hash.delete(:key)

How can I merge two hashes in Ruby?

You can merge two hashes in Ruby using the merge method. This method returns a new hash that combines the contents of the original hash and the other hash. If both hashes have the same key, the value from the other hash is used. Here’s an example:

hash1 = {key1: "value1"}
hash2 = {key2: "value2"}
merged_hash = hash1.merge(hash2)

How can I sort a hash in Ruby?

You can sort a hash in Ruby using the sort method. This method returns an array of arrays, where each sub-array is a key-value pair. The pairs are sorted by their keys. If you want to sort by values, you can use the sort_by method. Here’s an example:

hash = {b: 2, a: 1, c: 3}
sorted_hash = hash.sort.to_h

How can I check if a hash contains a certain key or value in Ruby?

You can check if a hash contains a certain key or value in Ruby using the has_key? or has_value? methods, respectively. These methods return true if the key or value is found, and false otherwise. Here’s an example:

hash = {key: "value"}
hash.has_key?(:key)
hash.has_value?("value")