Home > Articles > Programming > Ruby

Manipulating Structured Data in Ruby

  • Print
  • + Share This
  • 💬 Discuss

This chapter is from the book

Hal Fulton takes a look at arrays, hashes, and more complex data structures. Learn the similarities and differences between arrays and hashes; how to convert between them; and some interesting ways of extending their standard behavior. Also examine examples of inheriting from an existing class, and limited delegation by encapsulating an instance of another class. Learn how to store data creatively, ways to make use of various data structures, and how to create iterators for these classes.
This sample chapter is excerpted from The Ruby Way, by Hal Fulton.

In this Chapter

  • Working with Arrays
  • Working with Hashes
  • Working with Stacks and Queues
  • Working with Trees
  • Working with Graphs
  • Summary

All parts should go together without forcing. You must remember that the parts you are reassembling were disassembled by you. Therefore, if you can't get them together again, there must be a reason. By all means, do not use a hammer.
—IBM maintenance manual (1925)

Simple variables are not adequate for real-life programming. Every modern language supports more complex forms of structured data and also provides mechanisms for creating new abstract data types.

Historically, arrays are the earliest known and most widespread of the complex data structures. Long ago, in FORTRAN, they were called subscripted variables. Today they have changed somewhat, but the basic idea is the same in all languages.

More recently, the hash has become an extremely popular programming tool. Like an array, a hash is an indexed collection of data items; unlike an array, it may be indexed by any arbitrary object. (In Ruby, as in most languages, array elements are accessed by a numerical index.)

Finally, in this chapter we will look at more advanced data structures. Some of these are just special "views" of an array or hash; for example, stacks and queues can be implemented easily using arrays. Other structures such as trees and graphs may be implemented in different ways according to the situation and the programmer's preference.

But let's not get ahead of ourselves. We will begin with arrays.

Working with Arrays

Arrays in Ruby are indexed by integers and are zero based, just like C arrays. The resemblance ends there, however.

A Ruby array is dynamic. It is possible (but not necessary) to specify its size when you create it. After creation, it can grow as needed without any intervention by the programmer.

A Ruby array is heterogeneous in the sense that it can store multiple data types rather than just one type. Actually, it stores object references rather than the objects themselves, except in the case of immediate values such as Fixnum values.

An array keeps up with its own length so that we don't have to waste our time with calculating it or keeping an external variable in sync with the array. Also, iterators are defined so that, in practice, we rarely need to know the array length anyway.

Finally, the Array class in Ruby provides arrays with many useful functions for accessing, searching, concatenating, and otherwise manipulating arrays. We'll spend the remainder of this section exploring the built-in functionality and expanding on it.

Creating and Initializing an Array

The special class method [] is used to create an array; the data items listed within the brackets are used to populate the array. The three ways of calling this method are shown here (note that arrays a, b, and c will all be populated identically):

  a = Array.[](1,2,3,4)
  b = Array[1,2,3,4]
  c = [1,2,3,4]

Also, the class method new can take zero, one, or two parameters. The first parameter is the initial size of the array (number of elements). The second parameter is the initial value of each of the elements. Here's an example:

  d = Array.new       # Create an empty array
  e = Array.new(3)      # [nil, nil, nil]
  f = Array.new(3, "blah")  # ["blah", "blah", "blah"]

Accessing and Assigning Array Elements

Element reference and assignment are done using the class methods [] and []=, respectively. Each can take an integer parameter, a pair of integers (start and length), or a range. A negative index counts backward from the end of the array, starting at -1.

Also, the special instance method at works like a simple case of element reference. Because it can take only a single integer parameter, it is slightly faster. Here's an example:

  a = [1, 2, 3, 4, 5, 6]
  b = a[0]        # 1
  c = a.at(0)       # 1
  d = a[-2]        # 5
  e = a.at(-2)      # 5
  f = a[9]        # nil
  g = a.at(9)       # nil
  h = a[3,3]       # [4, 5, 6]
  i = a[2..4]       # [3, 4, 5]
  j = a[2...4]      # [3, 4]

  a[1] = 8        # [1, 8, 3, 4, 5, 6]
  a[1,3] = [10, 20, 30]  # [1, 10, 20, 30, 5, 6]
  a[0..3] = [2, 4, 6, 8] # [2, 4, 6, 8, 5, 6]
  a[-1] = 12       # [2, 4, 6, 8, 5, 12]

Note in the following example how a reference beyond the end of the array causes the array to grow (note also that a subarray can be replaced with more elements than were originally there, also causing the array to grow):

  k = [2, 4, 6, 8, 10]
  k[1..2] = [3, 3, 3]   # [2, 3, 3, 3, 8, 10]
  k[7] = 99        # [2, 3, 3, 3, 8, 10, nil, 99]

