Extracting Portions of Arrays
Problem
Given an array, you want to retrieve the elements of the array that occupy certain positions or have certain properties. You might to do this in a way that removes the matching elements from the original array.
Solution
To gather a chunk of an array without modifying it, use the array retrieval operator Array#[], or its alias Array#slice.
The array retrieval operator has three forms, which are the same as the corresponding forms for substring accesses. The simplest and most common form is array[index].It takes a number as input, treats it as an index into the array, and returns the element at that index. If the input is negative, it counts from the end of the array. If the array is smaller than the index, it returns nil. If performance is a big consideration for you, Array#at will do the same thing, and it's a little faster than Array#[]:
a = ("a".."h").to_a # => ["a", "b", "c", "d", "e", "f", "g", "h"] a[0] # => "a" a[1] # => "b" a.at(1) # => "b" a.slice(1) # => "b" a[-1] # => "h" a[-2] # => "g" a[1000] # => nil a[-1000] # => nil
The second form is array[range]. This form retrieves every element identified by an index in the given range, and returns those elements as a new array.
A range in which both numbers are negative will retrieve elements counting from the end of the array. You can mix positive and negative indices where that makes sense:
a[2..5] # => ["c", "d", "e", "f"] a[2…5] # => ["c", "d", "e"] a[0..0] # => ["a"] a[1..-4] # => ["b", "c", "d", "e"] a[5..1000] # => ["f", "g", "h"] a[2..0] # => [] a[0…0] # => [] a[-3..2] # => []
The third form is array[start_index, length]. This is equivalent to array[range. new(start_index…start_index+length)].
a[2, 4] # => ["c", "d", "e", "f"] a[2, 3] # => ["c", "d", "e"] a[0, 1] # => ["a"] a[1, 2] # => ["b", "c"] a[-4, 2] # => ["e", "f"] a[5, 1000] # => ["f", "g", "h"]
To remove a slice from the array, use Array#slice!. This method takes the same arguments and returns the same results as Array#slice, but as a side effect, the objects it retrieves are removed from the array.
a.slice!(2..5) # => ["c", "d", "e", "f"] a # => ["a", "b", "g", "h"] a.slice!(0) # => "a" a # => ["b", "g", "h"] a.slice!(1,2) # => ["g", "h"] a # => ["b"]
Discussion
The Array methods [], slice, and slice! work well if you need to extract one particular elements, or a set of adjacent elements. There are two other main possibilities: you might need to retrieve the elements at an arbitrary set of indexes, or (a catch-all) you might need to retrieve all elements with a certain property that can be determined with a code block.
To nondestructively gather the elements at particular indexes in an array, pass in any number of indices to Array#values_at. Results will be returned in a new array, in the same order they were requested.
a = ("a".."h").to_a # => ["a", "b", "c", "d", "e", "f", "g", "h"] a.values_at(0) # => ["a"] a.values_at(1, 0, -2) # => ["b", "a", "g"] a.values_at(4, 6, 6, 7, 4, 0, 3)# => ["e", "g", "g", "h", "e", "a", "d"]
Enumerable#find_all finds all elements in an array (or other class with Enumerable mixed in)for which the specified code block returns true. Enumerable#reject will find all elements for which the specified code block returns false.
a.find_all { |x| x < "e" } # => ["a", "b", "c", "d"] a.reject { |x| x < "e" } # => ["e", "f", "g", "h"]
To find all elements in an array that match a regular expression, you can use Enumerable#grep instead of defining a block that does the regular expression match:
a.grep /[aeiou]/ # => ["a", "e"] a.grep /[^g]/ # => ["a", "b", "c", "d", "e", "f", "h"]
It's a little tricky to implement a destructive version of Array#values_at, because removing one element from an array changes the indexes of all subsequent elements. We can let Ruby do the work, though, by replacing each element we want to remove with a dummy object that we know cannot already be present in the array. We can then use the C-backed method Array#delete to remove all instances of the dummy object from the array. This is much faster than using Array#slice! to remove elements one at a time, because each call to Array#slice! forces Ruby to rearrange the array to be contiguous.
If you know that your array contains no nil values, you can set your undesired values to nil, then use use Array#compress! to remove them. The solution below is more general.
class Array def strip_values_at!(*args) #For each mentioned index, replace its value with a dummy object. values = [] dummy = Object.new args.each do |i| if i < size values << self[i] self[i] = dummy end #Strip out the dummy object. delete(dummy) return values end end end a = ("a".."h").to_a a.strip_values_at!(1, 0, -2) # => ["b", "a", "g"] a # => ["c", "d", "e", "f", "h"] a.strip_values_at!(1000) # => [] a # => ["c", "d", "e", "f", "h"]
Array#reject! removes all items from an array that match a code block, but it doesn't return the removed items, so it won't do for a destructive equivalent of Enumerable#find_all. This implementation of a method called exTRact! picks up where Array#reject! leaves off:
class Array def extract! ary = self.dup self.reject! { |x| yield x } ary - self end end a = ("a".."h").to_a a.extract! { |x| x < "e" && x != "b" } # => ["a", "c", "d"] a # => ["b", "e", "f", "g", "h"]
Finally, a convenience method called grep_extract! provides a method that destructively approximates the behavior of Enumerable#grep.
class Array def grep_extract!(re) extract! { |x| re.match(x) } end end a = ("a".."h").to_a a.grep_extract!(/[aeiou]/) # => ["a", "e"] a # => ["b", "c", "d", "f", "g", "h"]
See Also
- Strings support the array lookup operator, slice, slice!, and all the methods of Enumerable, so you can treat them like arrays in many respects; see Recipe 1.13, "Getting the Parts of a String You Want"