#---Array---
Arrays are one of the most commonly used data structures . Ruby provides powerful and versatile arrays for all your needs.
Mastering them means you can get a lot done out of them.
There are many ways to create arrays.
Create an empty array:
arr = []
An array of items of same type:
nums = [11, 22, 33]
An array of arrays:
matrix_2_by_2 = [[1, 11], [2, 22], [3, 33]]
## Now create a 3 by 3 array. Go ahead. Make my day.
A nested array:
nested = [11, :a, [:alpha, :beta, [:matt, :joe]]]
An array of items of different types:
junkyard = [11, 'alpha', ['x', 'y'], nil, Object.new]
Creating with the Array.new
constructor (rarely used):
p Array.new(3) # => [nil, nil, nil]
p Array.new(3, 'a') # => ["a", "a", "a"]
Using a block to dynamically create items:
p Array.new(3) { |i| i * 2 } # => [0, 2, 4]
If you don’t like quoting strings, use %w()
to create
string arrays and %i()
to create symbol arrays:
p %w(one two three) # => ["one", "two", "three"]
p %i(one two three) # => [:one, :two, :three]
The operator method []
is used to access one or more
items from the array:
arr = ['a', 'b', 'c', 'd', 'e']
Array index starts from 0:
p arr[0] # => "a" (first item)
p arr[4] # => "e" (last item)
To count backwards (from last to first), use negative indices:
p arr[-1] # => "e" (last item)
p arr[-2] # => "d" (second-to-last item)
Extract a portion of an array:
p arr[1, 3] # => ["b", "c", "d"]
Same thing with a range:
p arr[1..3] # => ["b", "c", "d"]
p arr[1...3] # => ["b", "c"]
There are other ways to access items too:
p arr.first # => "a"
p arr.last # => "e"
p arr.first(2) # => ["a", "b"]
p arr.last(2) # => ["d", "e"]
p arr.take(2) # => ["a", "b"]
Accessing beyond range returns nil
. It doesn’t throw
any error:
p arr[5] # => nil
In some situations, this behavior can result in
unexpected bugs. If you want to know if the index is
out of range, use fetch
instead of []
.
This will blow up with an exception if the index is out of range:
p arr.fetch(5) rescue nil
## 💥 without `rescue nil` blows up with IndexError
You can provide a default value with fetch if there’s nothing in that index:
p arr.fetch(5, 'FIVE!') # => "FIVE!"
If you have a nested array, and you want to access a
specific item, you can use []
like so:
arr = [11, :a, [:alpha, :beta, [:matt, :joe]]]
p arr[2][2][1] # => :joe
But a better, safer way is to use dig
. Why?
Assume arr
is user input and you expect it to
have 3 items, with the third one being another array
whose third item is yet another array of 2 items.
But what if the user only supplied 2 items for arr
?
What if the 3rd item is not present, or is nil
? How
would the code above work?
It’ll blow up with a NoMethodError
exception:
arr[2] = nil
p arr[2][2][1] rescue nil
## 💥 without `rescue nil` blows up with:
## => undefined method '[]' for nil (NoMethodError)
But with dig
, you get nil
if there’s no item in any
of the intermediate indices:
p arr.dig(2, 2, 1) # => nil
And you get the correct value if the item exists:
arr[2] = [:alpha, :beta, [:matt, :joe]]
p arr.dig(2, 2, 1) # => :joe
This means you can use this in conditionals.
Prior to dig
, people had to do this:
if arr[2] && arr[2][2] && arr[2][2][1]
## do something
else
## inform user that input format is incorrect
end
Now you can simply do this. Lucky you:
if arr.dig(2, 2, 1)
else
end
Note: dig
also works with arrays containing hashes,
that in turn contain arrays, and so on:
arr = [11, :a, [:alpha, :beta, {matt: [22, 33]}]]
p arr.dig(2, 2, :matt, 1) # => 33
Note^2: This is okay, but not cool. Pattern matching is cool:
arr => [_, _, [_, _, {matt: [_, age_of_matt]}]]
p age_of_matt # => 33
arr = [11, 22]
<<
or push
to append an item to the array’s
end:arr << 33
arr.push(44)
p arr # => [11, 22, 33, 44]
unshift
to prepend an item to the array’s
beginning:arr.unshift(55)
p arr # => [55, 11, 22, 33, 44]
insert
to place an item at any given index:arr.insert(3, 7)
p arr # => [55, 11, 22, 7, 33, 44]
concat
to add arrays of items:arr.concat([:a, :b], [:c])
p arr # => [55, 11, 22, 7, 33, 44, :a, :b, :c]
Note: All these methods modify the original array, and also return the modified array as the return value.
pop
to remote the last item from the array,
and also return that last item:popped = arr.pop
p popped # => 55
p arr # => [66, 11, 22, 7, 33, 44]
delete_at
to delete an item at a specified
index:deleted = arr.delete_at(3)
p deleted # => 7
p arr # => [66, 11, 22, 33, 44]
delete
simply:10.times { arr.push(44) } # check arr's value
arr.delete(44)
p arr # => [66, 11, 22, 33]
Use the []=
operator method to replace an item in a
given index:
arr[2] = 222
p arr # => [66, 11, 222, 33]
You can replace consecutive items if you pass two
indices, or a range to []=
:
arr[1, 2] = [44, 55]
p arr # => => [66, 44, 55, 33]
arr[1..2] = [66, 77]
p arr # => => [66, 66, 77, 33]
The simplest way to loop over an array’s
items is the each
method:
arr = [11, 22, 33]
arr.each { |e| p e }
But it’s common to use many of the Enumerable
methods
(seen below) to iterate over an array because, besides
looping, they also do some additional things.
size
gives you the count of array items:p junkyard.size # => 5
empty?
returns true if array has no items:p junkyard.empty?, [].empty? # => [false, true]
map
takes a block and returns a new array of
items that’s the result of calling the block for each
item:p [1, 2, 3, 4].map { |e| e * 2 } # => [2, 4, 6, 8]
compact
removes all nil
s from an array:p [11, 22, nil, 33, nil].compact # => [11, 22, 33]
flatten
returns a single-dimensional array from all
the items flattened in a multi-dimensional array:p [1, 2, [3, 4], [[5, [6]]]].flatten
## => [1, 2, 3, 4, 5, 6]
uniq
removes duplicates:p [11, 22, 11].uniq # => [11, 22]
sort
does its thing:p [22, 11].sort # => [11, 22]
join
joins the items with the given string:p %w(cat mat bat).join # => "catmatbat"
p %w(cat mat bat).join(',') # => "cat,mat,bat"
sample
returns one or more random items:p arr.sample # => "a"
p arr.sample(3) # => ["d", "c", "b"]
shuffle
returns a new array with items shuffled:p arr.shuffle # => ["e", "a", "c", "b", "d"]
include?
returns true
if the given item is
included in the array:p %w(11 22 33).include?(22) # => false
p %w(11 22 33).include?('22') # => true
intersect?
is like include?
, but it
can check an array of items instead of just one:p %w(11 22 33).intersect?(%w(1 2 3)) # => false
p %w(11 22 33).intersect?(%w(1 33 3)) # => true
All the methods above don’t modify the array they were called on. They just return a new modified array.
arr = [11, nil, 'a', 11, ['x', 'y']]
p arr.compact # => [11, "a", 11, ["x", "y"]]
p arr.uniq # => [11, nil, "a", ["x", "y"]]
p arr.flatten # => [11, nil, "a", 11, "x", "y"]
## `arr` hasn't changed one bit!
p arr
## => [11, nil, 'a', 11, ['x', 'y']]
To modify the original array, use the “bang”
versions of these methods: compact!
, flatten!
,
uniq!
, sort!
, reverse!
etc.
arr.compact! # => [11, "a", 11, ["x", "y"]]
arr.uniq! # => [11, "a", ["x", "y"]]
arr.flatten! # => [11, "a", "x", "y"]
## Now `arr` is a changed man.
p arr
## => [11, "a", "x", "y"]
In general, prefer non-bang methods because they’re easier to debug as they don’t modify the original array.
a = [11, 22]
b = [22, 33]
c = [33, 44]
p (a + b + c) # => [11, 22, 22, 33, 33, 44]
union
or |
to return combined items from
all given arrays:p a.union(b, c) # => [11, 22, 33, 44]
p (a | b | c) # => [11, 22, 33, 44]
intersection
or &
to return common items from
all given arrays:p a.intersection(b) # => [22]
p a.intersection(b, c) # => []
p (a & b) # => [22]
p (a & b & c) # => []
difference
or -
to return items that are
not common among all given arrays:p a.difference(b, c) # => [11]
p (a - b - c) # => [11]
Note: union
, intersection
difference
all can
take any number of arrays as arguments, whereas the |
,
&
and -
operator methods can only take one array.
The Array class includes the Enumerable
module. That’s
where it gets a lot of its superpowers from.
In fact, the compact
, sort
, map
and uniq
methods
mentioned above… they all come from Enumerable.
There’s a lot more. Read the docs for full list. But I’ll list some of my favorite and most useful methods:
find
returns the first item that makes the
block return a truthy value:p [11, 22, 33].find { |e| e.odd? } # => 11
find_all
returns an array of all items that make the
block return a truthy value:p [11, 22, 33].find_all { |e| e.odd? } # => [11, 33]
all?
returns true only if all items make the block
return true:p [2, 4, 6].all? { |e| e.even? } # => true
any?
returns true if even one item makes the block
return true:p [2, 4, 6].any? { |e| e.odd? } # => false
none?
does the opposite. Returns true only if none
of the items make the block return true:p [2, 4, 6].none? { |e| e.odd? } # => true
grep?
is a fun method. It returns only items that
match a given pattern. But it can also grep items
within a range, or items that are only of a particular
class:p %w(duck buck nick).grep(/uck$/) # => ["duck", "buck"]
p [11, '22', 33].grep(Integer) # => [11, 33]
p [11, 22, 33].grep(20..30) # => [22]
each_with_index
is there just so you don’t have to
do this: i = 0
and then i += 1
for each iteration:[11, 22].each_with_index { |e, i| p "idx of #{e}: #{i}" }
## => "idx of 11: 0"
## => "idx of 22: 1"
each_with_object
is elegant. It saves you an extra
step when you’re building an object based on each array
item:hash_obj = {}
%w(a b c).each_with_object(hash_obj) do |ele, hash_obj|
hash_obj[ele.to_sym] = ele.upcase
end
p hash_obj # => {a: "A", b: "B", c: "C"}
reduce
is used when you want to reduce a whole array
into a single value based on a starting value, a
function and the individual array items one at a time:## sum of all items
p [10, 20, 30].reduce { |mem, e| mem + e } # => 60
## product of all items
p [10, 20, 30].reduce { |mem, e| mem * e } # => 6000
## 100, plus sum of all items
p [10, 20, 30].reduce(100) { |mem, e| mem + e } # => 160
In reduce
, mem
is just a block variable and it can
be named anything. But it’s customary to name it mem
or memo
with reduce
because it represents
“memory”. It is the value Ruby “keeps in memory”
during each iteration.
Many methods we discussed above have other names as well. These are method aliases.
map
is the same as collect
find
is detect
find_all
is filter
. It’s also select
reduce
is inject
include?
is member?
reduce
is inject
[]
is slice
Which one to use?
As with all things in Ruby related to style… it’s a
matter of preference.
(I prefer the left ones in the list above.)
to_h
and Hash[]
convert an array of arrays to a
hash:arr = [[:a, 1], [:b, 2]]
p arr.to_h # => {a: 1, b: 2}
p Hash[arr] # => {a: 1, b: 2}
to_json
converts an array to a JSON string. But you
need to require json
first in some older versions of
Ruby:require 'json'
puts arr.to_json # => [["a",1],["b",2]]
it
From Ruby 3.4, instead of this:
p [11, 22].map { |e| e.even? }
You can do this:
p [11, 22].map { it.even? } # => [false, true]
But be careful. If there’s already an it
variable in
the scope of the block’s definition, then only that will
be used inside the block. This is called closure.
it = 10
p [11, 22].map { it.even? } # => [true, true]
So, only use this feature:
it
variable
defined already