Finally, we should mention that an array assigned to a single element will actually insert that element as a nested array (unlike an assignment to a range), as shown here:

  m = [1, 3, 5, 7, 9]
  m[2] = [20, 30]     # [1, 3, [20, 30], 7, 9]

  # On the other hand...
  m = [1, 3, 5, 7, 9]
  m[2..2] = [20, 30]   # [1, 3, 20, 30, 7, 9]

The method slice is simply an alias for the [] method:

  x = [0, 2, 4, 6, 8, 10, 12]
  a = x.slice(2)        # 4
  b = x.slice(2,4)       # [4, 6, 8, 10]
  c = x.slice(2..4)      # [4, 6, 8]

The special methods first and last will return the first and last elements of an array, respectively. They will return nil if the array is empty. Here's an example:

  x = %w[alpha beta gamma delta epsilon]
  a = x.first   # "alpha"
  b = x.last    # "epsilon"

We have seen that some of the element-referencing techniques actually can return an entire subarray. There are other ways to access multiple elements, which we'll look at now.

The method indices will take a list of indices (or indexes, if you prefer) and return an array consisting of only those elements. It can be used where a range cannot (when the elements are not all contiguous). The alias is called indexes. Here's an example:

  x = [10, 20, 30, 40, 50, 60]
  y = x.indices(0, 1, 4)    # [10, 20, 50]
  z = x.indexes(2, 10, 5, 4)  # [30, nil, 60, 50]

Finding an Array's Size

The method length (or its alias size) will give the number of elements in an array. Note that this is one less than the index of the last item:

  x = ["a", "b", "c", "d"]
  a = x.length        # 4
  b = x.size         # 4

The method nitems is the same except that it does not count nil elements:

  y = [1, 2, nil, nil, 3, 4]
  c = y.size         # 6
  d = y.length        # 6
  e = y.nitems        # 4

Comparing Arrays

Comparing arrays is slightly tricky. If you do it at all, you should do it with caution.

The instance method <=> is used to compare arrays. It works the same as the other contexts in which it is used, returning either -1 (meaning "less than"), 0 (meaning "equal"), or 1 (meaning "greater than"). The methods == and != depend on this method.

Arrays are compared in an "elementwise" manner; the first two elements that are not equal will determine the inequality for the whole comparison. (Therefore, preference is given to the leftmost elements, just as if we were comparing two long integers "by eye," looking at one digit at a time.) Here's an example:

  a = [1, 2, 3, 9, 9]
  b = [1, 2, 4, 1, 1]
  c = a <=> b      # -1 (meaning a < b)

If all elements are equal, the arrays are equal. If one array is longer than another, and they are equal up to the length of the shorter array, the longer array is considered to be greater:

  d = [1, 2, 3]
  e = [1, 2, 3, 4]
  f = [1, 2, 3]
  if d == f
   puts "d equals f"  # Prints "d equals f"
  end

Because the Array class does not mix in the Comparable module, the usual operators, <, >, <=, and >=, are not defined for arrays. However, we can easily define them ourselves if we choose:

  class Array

   def <=> other) 
    (self <=> other)== -1
   end

   def <=(other)
    (self < other) or (self == other)
   end

   def >(other)
    (self <=> other) == 1
   end

   def >=(other)
    (self > other) or (self == other)
   end

  end

Having defined them, we can use them as you would expect:

  if a < b
   print "a < b"    # Prints "a < b"
  else
   print "a >= b"
  end
  if d < e
   puts "d < e"    # Prints "d < e"
  end

It is conceivable that comparing arrays will result in the comparison of two elements for which <=> is undefined or meaningless. This will result in a runtime error (a TypeError) because the comparison 3 <=> "x" is problematic:

  g = [1, 2, 3]
  h = [1, 2, "x"]
  if g < h       # Error!
   puts "g < h"    # No output
  end

However, in case you are still not confused, equal and not-equal will still work in this case. This is because two objects of different types are naturally considered to be unequal, even though we can't say which is greater or less than the other:

  if g != h       # No problem here.
   puts "g != h"    # Prints "g != h"
  end

Finally, it is conceivable that two arrays containing mismatched data types will still compare with the < and > operators. In the case shown here, we get a result before we stumble across the incomparable elements:

  i = [1, 2, 3]
  j = [1, 2, 3, "x"]
  if i < j       # No problem here.
   puts "i < j"    # Prints "i < j"
  end

Sorting an Array

The easiest way to sort an array is to use the built-in sort method, as shown here:

  words = %w(the quick brown fox)
  list = words.sort # ["brown", "fox", "quick", "the"]
  # Or sort in place:
  words.sort!    # ["brown", "fox", "quick", "the"]

This method assumes that all the elements in the array are comparable with each other. A mixed array, such as [1, 2, "three", 4], will normally give a type error.

