Saturday, 7 November 2009

Ruby Arrays

An array is a group of values in a certain order. You can mix-and-match what you put in the array (it is all objects with Ruby), including hashes and other arrays.
a = ['one', 2, 3.0]

A quick way to create an array of strings (if each string is a single word) is like this (you can use any matching brackets, or indeed more punctuation):
a = %w(one two three)
# => ["One", "Two", "Three"]

To add an element to an existing array do this:
array << "new element"
You can join two arrays using the addition operator.
b = [4, 16]
c = a + b
# => ["One", "Two", "Three", 4, 16]

Use include? to determine if the given object is in the array. To access an array member use [], or at or fetch. The [] and at methods return nil if the index is out of range, while fetch throws an exception, or a default value of given. A negative index counts back from the end, while a range returns a subset of the array.
ary = %w(zero one two three four five six)
p ary[2]
# => "two"
# => "three"
p ary[-1]
# => "six"
p ary[2..4]
# => ["two, "three", "four"]

Ruby has some very neat tricks with arrays. Want to delete all the elements of an array of hashes that have a body that is nil?
array.delete_if { |x| x.body.nil? }

The delete_if removes from the array any elements that evaulate to true in the block. Note that the delete_if method seems to ignore the convention of naming methods that affect the object itself with an exclamation mark.

The join method concatenates each member of an array into a long string. The supplied parameter is used to separate each item. The * operator does the same.
a = %w(one two three four)
# => ["one", "two", "three", "four"]
a * ', '
# => "one, two, three, four"

map, select and reject
The map method (aka collect) constructs a new array by processing each element in the array as per the block, while select returns a new array containing only those elements where the block evaluates to true. The reject method gives an array for the elements where the block is not true.
people = [
{:name => 'Fred', :age => 19},
{:name => 'Boris', :age => 23},
{:name => 'Mary', :age => 27},

p {|e| e[:name]}
# => ["Fred", "Boris", "Mary"]

p {|e| e[:age] <> [{:name=>"Fred", :age=>19}]

p people.reject {|e| e[:age] <> [{:name=>"Boris", :age=>23},
{:name=>"Mary", :age=>27}]

The sort method will, as the name suggests, sort the array, using the <=> relationship. Alternatively you can supply block to have it sorted by a custom comparison.
people.sort { |a, b| a[:age] <=> b[:age] }
# youngest to oldest

Note that your comparison must return -1, 0, or +1. In this example, I therefore could not use <.

If you use the Enumerable mixin discussed later you also have a sort_by method. This is not as fast to run, but can be quicker to code. In this method, the block determines a value for the element, for ranking purposes. Here is the previous example re-written.
people.sort_by { |a| a[:age] }

pop and push, shift and unshift
You can use push and pop to add or remove the last element, and unshift and shift to add and remove from the start.
a = %w(one two three)
# => ["one", "two", "three"]
a.push 'four'
# => ["one", "two", "three", "four"]
# => "four"
# => ["one", "two", "three"]
a.unshift 'zero'
# => ["zero", "one", "two", "three"]
# => "zero"
# => ["one", "two", "three"]

Extending Array, part 1
You can, of course, add your own methods to Array. Here are some examples.
class Array

# Shuffle an array
# from
def shuffle
sort_by { rand }
def shuffle!
self.replace shuffle

# Randomly pick one element of the array.
def pick
fetch rand(length)

# Returns a total over each element in the array
# where the value for an element is determined
# by the given block. This example will look
# through an array of hashes
# and return the total of the square of values
# with the key :value
# { |e| e[:value] * e[:value] }
def total &prc
val = 0
each do |e|
val +=

# Returns an element that best fits the criteria
# given by the block. This example will look
# through an array of hashes and return the element
# with the highest value with the key :value
# ary.find_best { |x, y| x[:value] < y[:value] }
def find_best &prc
best = fetch(0)
each { |e| best = e if yield(best, e) }

# Returns the elements of the given array as
# string with each element listed
# in the form "one, two and three".
def list
join(', ').reverse.sub(' ,', ' dna ').reverse

Extending Array, part 2
Another way to extend Array is to "mixin" the Enumerable module.
class Array
include Enumerable

This has several interesting methods (and also some that are already in Array). Use any? and all? to determine if at least one element evalauates to true on the block, or all of them do.
a = %w(one two three)
a.any? {|e| e.length == 5}
# => true
p a.all? {|e| e.length == 5}
# => false

You can find an element in an array using find (aka detect). This method returns the first element for which the block evaulates to true. The method takes an optional parameter, this is returned if no element is found that fits.
people.find { |e| e[:age] == 23 }

The inject method combines each element of the array.
total_age = people.inject(0) { |memo, e| memo += e[:age] }
# => 69

The supplied parameter is the initial value (in this example, zero). This is technically optional, but going to be necessary in most cases (including this example) for the calculation to work in the first iteration. The memo is the ongoing value, so the increment is added to this.


Struggling with Ruby: Contents Page