Chapter 4. Arrays and Hashes

image with no caption

Up to now, you’ve generally been using objects one at a time. In this chapter, you’ll find out how to create a list of objects. You’ll start by looking at the most common type of list structure: an array.

Arrays

An array is a sequential collection of items in which each item can be indexed. In Ruby (unlike many other languages), a single array can contain items of mixed data types such as strings, integers, and floats or even a method call that returns some value. For example, the final element in a1 shown here calls my method, array_length, which returns the length of the array, a0:

array0.rb

def array_length( anArray )
    return anArray.length
end

a0 = [1,2,3,4,5]
a1 = [1,'two', 3.0, array_length( a0 ) ]
p( a1 )        #=>[1, "two", 3.0, 5]

The first item in an array has the index 0, which means the final item has an index equal to the total number of items in the array minus 1. Given the array a1, shown previously, this is how to obtain the values of the first and last items:

a1[0]        # returns 1st item (at index 0)
a1[3]        # returns 4th item (at index 3)

You’ve already used arrays a few times—for example, in 2adventure.rb you used an array to store a map of Room objects:

mymap = Map.new([room1,room2,room3])

Creating Arrays

Like many other programming languages, Ruby uses square brackets to delimit an array. You can easily create an array, fill it with some comma-delimited values, and assign it to a variable:

arr = ['one','two','three','four']

As with most other things in Ruby, arrays are objects. They are defined, as you might guess, by the Array class and, just like strings, they are indexed from 0. You can reference an item in an array by placing its index between square brackets. If the index is invalid, nil is returned:

array1.rb

arr = ['a', 'b', 'c']
puts(arr[0])      # shows 'a'
puts(arr[1])      # shows 'b'
puts(arr[2])      # shows 'c'
puts(arr[3])      # nil

array1.rb

An array may include expressions that yield values. Let’s assume you have already created this method:

array2.rb

def hello
  return "hello world"
end

You can now declare this array:

x = [1+2, hello, `dir`]

Here, the first element is a mathematical expression that yields the integer 3, and the second is the string “hello world” (returned by the method hello). If you run this on Windows, the third array element will be a string containing a directory listing. This is because `dir` is a back-quoted string, which is executed by the operating system (see Chapter 3). The final “slot” in the array is, therefore, filled with the value returned by the dir command, which happens to be a string of filenames. If you are running on a different operating system, you may need to substitute an appropriate command at this point. (For example, if you’re running a Unix-like operating system, you could substitute `ls` to get a similar string of filenames.)

dir_array.rb

If you want to create an array of single-quoted strings but can’t be bothered to type all the quotation marks, a shortcut is to put unquoted text separated by spaces between parentheses preceded by %w (or use a capital %W for double-quoted strings, as explained in Chapter 3).

array2.rb

y = %w( this is an array of strings )

The previous code assigns the array shown next to the variable, y:

["this", "is", "an", "array", "of", "strings"]

You can also create arrays using the usual object construction method, new. Optionally, you can pass an integer to new to create an empty array of a specific size (with each element set to nil), or you can pass two arguments: the first to set the size of the array and the second to specify the element to place at each index of the array, like this:

a = Array.new                     # an empty array
a = Array.new(2)                  # [nil,nil]
a = Array.new(2,"hello world")    # ["hello world","hello world"]

Multidimensional Arrays

To create a multidimensional array, you can create one array and then add other arrays to each of its “slots.” For example, this creates an array containing two elements, each of which is itself an array of two elements:

a = Array.new(2)
a[0]= Array.new(2,'hello')
a[1]= Array.new(2,'world')

Note

You can also create an Array object by passing an array as an argument to the new method. Be careful, though: Ruby considers it a syntax error if you fail to leave a space between the new method and the opening square bracket. In other words, this works: a = Array.new [1,2,3]. However, this doesn’t: a = Array.new[1,2,3]. But using parentheses always works, no matter where you put a space: a = Array.new([1,2,3]).

It is also possible to nest arrays inside one another using square brackets. This creates an array of four arrays, each of which contains four integers:

a = [    [1,2,3,4],
    [5,6,7,8],
    [9,10,11,12],
    [13,14,15,16]  ]

In the previous code, I have placed the four “subarrays” on separate lines. This is not obligatory, but it does help clarify the structure of the multidimensional array by displaying each subarray as though it were a row, similar to the rows in a spreadsheet. When talking about arrays within arrays, it is convenient to refer to each nested array as a “row” of the “outer” array.

For some more examples of using multidimensional arrays, load the multi_array.rb program. This starts by creating an array, multiarr, containing two other arrays. The first of these arrays is at index 0 of multiarr, and the second is at index 1:

