A Quick Intro to Ruby's Set Collection, Part 1: Creating Sets, and Adding/Removing Objects
Tags: ruby, tutorial, set, datastructures
Published: 2019-10-13 21:06:47

---
title: "A Quick Intro to Ruby's Set Collection, Part 1: Creating Sets, and Adding/Removing Objects"
published: true
description: a quick intro to ruby sets: creating sets, adding objects, and removing objects
tags: ruby, tutorial, set, datastructures
series: "A Quick Intro to Ruby's Set"
---

Recently, both my personal projects and code challenges have involved situations for collecting a unique list of objects. My usual go-to strategy is a combination of Ruby's array collection, plus the Array.uniq method.

This strategy works, but isn't always **optimal** based on the situation. For instance, it's helpful that Array.uniq will remove duplicates while also maintaining object order--but that's only helpful *if object order mattered in the first place!*

Luckily, there are alternatives! Ruby has a Set collection that functions very similarly to an array, but with a few crucial differences:

* All objects in a Set are **guaranteed unique** *(arrays can have duplicates)*
* Objects in a Set are **not** ordered *(arrays are ordered by index)*
* Sets are built on top of Hashes for **super-fast object lookup** *(arrays are just dynamic arrays under-the-hood)*

Or, [summarized by RubyGuides' article on the Set class:](https://www.rubyguides.com/2018/08/ruby-set-class/)

> A set is a class that stores items like an array…
> 
> But with some special attributes that make it  **10x faster**  in specific situations!
>
> On top of that:
> 
> All the items in a set are  **guaranteed to be unique**.

10x faster, huh? Sounds good to me!


# Overview

In this first article, we'll introduce the following basics of Ruby's Set class:

1. How to create a set
2. Adding objects to sets
3. Removing objects from sets


## Use case: Turning text into a set of unique one-word strings

For our use case, we'll be using one that I encountered in a personal project: **turning a long string into a set of unique one-word strings.**

My Dungeons & Dragons character creation tool, [Friendly Character Generator](https://friendly-character-generator.herokuapp.com/), scans a newly-generated character for one-word search tags that are then used to find matching backstory snippets in the database. 

For instance, the character Big Sword Knight has a skill that contains the word "armor": 

> "When you choose this domain at 1st level, you gain proficiency with heavy armor."

The database has the following backstory snippet, which should be found by querying the search tag "armor":

>"Due to their obsession with infinite levels of constitution and unkillability, they sleep (and shower) in their full armor, and they tend to reek--remember to do laundry before any stealth missions!"

So, to accomplish this, we need to turn the string *“When you choose this domain at 1st level, you gain proficiency with heavy armor.”* into a set of unique, one-word strings that we can use to query the database.

Why is a Set appropriate here? There's a few advantages:

1. **Uniqueness** -- since we want to query the database for a given word one time only, the set guarantees we won't repeat any words (even if we add more words to it later!)
2.  **Efficient Lookups** -- if we *do* need to add more words, it is extremely fast to check if one (or more!) words already exist within the set
3. **Unordered** -- the order of words we query doesn't matter, so no need for an array's ordering!

## Sidenote: Text normalization

I really enjoy string manipulation, so I decided to use some fancy Regex and basic string methods to normalize the string--removing punctuation, separating hyphenated words, making everything lowercase, etc. 

For funsies, here's the code used for text normalization:

```ruby
string = "When you choose this domain at 1st level, you gain proficiency with heavy armor."

regex1 = /(-)|(--)|(\.\.\.)|(_)/          # separate words joined by hyphens, m-dashes, underscores, and ellipses
regex2 = /([.,:;?!"'`@#$%^&*()+={}-])/    # remove all other punctuation
string.gsub!(regex1, " ")
string.gsub!(regex2, "")
string.downcase!

puts string
#=> "when you choose this domain at 1st level you gain proficiency with heavy armor"
```


# How to create a set

## require 'set'

Set is an odd collection because Ruby makes us explicitly require it:

`require 'set'`


## Start with an array

After text normalization, let's assume we start with this string:

`string = "when you choose this domain at 1st level you gain proficiency with heavy armor"`

The most direct way to create a set is by converting an array, so let's go ahead and and .split(" ") the string where there are spaces:

```ruby 
array = string.split(" ")
#=> ["when", "you", "choose", "this", "domain", "at", "1st", "level",
#    "you", "gain", "proficiency", "with", "heavy", "armor"]
```

Now we have few ways to create a set from this array:


## Set.new(array)

We can use the `Set` class's `.new` method, and pass our array into it:
```ruby
set = Set.new(array)
#=> #<Set: {"when", "you", "choose", "this", "domain", "at", "1st", 
#   "level", "gain", "proficiency", "with", "heavy", "armor"}>
```
Voila! The duplicate string "you" only appears once in our new set--and we can rest assured that no more duplicates can be added!

If no argument is passed, an empty set will be created.


## array.to_set

We can also use the enumerable method `.to_set` on our array to create a new set:
```ruby
set = array.to_set
#=> #<Set: {"when", "you", "choose", "this", "domain", "at", "1st",
#   "level", "gain", "proficiency", "with", "heavy", "armor"}>
```
Super-easy, same result!


## Set[]

The last way we can create a set is by simply writing out the array with `Set` prepended to it:

```ruby
Set["when", "you", "choose", "this", "domain", "at", "1st", "level",
	"you", "gain", "proficiency", "with", "heavy", "armor"]
#=> #<Set: {"when", "you", "choose", "this", "domain", "at", "1st",
#   "level", "gain", "proficiency", "with", "heavy", "armor"}>
```
But, in our case, we're violating the DRY doctrine by literally rewriting the array! This obviously works better for situations where you want to start by writing out the Set by hand.

## Now we can enumerate with .each!

Once we have our `set`  created, we can use `.each` to cycle through the objects, just like with arrays! The main difference is that **.each will pass the objects in a RANDOM order**--since the Set class is built on top of Hash, its contents will not be passed in a predictable order, and we don't have access to an index integer.

```ruby
set.each do |str|
    puts str
end

#=> when
#	you
#	choose
#	this
#	domain
#	at
#	1st
#	level
#	gain
#	proficiency
#	with
#	heavy
#	armor
```

It may *look* ordered, but we can't always guarantee that particular order. [From the Ruby docs on Sets](https://ruby-doc.org/stdlib-2.6.5/libdoc/set/rdoc/Set.html), emphasis mine:

> Set implements a collection of **unordered** values with no duplicates.

So, just don't count on that same order happening again.


# Adding objects to sets

Let's say we want to add the name of our character, Big Sword Knight, to our set. We have the name stored as three normalized, one-word strings in an array:

`name_array = ["big", "sword", "knight"]`

Now we want to add each of these to our set. We can add them one-by-one, or all-at-once. We also have an option to return `nil` if a given object *cannot* be added, due to being a duplicate.

_**NOTE: We could actually replace our `name_array` with another set!** Since we can use the `.each` method on both arrays and sets, it wouldn't break the code we're about to write. **BUT**, we're using an array just to easily differentiate the objects._


## .add

The most straightforward way is to use `.add` to add an object to our set:
```ruby
set.add("knight")
#=> #<Set: {"when", "you", "choose", "this", "domain", "at", "1st",
#  "level", "gain", "proficiency", "with", "heavy", "armor", "knight"}>
```


We can also iterate through `name_array` and repeat the operation with each string it contains:
```ruby
name_array.each do |str|
	set.add(str)
end
#=> #<Set: {"when", "you", "choose", "this", "domain", "at", "1st",
#   "level", "gain", "proficiency", "with", "heavy", "armor", "big",
#   "sword", "knight"}>
```

In the console output, we can see that the new strings appear at the end of the set--**BUT REMEMBER THAT THESE OBJECTS ARE NOT ORDERED!**

We can also use `.add` to add the entire array, as sets can contain different object types:

```ruby
set.add(name_array)
#=> #<Set: {"when", "you", "choose", "this", "domain", "at", "1st", 
#   "level", "gain", "proficiency", "with", "heavy", "armor", 
#   ["big", "sword", "knight"]}>
```

Bam! Whole array in the set. If you iterate through sets containing different types, remember to build in handling for that!


## << (shovel)

The same behavior as `.add` can be achieved with its alias, the shovel `<<` operator:

```ruby
name_array.each do |str|
	set << str
end
#=> #<Set: {"when", "you", "choose", "this", "domain", "at", "1st",
#   "level", "gain", "proficiency", "with", "heavy", "armor", "big",
#   "sword", "knight"}>
```

**NOTE:** In both the `.add` and `<<` methods, **nothing happens** (and nothing is returned) when an object **cannot** be added to a set.


## .merge

You can simplify the `.each` loop to add individual objects from another collection to the set by using `.merge`. This will pass the objects one-by-one, and is a lot more readable. 

```ruby
set.merge(name_array)
#=> #<Set: {"when", "you", "choose", "this", "domain", "at", "1st", 
#   "level", "gain", "proficiency", "with", "heavy", "armor", "big", 
#   "sword", "knight"}>
```

Just like `.add` and `<<` above, `.merge` will **NOT** return anything from failing to add an object.


## .add?

In contrast to `.add`, `<<`, and `merge` methods, `.add?` will return `nil` if an object **cannot** be added to a set:

```ruby
set.add?("armor")
#=> nil
```

This is is especially useful if you need to do things like keep track of how many objects are rejected from the set:

```ruby
duplicate_counter = 0
duplicate_array = ["proficiency", "with", "heavy", "armor"]

duplicate_array.each do |str|
    if set.add?(str) == nil
        duplicate_counter += 1
    end
end

puts duplicate_counter
#=> 4
```





## Note: These methods mutate the original set!

All of the above methods will mutate the original set by changing its contents. This differentiates the methods from other [Set pipe method](https://ruby-doc.org/stdlib-2.6.5/libdoc/set/rdoc/Set.html#method-i-7C) `|` (also aliased as `+` and `.union`), which will **make a copy of the set to mutate and return**, while the original set remains unchanged.


# Removing objects from sets

Now, let's say we want to remove some words unrelated to our characters backstory, like prepositions and pronouns:

`remove_array = ["when", "you", "this", "at", "with"]`

## .delete

`.delete` is the opposite of `.add`, and will remove the object passed to it from the set (if it exists):
```ruby
set.delete("when")
#=> #<Set: {"you", "choose", "this", "domain", "at", "1st", "level", 
#   "gain", "proficiency", "with", "heavy", "armor"}>
```

We can also iterate through `remove_array` to repeat the `.delete` operation:
```ruby
remove_array.each do |str|
    set.delete(str)
end
#=> #<Set: {"choose", "domain", "1st", "level", "gain", "proficiency", 
#   "heavy", "armor"}>
```

As with `.add`, calling `.delete` on a collection like an array will try to delete the array **as a whole, matching object** and not by its individual contents:

```ruby
set.delete(remove_array)
#=> #<Set: {"when", "you", "choose", "this", "domain", "at", "1st",
#   "level", "gain", "proficiency", "with", "heavy", "armor"}>
```

Unfortunately, there is no equivalent shovel for `.delete`. 
:'(

## .subtract

`.subtract` is the opposite of `.merge`, and will simplify the `.each` loop by calling `.delete` on each object within a passed collection:

```ruby
set.substract(remove_array)
#=> #<Set: {"choose", "domain", "1st", "level", "gain", "proficiency", 
#   "heavy", "armor"}>
```
Keep in mind, `.delete` and `.subtract` will **not return anything** if a given object **cannot** be removed from the set.

## .delete?

`.delete?` will return `nil` if an object **cannot** be removed from the set:

```ruby
set.delete?("shield")
#=> nil
```

Y'know, if you *like that sort of thing.*


## Note: These methods mutate the original set, too!

Just like the addition methods above, these deletion methods will all mutate the original set. For methods that **make a copy of the set to mutate and return**, see the [Set "minus" method](https://ruby-doc.org/stdlib-2.6.5/libdoc/set/rdoc/Set.html#method-i-2D) `-` (also aliased as [`.difference`](https://ruby-doc.org/stdlib-2.6.5/libdoc/set/rdoc/Set.html#method-i-difference)).

# Conclusion

Now you're ready to create some sets in your Ruby code! You can practice by looking for opportunities to use sets instead of arrays in your own code. The main two questions you can ask are:

* **Do the objects need to be unique?**
* **Does the object order matter?**


In future articles, we'll continue by looking into:
1. Object lookup in sets
2. Checking for subsets within a set
3. Splitting a set into subsets with the Set.divide method

## Links and Sources

[Excellent introduction to Ruby Sets by Al Scott](https://spin.atomicobject.com/2012/09/04/when-is-a-set-better-than-an-array-in-ruby/)

[Ruby docs - Sets](https://ruby-doc.org/stdlib-2.6.5/libdoc/set/rdoc/Set.html)

[RubyGuides intro to Sets](https://www.rubyguides.com/2018/08/ruby-set-class/)

[DotNetPerls - Ruby Set examples](https://www.dotnetperls.com/set-ruby)


### Got any tips, tricks, or instances where you like to use sets? Please feel free to share in the comments below!