In a case like this one, you can use the block form of the same method call. The example here assumes that there is at least a to_s method for each element (to convert it to a string):

  a = [1, 2, "three", "four", 5, 6]
  b = a.sort {|x,y| x.to_s <=> y.to_s}
  # b is now [1, 2, 5, 6, "four", "three"]

Of course, such an ordering (in this case, depending on ASCII) may not be meaningful. If you have such a heterogeneous array, you may want to ask yourself why you are sorting it in the first place or why you are storing different types of objects.

This technique works because the block returns an integer (-1, 0, or 1) on each invocation. When a -1 is returned, meaning that x is less than y, the two elements are swapped. Therefore, to sort in descending order, we could simply swap the order of the comparison, like this:

  x = [1, 4, 3, 5, 2]
  y = x.sort {|a,b| b <=> a}  # [5, 4, 3, 2, 1]

The block style can also be used for more complex sorting. Let's suppose we want to sort a list of book and movie titles in a certain way: We ignore case, we ignore spaces entirely, and we want to ignore any certain kinds of embedded punctuation. Listing 3.1 presents a simple example. (Both English teachers and computer programmers will be equally confused by this kind of alphabetizing.)

Listing 3.1 Specialized Sorting

  titles = ["Starship Troopers",
       "A Star is Born",
       "Star Wars",
       "Star 69",
       "The Starr Report"]
  sorted = titles.sort do |x,y|
   # Delete leading articles
   a = x.sub(/^(a |an |the )/i, "")
   b = y.sub(/^(a |an |the )/i, "")
   # Delete spaces and punctuation
   a.delete!(" .,-?!")
   b.delete!(" .,-?!")
   # Convert to uppercase
   a.upcase!
   b.upcase!
   # Compare a and b
   a <=> b
  end

  # sorted is now:
  # [ "Star 69", "A Star is Born", "The Starr Report"
  #  "Starship Troopers", "Star Wars"]

This example is not overly useful, and it could certainly be written more compactly. The point is that any arbitrarily complex set of operations can be performed on two operands in order to compare them in a specialized way. (Note, however, that we left the original operands untouched by manipulating copies of them.) This general technique can be useful in many situations—for example, sorting on multiple keys or sorting on keys that are computed at runtime.

Selecting from an Array by Criteria

Sometimes we want to locate an item or items in an array much as though we were querying a table in a database. There are several ways to do this; the ones we outline here are all mixed in from the Enumerable module.

The detect method will find at most a single element. It takes a block (into which the elements are passed sequentially) and returns the first element for which the block evaluates to a value that is not false. Here's an example:

  x = [5, 8, 12, 9, 4, 30]
  # Find the first multiple of 6
  x.detect {|e| e % 6 == 0 }     # 12
  # Find the first multiple of 7
  x.detect {|e| e % 7 == 0 }     # nil

Of course, the objects in the array can be of arbitrary complexity, as can the test in the block.

The find method is a synonym for detect; the method find_all is a variant that will return multiple elements as opposed to a single element. Finally, the method select is a synonym for find_all. Here's an example:

  # Continuing the above example...
  x.find {|e| e % 2 == 0}      # 8
  x.find_all {|e| e % 2 == 0}    # [8, 12, 4, 30]
  x.select {|e| e % 2 == 0}     # [8, 12, 4, 30]

The grep method will invoke the relationship operator to match each element against the pattern specified. In its simplest form, it will simply return an array containing the matched elements. Because the relationship operator (===) is used, the so-called pattern need not be a regular expression. (The name grep, of course, comes from the Unix tool of the same name, historically meaning something like general regular expression pattern-matcher.) Here's an example:

  a = %w[January February March April May]
  a.grep(/ary/)   # ["January, "February"]
  b = [1, 20, 5, 7, 13, 33, 15, 28]
  b.grep(12..24)   # [20, 13, 15]

There is a block form that will effectively transform each result before storing it in the array; the resulting array contains the return values of the block rather than the values passed into the block:

  # Continuing above example...
  # Let's store the string lengths
  a.grep(/ary/) {|m| m.length}   # [7, 8]
  # Let's square each value
  b.grep(12..24) {|n| n*n}     # {400, 169, 225}

The reject method is complementary to select. It excludes each element for which the block evaluates to true. The in-place mutator reject! is also defined:

  c = [5, 8, 12, 9, 4, 30]
  d = c.reject {|e| e % 2 == 0}  # [5, 9]
  c.reject! {|e| e % 3 == 0}
  # c is now [5, 8, 4]