multi_array.rb

multiarr = [['one','two','three','four'],[1,2,3,4]]

Next you need to find some way to locate the individual elements within arrays, which are themselves contained inside other arrays. You’ll consider this problem in the next section.

Iterating over Arrays

You can access the elements of an array by iterating over them using a for loop. In many programming languages, a for loop counts over a fixed number of elements from a starting number (such as 0) to an ending number (such as 10), incrementing a counter variable (such as i) at each pass through the loop. So, in other languages, you might be used to writing a loop something like this: for i = 1 to 10.

In Ruby, the normal for loop counts over all the items in a collection and may be referred to as a for..in loop. Its counter variable is assigned each object in a collection, one by one, at each pass through the loop. The syntax may be summarized as for anObject in aCollection, and at each turn through the loop, the variable anObject is assigned a new item from the collection aCollection until no more items remain. The loop shown next iterates over two elements, namely, the two subarrays at index 0 and 1:

for i in multiarr
    puts(i.inspect)
end

This displays the following:

["one", "two", "three", "four"]
[1, 2, 3, 4]

So, how do you iterate over the items (the strings and integers) in each of the two subarrays? If there is a fixed number of items, you could specify a different iterator variable for each, in which case each variable will be assigned the value from the matching array index.

Here you have four subarray slots, so you could use four variables like this:

