A couple words on Arrays in Ruby
- 24 September 2017
- Ruby
- A couple words on Arrays in Ruby
We all work with arrays on daily basis. It's quite easy to start using arrays, no problem with that. But there are some interesting methods and behavior that I wanted to cover in this post.
Let's start with this one:
arr = [1, 2, 3]
arr[9] = 'foo'
p arr # => [1, 2, 3, nil, nil, nil, nil, nil, nil, "foo"]
Ruby adjusted the length of the array and filled missing elements with nil
.
Array creation
arr = Array.new # => []
Works the same as arr = []
.
We can define size and the default value for Array using the following code:
arr = Array.new(3, 'foo') # => ["foo", "foo", "foo"]
arr[6] = 'bar'
p arr # => ["foo", "foo", "foo", nil, nil, nil, "bar"]
As we can see, Ruby assigned "foo" value to all elements of the Array during initialization. But when it needed to adjust the length of the Array, Ruby used nil
as a default value.
We can pass a block to generate values of Array:
arr = Array.new(3) { |i| "item-#{i}" }
p arr # => ["item-0", "item-1", "item-2"]
There is one gotcha with default values for arrays though. Because the same default value will be used for all elements of an array we might have problem like this:
arr = Array.new(2, Hash.new)
p arr # => [{}, {}]
arr[0][:foo] = 'bar'
p arr # => [{:foo=>"bar"}, {:foo=>"bar"}]
It's interesting how changes to the first element of array changed the second one too. That's because they refer to the same hash.
To solve this problem we should instantiate new Hash for each element of Array:
arr = Array.new(2) { Hash.new }
p arr # => [{}, {}]
arr[0][:foo] = 'bar'
p arr # => [{:foo=>"bar"}, {}]
Now each element of this array refers to the unique array.
If you want to wrap some value into an array, you can use:
arr = Array(3) # => [3]
arr = Array([1,2,3]) # => [1,2,3]
That helps when you want to be sure that you're dealing with arrays.
Slicing
Array
includes Enumerable
module, so we have all nifty methods there, like: find
, map
, inject
, etc.
For slicing, we can use method slice
or []
, they work the same way.
arr = [1,2,3,4]
arr[0] # => 1
arr[20] # => nil
It also accepts second parameter length:
arr = [1,2,3,4]
arr[0, 2] # => [1, 2]
We can read it as: take 2 elements from Array starting from the element with the index of 0
.
It accepts ranges too:
arr = [1,2,3,4]
p arr[0..2] # => [1, 2, 3]
All these cases return new arrays, but if you want to change original array, there is a slice!
method.
arr = [1, 2, 3]
arr.slice!(1, 2) # => [2, 3]
p arr # => [1]
Just wanted to mention another method called values_at
that takes an array of indices and returns array that consists of only those elements.
Inserting
Ok, what if we want to insert values from one array into another:
arr = [1,2,3]
arr.insert(1, [10, 20])
p arr # => [1, [10, 20], 2, 3]
It inserted [10, 20]
array as a second value of the arr
. If we want to insert just values, there are couple options:
arr.insert(1, [10, 20]).flatten! # => [1, 10, 20, 2, 3]
Probably the less efficient way, just wanted to mention flatten!
here :)
Then, we can use splat operator. I like this approach:
arr = [1,2,3]
arr.insert(1, *[10, 20])
p arr # => [1, 10, 20, 2, 3]
To substitute one or more values by values from another array, we can use this code:
arr = [1,2,3]
arr[0..0] = [10, 20]
p arr # => [10, 20, 2, 3]
Comparing arrays
To compare arrays we can use "spaceship" operator <=>
. Documentation says:
Comparison — Returns an integer (-1, 0, or +1) if this array is less than, equal to, or greater than other_ary.
It's a little bit tricky, so let's figure out how that actually works.
When comparing arrays, Ruby goes element by element, for example:
arr = [1,2,3]
another_arr = [1,2,4]
arr <=> another_arr # => -1
Ruby compared: 1 from arr
to 1 from another_arr
, then 2 to 2, and then 3 is less than 4, so it returned -1
which means that arr < another_arr
.
If each element is the same in both arrays it returns 0
:
arr = [1, 2, 3]
another_arr = [1, 2, 3]
arr <=> another_arr # => 0
If any element of the first array is greater than the corresponding element in the another array, it returns +1:
arr = [1, 5, 3]
another_arr = [1, 2, 3]
arr <=> another_arr # => 1 (5 is greater than 2)
What if elements of arrays are not comparable? Let's see:
arr = [1, 5, 3]
another_arr = [1, "test", 3]
arr <=> another_arr # => nil
If Ruby cannot compare elements, it returns nil
.
What if a size of arrays is different?
arr = [1, 2, 3]
another_arr = [1, 2, 3, 4]
p arr <=> another_arr # => -1
If the first array size is less than the size of another one, it returns -1
.
If the first one is bigger, it returns +1
:
arr = [1, 2, 3, 4, 5]
another_arr = [1, 2]
arr <=> another_arr # => 1
Union |
and Intersection &
Arrays can contain non-unique values, for example:
arr = [1, 1, 2, 2]
We can leave just unique values using uniq!
method.
Sometimes we need to make sure that when we merge arrays we merge just unique values. For example:
arr = ["foo", "bar"]
arr2 = ["bar", "baz"]
arr + arr2 # => ["foo", "bar", "bar", "baz"]
"bar" is duplicated in this case.
If we don't want to have that duplicated "bar" in resulting array, we can use union method:
arr = ["foo", "bar"]
arr2 = ["bar", "baz"]
p arr | arr2 # => ["foo", "bar", "baz"]
Set Union — Returns a new array by joining ary with other_ary, excluding any duplicates and preserving the order from the given arrays.
If we want to know just intersection between those arrays, we can use &
method:
arr = ["foo", "bar"]
arr2 = ["bar", "baz"]
p arr & arr2 # => ["bar"]
It allows us to check if one array includes all values of another array:
arr = ["foo", "bar", "baz"]
arr2 = ["bar", "baz"]
p (arr & arr2) == arr2 # => true
There is another way to do the same:
(arr2 - arr).empty? # => true
Ruby has Sets, which is a better fit for all these needs because Set:
Set implements a collection of unordered values with no duplicates. This is a hybrid of Array's intuitive inter-operation facilities and Hash's fast lookup.
It has methods like: subset?
, superset?
, intersect?
, intersections
, etc.
It's easy to convert an array into a set and get all functionality we need:
require 'set'
set = ["foo", "bar", "baz"].to_set
set2 = ["bar", "baz"].to_set
p set.superset?(set2) # => true
p set2.subset?(set) # => true
p set.intersect?(set2) # => true
p set.intersection set2 # => #<Set: {"bar", "baz"}>
Random
I was going through some exercises and there was the following task:
ary = []
ary << 1 && false
true || ary << 2
false && ary << 3
false || ary << 4
p ary
It's more about flow, rather than Arrays, but still interesting. What do you think this code returns? Let me know in comments if your suggestion was right :)
Thanks for reading!