The min and max methods may be used to find the minimum and maximum values in an array. There are two forms of each of these. The first form uses the "default" comparison, whatever that may be in the current situation (as defined by the <=> method). The second form uses a block to do a customized comparison. Here's an example:

  a = %w[Elrond Galadriel Aragorn Saruman Legolas]
  b = a.min                 # "Aragorn"
  c = a.max                 # "Saruman"
  d = a.min {|x,y| x.reverse <=> y.reverse} # "Elrond"
  e = a.max {|x,y| x.reverse <=> y.reverse} # "Legolas"

Suppose we want to find the index of the minimum or maximum element (assuming it is unique). We could use the index method for tasks such as this, as shown here:

  # Continuing above example...
  i = a.index a.min   # 2
  j = a.index a.max   # 3

This same technique can be used in other similar situations. However, if the element is not unique, the first one in the array will naturally be the one found.

Using Specialized Indexing Functions

The internals of a language handle the mapping of array indexes to array elements through what is called an indexing function. Because the methods that access array elements can be overridden, we can in effect index an array in any way we wish.

For example, in Listing 3.2, we implement an array that is "one-based" rather than "zero-based."

Listing 3.2 Implementing a One-Based Array

  class Array2 < Array

   def [](index)
    if index>0
     super(index-1)
    else
     raise IndexError
    end
   end

   def []=(index,obj)
    if index>0
     super(index-1,obj)
    else
     raise IndexError
    end
   end

  end


  x = Array2.new

  x[1]=5
  x[2]=3
  x[0]=1 # Error
  x[-1]=1 # Error

Note that the negative indexing (from the end of an array) is disallowed here. Also, be aware that if this were a real-life solution, there would be other changes to make, such as the slice method and others. However, this gives the general idea.