for (a,b,c,d) in multiarr
    print("a=#{a}, b=#{b}, c=#{c}, d=#{d}
" )
end

You could also use a for loop to iterate over all the items in each subarray individually:

multi_array2.rb

for s in multiarr[0]
   puts(s)
end
for s in multiarr[1]
   puts(s)
end

Both of these techniques (multiple iterator variables and multiple for loops) have two requirements: that you know how many items there are in either the “rows” or the “columns” of the grid of arrays and that each subarray contains the same number of items as each other.

For a more flexible way of iterating over multidimensional arrays, you could use nested for loops. An outer loop iterates over each row (subarray), and an inner loop iterates over each item in the current row. This technique works even when subarrays have varying numbers of items:

for row  in multiarr
   for item in row
     puts(item)
   end
end

You’ll be looking at for loops and other iterators in more depth in the next chapter.

Indexing into Arrays

As with strings (see Chapter 3), you can index from the end of an array using negative numbers, where −1 is the index of the last element, −2 is the second-to-last, and so on. You can also use ranges, like this:

array_index.rb

arr = ['h','e','l','l','o',' ','w','o','r','l','d']

print( arr[0,5] )        #=> hello (or) ["h", "e", "l", "l", "o"]
print( arr[-5,5 ] )      #=> world (or) ["w", "o", "r", "l", "d"]
print( arr[0..4] )       #=> hello (or) ["h", "e", "l", "l", "o"]
print( arr[-5..-1] )     #=> world (or) ["w", "o", "r", "l", "d"]

Note that the output displayed by print or puts may vary depending on your version of Ruby. When Ruby 1.8 displays the elements in an array, it shows them one after the other so they look like a single string, as in hello. Ruby 1.9, however, shows the items in array format, as in ["h", "e", "l", "l", "o"].

If you use p instead of print to inspect the array, both Ruby 1.8 and 1.9 display the same result:

p( arr[0,5] )     #=> ["h", "e", "l", "l", "o"]
p( arr[0..4] )    #=> ["h", "e", "l", "l", "o"]

As with strings, when you provide two integers in order to return a number of contiguous items from an array, the first integer is the start index, while the second is a count of the number of items (not an index):

arr[0,5]    # returns 5 chars - ["h", "e", "l", "l", "o"]

You can also make assignments by indexing into an array. Here, for example, I first create an empty array and then put items into indexes 0, 1, and 3. The “empty” slot at index 2 will be filled with a nil value:

array_assign.rb

arr = []

arr[0] = [0]
arr[1] = ["one"]
arr[3] = ["a", "b", "c"]

# arr now contains:
# [[0], ["one"], nil, ["a", "b", "c"]]

Once again, you can use start-end indexes, ranges, and negative index values:

arr2 = ['h','e','l','l','o',' ','w','o','r','l','d']

arr2[0] = 'H'
arr2[2,2] = 'L', 'L'
arr2[4..6] = 'O','-','W'
arr2[-4,4] = 'a','l','d','o'

# arr2 now contains:
# ["H", "e", "L", "L", "O", "-", "W", "a", "l", "d", "o"]

Copying Arrays

Note that when you use the assignment operator (=) to assign one array variable to another variable, you are actually assigning a reference to the array; you are not making a copy. For example, if you assign one array called arr1 to another array called arr2, any changes made to either variable will also alter the value of the other because both variables refer to the same array. If you want the variables to reference two different arrays, you can use the clone method to make a new copy:

array_copy.rb

arr1=['h','e','l','l','o',' ','w','o','r','l','d']
arr2=arr1  # arr2 is now the same as arr1.
           # Change arr1 and arr2 changes too!
arr3=arr1.clone
           # arr3 is a copy of arr1.
           # Change arr3 and arr2 is unaffected

Testing Arrays for Equality

The comparison operator for arrays is <=>. This compares two arrays—let’s call them arr1 and arr2. It returns −1 if arr1 is less than arr2, it returns 0 if arr1 and arr2 are equal, and it returns 1 if arr2 is greater than arr1. But how does Ruby determine whether one array is “greater than” or “less than” another? It compares each item in one array with the corresponding item in the other. When two values are not equal, the result of their comparison is returned. In other words, if this comparison were made:

[0,10,20] <=> [0,20,20]

the value −1 would be returned. This means the first array is “less than” the second, since the integer at index 1 of the first array (10) is less than the integer at index 1 in the second array (20).

If you want to make a comparison based on the array’s length rather than the value of its elements, you can use the length method:

#  Here [2,3,4].length is less than [1,2,3,4].length
p([1,2,3].length<=>[1,2,3,4].length)    #=> −1
p([2,3,4].length<=>[1,2,3,4].length)    #=> −1

If you are comparing arrays of strings, then comparisons are made on the ASCII values of the characters that make up those strings. If one array is longer than another and the elements in both arrays are equal, then the longer array is deemed to be “greater.” However, if two such arrays are compared and one of the elements in the shorter array is greater than the corresponding element in the longer array, then the shorter array is deemed to be greater.

array_compare.rb

p([1,2,3]<=>[2,3,4])            #=> −1     (array 1 < array 2)
p([2,3,4]<=>[1,2,3])            #=> 1      (array 1 > array 2)
p([1,2,3,4]<=>[1,2,3])          #=> 1      (array 1 > array 2)all
p([1,2,3,4]<=>[100,200,300])    #=> −1     (array 1 < array 2)
p([1,2,3]<=>["1","2","3"])      #=> nil    (invalid comparison)

Sorting Arrays

The sort method compares adjacent array elements using the comparison operator <=>. This operator is defined for many Ruby classes, including Array, String, Float, Date, and Fixnum. The operator is not, however, defined for all classes (that is to say, it is not defined for the Object class from which all other classes are derived). One of the unfortunate consequences of this is that it cannot be used to sort arrays containing nil values. However, it is possible to get around this limitation by defining your own sorting routine. This is done by sending a block to the sort method. You’ll learn about blocks in detail in Chapter 10, but for now it’s enough to know a block is a chunk of code delimited either by curly brackets or by the keywords do and end. The following block determines the comparison used by the sort method:

arr.sort{
  |a,b|
    a.to_s <=> b.to_s
}

Here arr is an array object, and the variables a and b represent two contiguous array elements. I’ve converted each variable to a string using the to_s method; this converts nil to an empty string that will be sorted “low.” Note that although my sorting block defines the sort order of the array items, it does not change the array items themselves. So, nil will remain as nil, and integers will remain as integers. The string conversion is used only to implement the comparison, not to change the array items.

array_sort.rb

arr = ['h','e','l','l','o',' ',nil,'w','o','r','l','d',1,2,3,nil,4,5]

# sort ascending from nil upwards
sorted_arr = arr.sort{
    |a,b|
        a.to_s <=> b.to_s
    }

p(sorted_arr )

This is the array created and displayed by the previous code:

[nil, nil, " ", 1, 2, 3, 4, 5, "d", "e", "h", "l", "l", "l", "o", "o", "r", "w"]

The array_sort.rb program supplied in the code archive also contains a method to sort in descending order. This is done simply by changing the order of the items on either side of the comparison operator:

reverse_sorted_arr = arr.sort{
    |a,b|
        b.to_s <=> a.to_s
    }

Comparing Values

The comparison “operator” <=> (which is, in fact, a method) is defined in the Ruby module named Comparable. For now, you can think of a module as a sort of reusable code library. You’ll be looking more closely at modules in Chapter 12.

You can include the Comparable module in your own classes. This lets you override the <=> method to enable you to define exactly how comparisons will be made between specific object types. For example, you may want to subclass Array so that comparisons are made based purely on the length of two arrays rather than on the value of each item in the array (which is the default, as explained in See Testing Arrays for Equality). This is how you might do this:

comparisons.rb

class MyArray < Array
  include Comparable

  def <=> ( anotherArray )
    self.length <=> anotherArray.length
  end
end

Now, you can initialize two MyArray objects like this:

myarr1 = MyArray.new([0,1,2,3])
myarr2 = MyArray.new([1,2,3,4])

And you can use the <=> method defined in MyArray to make comparisons:

# Two MyArray objects
myarr1 <=> myarr2        #=> 0

This comparison returns 0, which indicates that the two arrays are equal (since our <=> method evaluates equality according to length alone). If, on the other hand, you were to initialize two standard arrays with exactly the same integer values, the Array class’s own <=> method would perform the comparison:

# Two Array objects
arr1 <=> arr2        #=> −1

Here the comparison returns −1, which indicates that the first array evaluates to “less than” the second array, since the Array class’s <=> method compares the numerical values of each item in arr1 and these are less than the values of the items at the same indexes in arr2.

But what if you want to make “less than,” “equal to,” and “greater than” comparisons using the traditional programming notation?

<                 # less than
==                # equal to
>                 # greater than

In the MyArray class, you can make comparisons of this sort without writing any additional code. This is because the Comparable module, which has been included in the MyArray class, automatically supplies these three comparison methods; each method makes its comparison based on the definition of the <=> method. Since our <=> makes its evaluation based on the number of items in an array, the < method evaluates to true when the first array is shorter than the second, == evaluates to true when both arrays are of equal length, and > evaluates to true when the second array is longer than the first:

p( myarr1 < myarr2 )      #=> false
p( myarr1 == myarr2 )     #=> true

The standard Array class does not include the Comparable module. So if you try to compare two ordinary arrays using <, ==, or >, Ruby will display an error message telling you that the method is undefined.

However, it’s easy to add these three methods to a subclass of Array. All you have to do is include Comparable, like this:

class Array2 < Array
  include Comparable
end

The Array2 class will now perform its comparisons based on the <=> method of Array—that is, by testing the values of the items stored in the array rather than merely testing the length of the array. Assuming that the Array2 objects, arr1 and arr2, are initialized with the same arrays that you previously used for myarr1 and myarr2, you would now see these results:

p( arr1 < arr2 )        #=> true
p( arr1 > arr2 )        #=> false

Array Methods

Several of the standard array methods modify the array itself rather than returning a modified copy of the array. These include the methods marked with a terminating exclamation point, such as sort!, reverse!, flatten!, and compact!. These also include the << method, which modifies the array to its left by adding to it the array on its right; clear, which removes all the elements from the given array; and delete and delete_at, which remove selected elements. Table 4-1 shows some of the more commonly used Array methods.

Table 4-1. Commonly Used Array Methods

Array

Task

&

Returns common elements of two arrays, no duplicates

+

Returns array concatenating two arrays

-

Returns array with items in second array removed from first

<<

Modifies first array by appending items from second array

clear

Modifies array by removing all elements

compact

Returns array with nil items removed

compact!

Modifies array by removing nil items

delete( object )

Modifies array by deleting object

delete_at( index )

Modifies array by deleting item at index

flatten

Unpacks nested array items and returns array

flatten!

Modifies array by unpacking nested array items

length

Returns number of elements in array

reverse

Returns array with elements in reverse order

reverse!

Modifies array by reversing element order

sort

Returns array sorted using <=>

sort!

Modifies array sorted using <=>

You can try the previous methods in the array_methods.rb sample program. Here are a few examples:

array_methods.rb

arr1 = [1,1,2,2,3,3]
arr2 = [1,2,3,4,5,6,7,8,9]
arr3 = ['h','e','l','l','o',' ',nil,'w','o','r','l','d']

p(arr1&arr2 )       #=> [1, 2, 3]
p(arr1+arr2)        #=> [1, 1, 2, 2, 3, 3, 1, 2, 3, 4, 5, 6, 7, 8, 9]
p(arr1-arr2)        #=> []
p(arr2-arr1)        #=> [4, 5, 6, 7, 8, 9]
arr1<<arr2
p(arr1)             #=> [1, 1, 2, 2, 3, 3, [1, 2, 3, 4, 5, 6, 7, 8, 9]]
arr1.clear
p(arr1)             #=>[]

Although most of the behavior array methods may be deduced from their names, the flatten and compact methods need some explanation. An array is said to be flattened when it contains no subarrays. So if you have an array like [1,[2,3]], you can call [1,[2,3]].flatten to return this array: [1,2,3].

An array is said to be compacted when it contains no nil items. So if you have an array like [1,2,nil,3], you can call [1,2,nil,3].compact to return this array: [1,2,3]. The methods of Array can be chained together by placing one method call directly after the other:

flatten_compact.rb

p( [1,nil,[2,nil,3]].flatten.compact ) #=> [1,2,3]
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.9.197