A similar approach can be used to implement multidimensional arrays (as you'll see later in the section "Using Multidimensional Arrays").

It is also possible to implement something like a triangular matrix (see Listing 3.3). This is like a special case of a two-dimensional array in which element x,y is always the same as element y,x (so that only one needs to be stored). This is sometimes useful, for example, in storing an undirected graph (as you'll see toward the end of this chapter).

Listing 3.3 Triangular Matrix

  class TriMatrix

   def initialize
    @store = []
   end

   def [](x,y)
    if x > y
     index = (x*x+x)/2 + y
     @store[index]
    else
     raise IndexError
    end
   end

   def []=(x,y,v)
    if x > y
     index = (x*x+x)/2 + y
     @store[index] = v
    else
     raise IndexError
    end
   end

  end


  t = TriMatrix.new

  t[3,2] = 1
  puts t[3,2] # 1

  puts t[2,3] # IndexError

Here, we have chosen to implement the matrix so that the row number must be greater than or equal to the column number; we also could have coded it so that the same pair of indexes simply mapped to the same element. These design decisions will depend on your use of the matrix.

It would have been possible to inherit from Array, but we thought this solution was easier to understand. The indexing formula is a little complex, but 10 minutes with pencil and paper should convince anyone it is correct. Some enhancements could probably be made to this class to make it truly useful, but we will leave that to you, the reader.

Also, it is possible to implement a triangular matrix as an array containing arrays that increase in size as the row number gets higher. This is somewhat similar to what we have done in the section "Using Multidimensional Arrays." The only tricky part would be to make sure that a row does not accidentally grow past its proper size.

Implementing a Sparse Matrix

Sometimes we need an array that has very few of its elements defined; the rest of its elements can be undefined (or more often zero). This so-called "sparse matrix" has historically been a waster of memory that has led people to seek indirect ways of implementing it.

Of course, in most cases, a Ruby array will suffice, because modern architectures typically have large amounts of memory. An unassigned element will have the value nil, which takes only a few bytes to store.

On the other hand, assigning an array element beyond the previous bounds of the array also creates all the nil elements in between. For example, if elements 0 through 9 are defined, and we suddenly assign to element 1000, we have in effect caused elements 10 through 999 to spring into being as nil values. If this is unacceptable, you might consider an alternative.

The alternative we have to suggest, however, does not involve arrays at all. If you really need a sparse matrix, a hash might be the best solution. See the section "Using a Hash As a Sparse Matrix" for more information.

Using Arrays As Mathematical Sets

Most languages do not directly implement sets (Pascal being one exception). However, Ruby arrays have some features that make them usable as sets. We'll present these here and add a few of our own.

First of all, an array can have duplicate entries. If you specifically want to treat the array as a set, you can remove these entries (using uniq or uniq!).

The two most basic operations performed on sets are union and intersection. These are accomplished by the | (or) and & (and) operators, respectively. In accordance with the idea that a set does not contain duplicates, any duplicates will be removed. (This may be contrary to your expectations if you are used to array union and intersection operations in some other language.) Here's an example:

  a = [1, 2, 3, 4, 5]
  b = [3, 4, 5, 6, 7]
  c = a | b      # [1, 2, 3, 4, 5, 6, 7]
  d = a & b      # [3, 4, 5]
  # Duplicates are removed...
  e = [1, 2, 2, 3, 4]
  f = [2, 2, 3, 4, 5]
  g = e & f      # [2, 3, 4]

The concatenation operator + can be used for set union, but it does not remove duplicates.

The - method is a "set difference" operator that will produce a set with all the members of the first set except the ones appearing in the second set. (See the section "Finding Elements in One Array but Not Another" for more information.) Here's an example:

  a = [1, 2, 3, 4, 5]
  b = [4, 5, 6, 7]
  c = a - b      # [1, 2, 3]
  # Note that the extra items 6 and 7 are irrelevant.

To "accumulate" sets, you can use the |= operator; as expected, a |= b simply means a = a | b. Likewise &= can progressively "narrow down" the elements of a set.

There is no exclusive-or defined for arrays, but we can make our own very easily. In set terms, this corresponds to elements that are in the union of two sets but not in the intersection. Here's an example:

  class Array

   def ^(other)
    (self | other) - (self & other)
   end

  end

  x = [1, 2, 3, 4, 5]
  y = [3, 4, 5, 6, 7]
  z = x ^ y      # [1, 2, 6, 7]

To check for the presence of an element in a set, we can use the method include? or member? (essentially an alias mixed in from Comparable), like so:

  x = [1, 2, 3]
  if x.include? 2
   puts "yes"   # Prints "yes"
  else
   puts "no"
  end

Of course, this is a little backward from what we are used to in mathematics, where the operator resembling a Greek epsilon denotes set membership. It is backward in the sense that the set is on the left rather than on the right; we are not asking "Is this element in this set?" but rather "Does this set contain this element?"

Many people will not be bothered by this at all. However, if you are used to Pascal or Python (or you have ingrained mathematical inclinations), you may want to use a different way. We present two options here:

  class Object

   def in(other)
    other.include? self
   end

  end

  x = [1, 2, 3]
  if 2.in x
   puts "yes"   # Prints "yes"
  else
   puts "no"
  end

This is still a trifle ugly, but at least the ordering is more familiar. As for making it look "more like an operator," Ruby's amazingly flexible parser allows you to write the expression 2.in x instead as 2 .in x or even 2. in x, should you wish to go that far.

For those who can't stand the presence of that period, it is conceivable that we could overload an operator such as <= for that purpose. However, something like this should be done with caution.

There has been talk of a Python-like (or Pascal-like) in operator for Ruby. However, it is no more than talk at this time.

How do we tell whether a set is a subset or a superset of another? There are no built-in methods, but we can do it as demonstrated in Listing 3.4.

Listing 3.4 Subset and Superset

  class Array

   def subset?(other)
    self.each do |x|
     if !(other.include? x)
      return false
     end
    end
    true
   end

   def superset?(other)
    other.subset?(self)
   end

  end

  a = [1, 2, 3, 4]
  b = [2, 3]
  c = [2, 3, 4, 5]

  flag1 = c.subset? a   # false
  flag2 = b.subset? a   # true
  flag3 = c.superset? b  # true

Note that we've chosen the "natural" ordering—that is, x.subset? y means "Is x a subset of y?" rather than vice versa.

To detect the null set (or empty set), we simply detect the empty array. The empty? method will do this.

The concept of set negation (or complement) depends on the concept of a universal set. Because in practical terms this will vary from one application or situation to another, the best way is the simplest—define the universe and then do a set difference, as shown here:

  universe = [1, 2, 3, 4, 5, 6]
  a = [2, 3]
  b = universe - a  # complement of a = [1, 4, 5, 6]

Of course, if you really feel the need, you could define a unary operator (such as - or ~) to do this.

You can iterate through a set just by iterating through the array. The only difference is that the elements will come out in order, which you may not want. To see how to iterate randomly, refer to the section "Iterating over an Array."

Finally, we may sometimes want to compute the powerset of a set. This is simply the set of all possible subsets (including the null set and the original set itself). Those familiar with discrete math, especially combinatorics, will see that there must be 2n of these subsets. We can generate the powerset as demonstrated in Listing 3.5.

Listing 3.5 Powerset of a Set

  class Array

   def powerset
    num = 2**size
    ps = Array.new(num, [])
    self.each_index do |i|
     a = 2**i
     b = 2**(i+1) - 1
     j = 0
     while j < num-1
      for j in j+a..j+b
       ps[j] += [self[i]]
      end
      j += 1
     end
    end
    ps
   end

  end

  x = [1, 2, 3]
  y = x.powerset
  # y is now:
  #  [[], [1], [2], [1,2], [3], [1,3], [2,3], [1,2,3]]

Randomizing an Array

Sometimes we want to scramble an array into a random order. The first example that might come to mind is a card game, but there are other circumstances, such as presenting a list of questions to a user in a random order, in which we might use this.

To accomplish this task, we can use rand in the Kernel module. Here's one way to do this:

 class Array

  def randomize
   arr=self.dup
   arr.collect { arr.slice!(rand arr.length) }
  end

  def randomize!
   arr=self.dup
   result = arr.collect { arr.slice!(rand arr.length) }
   self.replace result
  end

 end

 x = [1, 2, 3, 4, 5]
 y = x.randomize   # [3, 2, 4, 1 ,5]
 x.randomize!     # x is now [3, 5, 4, 1, 2]

The key to understanding this solution is knowing that the slice! method will return the value of an array element and, at the same time, delete that element from the array (so that it cannot be used again).

There are other ways to perform this operation. If you find a better one, let us know.

If we wanted simply to pick an array element at random (without disallowing duplicates), we could do that as follows.

  class Array

   def pick_random
    self[rand(self.length)]
   end

  end

Finally, remember that any time you are using rand, you can generate a predictable sequence (for example, for testing) simply by seeding with a known seed using srand.

Using Multidimensional Arrays

If you want to use multidimensional arrays for numerical purposes, an excellent library in the Ruby Application Archive called NArray (by Masahiro Tanaka) is available. If you want to use matrixes, you can use the matrix.rb standard library, as mentioned in Chapter 2, "Simple Data Tasks."

In Listing 3.6, we present a way of handling multidimensional arrays by overloading the [] and []= methods to map elements onto a nested array. The class Array3 presented here will handle three-dimensional arrays in a rudimentary fashion, but it is far from complete.

Listing 3.6 Three-dimensional Array

  class Array3

   def initialize
     @store = [[[]]]
   end

   def [](a,b,c)
    if @store[a]==nil ||
      @store[a][b]==nil ||
      @store[a][b][c]==nil
     return nil
    else
     return @store[a][b][c]
    end
   end

   def []=(a,b,c,x)
    @store[a] = [[]] if @store[a]==nil
    @store[a][b] = [] if @store[a][b]==nil
    @store[a][b][c] = x
   end

  end


  x = Array3.new
  x[0,0,0] = 5
  x[0,0,1] = 6
  x[1,2,3] = 99

  puts x[1,2,3]

Note that all we really gain here is the convenience of a "comma" notation [x,y,z] instead of the more C-like [x][y][z]. If the C-style notation is acceptable to you, you can just use nested arrays in Ruby. Another minor benefit is the prevention of the situation in which nil is the receiver for the bracket method.

Finding Elements in One Array but Not Another

Finding elements in one array but not another is simpler in Ruby than in many languages. It is a simple "set difference" problem:

  text = %w[the magic words are squeamish ossifrage]
  dictionary = %w[an are magic the them these words]
  # Find potential misspellings
  unknown = text - dictionary  # ["squeamish", "ossifrage"]

Transforming or Mapping Arrays

The collect method (part of Enumerable) is a useful little tool that proves to be a time and labor saver in many circumstances. If you are a Smalltalk programmer, this may be more intuitive than if you come from a C background.

This method simply operates on each element of an array in some arbitrary way to produce a new array. In other words, it "maps" an array onto another array (hence the synonym map). Here's an example:

  x = %w[alpha bravo charlie delta echo foxtrot]
  # Get the initial letters
  a = x.collect {|w| w[0..0]}    # %w[a b c d e f]
  # Get the string lengths
  b = x.collect {|w| w.length}    # [5, 5, 7, 5, 4, 7]
  # map is just an alias
  c = x.map {|w| w.length}      # [5, 5, 7, 5, 4, 7]

The in-place variant collect! (or map!) is also defined:

  x.collect! {|w| w.upcase}
  # x is now %w[ALPHA BRAVO CHARLIE DELTA ECHO FOXTROT]
  x.map! {|w| w.reverse}
  # x is now %w[AHPLA OVARB EILRAHC ATLED OHCE TORTXOF]

Removing nil Values from an Array

The compact method (or its in-place version compact!) will remove nil values from an array, leaving the rest untouched:

  a = [1, 2, nil, 3, nil, 4, 5]
  b = a.compact   # [1, 2, 3, 4, 5]
  a.compact!    # a is now [1, 2, 3, 4, 5]

Removing Specific Array Elements

It is easy to delete elements from a Ruby array, and there are many ways to do it. If you want to delete one specific element by index, delete_at is a good way:

  a = [10, 12, 14, 16, 18]
  a.delete_at(3)       # Returns 16
  # a is now [10, 12, 14, 18]
  a.delete_at(9)       # Returns nil (out of range)

If you want to delete all instances of a certain piece of data, delete will do the job. It will return the value of the objects deleted or nil if the value was not found. Here's an example:

  b = %w(spam spam bacon spam eggs ham spam)
  b.delete("spam")      # Returns "spam"
  # b is now ["bacon", "eggs", "ham"]
  b.delete("caviar")     # Returns nil

The delete method will also accept a block. This may be a little counterintuitive, though. All that happens is that the block is evaluated (potentially performing a wide range of operations) if the object is not found and the value of the block is returned, as shown here:

  c = ["alpha", "beta", "gamma", "delta"]
  c.delete("delta") { "Nonexistent" }
  # Returns "delta" (block is never evaluated)
  c.delete("omega") { "Nonexistent" }
  # Returns "Nonexistent"

The delete_if method will pass every element into the supplied block and delete the elements for which the block evaluates to true. It behaves similarly to reject!, except that the latter can return nil when the array remains unchanged. Here's an example:

  email = ["job offers", "greetings", "spam", "news items"]
  # Delete four-letter words
  email.delete_if {|x| x.length==4 }
  # email is now ["job offers", "greetings", "news items"]

The slice! method accesses the same elements as slice but deletes them from the array as it returns their values:

  x = [0, 2, 4, 6, 8, 10, 12, 14, 16]
  a = x.slice!(2)             # 4
  # x is now [0, 2, 6, 8, 10, 12, 14, 16]
  b = x.slice!(2,3)            # [6, 8, 10]
  # x is now [0, 2, 12, 14, 16]
  c = x.slice!(2..3)            # [12, 14]
  # x is now [0, 2, 16]

The shift and pop methods can be used for deleting array elements (for more about their intended uses, see the discussion of stacks and queues elsewhere in this chapter):

  x = [1, 2, 3, 4, 5]
  x.pop          # Delete the last element
  # x is now [1, 2, 3, 4]
  x.shift         # Delete the first element
  # x is now [2, 3, 4]

Finally, the clear method will delete all the elements in an array. It is equivalent to assigning an empty array to the variable, but it's marginally more efficient. Here's an example:

  x = [1, 2, 3]
  x.clear
  # x is now []

Concatenating and Appending onto Arrays

Very frequently we want to take an array and append an element or another array. There are many ways to do this with a Ruby array.

The "append" operator << will append an object onto an array; the return value is the array itself so that these operations can be "chained":

  x = [1, 5, 9]
  x << 13    # x is now [1, 5, 9, 13]
  x << 17 << 21 # x is now [1, 5, 9, 13, 17, 21]

Similar to the append are the unshift and push methods, which add to the beginning and end of an array, respectively. See the section "Using an Array As a Stack or Queue" for more information.

Arrays may be concatenated with the concat method or by using the + and += operators:

  x = [1,2]
  y = [3,4]
  z = [5,6]
  b = y + z     # [3,4,5,6]
  b += x      # [3,4,5,6,1,2]
  z.concat y    # z is now [5,6,3,4]

Using an Array As a Stack or Queue

The basic stack operations are push and pop, which add and remove items, respectively, at the end of an array. The basic queue operations are shift (which removes an item from the beginning of an array) and unshift (which adds an element to the beginning). The append operator, can also be used to add an item to the end of an array (basically a synonym for push).

Don't get confused. The shift and unshift methods work on the beginning of an array; the push, pop, and << methods work on the end.

For a better discussion of this topic, see the section "Working with Stacks and Queues."

Iterating over an Array

The Array class has the standard iterator each, as is to be expected. However, it also has other useful iterators.

The reverse_each method will iterate in reverse order. It is equivalent to using reverse and then each, but it is faster. Here's an example:

  words = %w(Son I am able she said)
  str = ""
  words.reverse_each { |w| str += "#{w} "}
  # str is now "said she able am I Son "

If we only want to iterate over the indexes, we can use each_index. Saying x.each_index is equivalent to saying (0..(x.size-1)).each (that is, iterating over the range of indexes).

The iterator each_with_index (mixed in from Comparable) will pass both the element and the index into the block, as shown here:

  x = ["alpha", "beta", "gamma"]
  x.each_with_index do |x,i|
   puts "Element #{i} is #{x}"
  end
  # Produces three lines of output

Suppose you wanted to iterate over an array in random order? The following example uses the iterator random_each (which simply invokes the randomize method from section "Randomizing an Array"):

  class Array

  # Assumes we have defined randomize

   def random_each
    temp = self.randomize
    temp.each {|x| yield x}
   end

  end

  dwarves = %w(Sleepy Dopey Happy Sneezy Grumpy Bashful Doc)
  list = ""
  dwarves.random_each {|x| list += "#{x} "}
  # list is now:
  # "Bashful Dopey Sleepy Happy Grumpy Doc Sneezy "
  # (Your mileage may vary.)

Interposing Delimiters to Form a String

Frequently we will want to insert delimiters in between array elements in a "fencepost" fashion; that is, we want to put delimiters between the elements, but not before the first one or after the last one. The method join will do this, as will the * operator:

  been_there = ["Veni", "vidi", "vici."]
  journal = been_there.join(", ")    # "Veni, vidi, vici."

  # Default delimiter is space
  letters = ["Phi","Mu","Alpha"]
  musicians = letters.join        # "Phi Mu Alpha"

  people = ["Bob","Carol","Ted","Alice"]
  movie = people * " and "
  # movie is now "Bob and Carol and Ted and Alice"

Note that if we really need to treat the last element differently, perhaps by inserting the word and, we can do it manually, like so:

  list = %w[A B C D E F]
  with_commas = list[0..-2]*", " + ", and " + list[-1]
  # with_commas is now "A, B, C, D, E, and F"

Reversing an Array

To reverse the order of an array, use the reverse or reverse! method:

  inputs = ["red", "green", "blue"]
  outputs = inputs.reverse     # ["green","blue","red"]
  priorities = %w(eat sleep code)
  priorities.reverse!        # ["code","sleep","eat"]

Removing Duplicate Elements from an Array

If you want to remove duplicate elements from an array, the uniq method (or its in-place mutator uniq!) will do the job:

  breakfast = %w[spam spam eggs ham eggs spam]
  lunch = breakfast.uniq  # ["spam","eggs","ham"]
  breakfast.uniq!     # breakfast has changed now

Interleaving Arrays

Suppose you want to take two arrays and "interleave" them so that the new array contains alternating elements from each of the two original ones. There must be a hundred ways to do this. Here is one way:

  a = [1, 2, 3, 4]
  b = ["a", "b", "c", "d"]
  c = []
  a.each_with_index { |x,i| c << x << b[i]}
  # c is now [1, "a", 2, "b", 3, "c", 4, "d"]

Counting Frequency of Values in an Array

There is no count method for arrays as there is for strings (to count the occurrences of each data item). Therefore, we've created one here:

  class Array

   def count
    k=Hash.new(0)
    self.each{|x| k[x]+=1 }
    k
   end

  end

  meal = %w[spam spam eggs ham eggs spam]
  items = meal.count
  # items is {"ham" => 1, "spam" => 3, "eggs" => 2}
  spams = items["spam"]  # 3

Note that a hash is returned. No pun intended.

Inverting an Array to Form a Hash

An array is used to associate an integer index with a piece of data. However, what if you want to invert that association (that is, associate the data with the index, thus producing a hash)? The following method will do just that:

  class Array

   def invert
    h={}
    self.each_with_index{|x,i| h[x]=i}
    h
   end

  end

  a = ["red","yellow","orange"]
  h = a.invert   # {"orange"=>2, "yellow"=>1, "red"=>0}

Synchronized Sorting of Multiple Arrays

Suppose you want to sort an array, but you have other arrays that corresponded with this one on an element-for-element basis. In other words, you don't want to get them out of sync. How would you do this?

The solution we present in Listing 3.7 will sort an array and gather the resulting set of indexes. The list of indexes (itself an array) can be applied to any other array to put its elements in the same order.

Listing 3.7 Synchronized Array Sorting

class Array

 def sort_index
  d=[]
  self.each_with_index{|x,i| d[i]=[x,i]}
  if block_given?
   d.sort {|x,y| yield x[0],y[0]}.collect{|x| x[1]}
  else
   d.sort.collect{|x| x[1]}
  end
 end

 def sort_by(ord=[])
  return nil if self.length!=ord.length
  self.indexes(*ord)
 end

end


a = [21, 33, 11, 34, 36, 24, 14]
p a
p b=a.sort_index
p a.sort_by b
p c=a.sort_index {|x,y| x%2 <=> y%2}

p a.sort_by c

Establishing a Default Value for New Array Elements

When an array grows and new (unassigned) elements are created, these elements default to nil values:

  a = Array.new
  a[0]="x"
  a[3]="y"
  # a is now ["x", nil, nil, "y"]

What if we want to set those new elements to some other value? As a specific instance of a general principle, we offer the ZArray class in Listing 3.8, which will default new unassigned elements to 0.

Listing 3.8 Specifying a Default for Array Elements

  class ZArray < Array

   def [](x)
    if x > size
     for i in size+1..x
      self[i]=0
     end
    end
    v = super(x)
   end

   def []=(x,v)
    max = size
    super(x,v)
    if size - max > 1
     (max..size-2).each do |i|
      self[i] = 0
     end
    end
   end

  end


  num = ZArray.new
  num[1] = 1
  num[2] = 4
  num[5] = 25
  # num is now [0, 1, 4, 0, 0, 25]
  • + Share This
  • 🔖 Save To Your Account
Ruby Way, The

This chapter is from the book

Ruby Way, The

Discussions

comments powered by Disqus