Working with Ruby
Hi, I am Jan. This is my old Ruby blog. I still post about Ruby, but I now do it on idiosyncratic-ruby.com. You should also install Irbtools to improve your IRB.

New Array and Enumerable methods in Ruby 1.9.2: keep_if, chunk...

In Ruby, dealing with Arrays and similar objects is pretty fun. And we have gotten more possibilities with Ruby 1.9.2 :)

The NEWS file says:

  * Array
    * new method:
      * Array#keep_if
      * Array#repeated_combination
      * Array#repeated_permutation
      * Array#rotate
      * Array#rotate!
      * Array#select!
      * Array#sort_by!

    * extended methods:
      * Array#{uniq,uniq!,product} can take a block.
...
  * Enumerable
    * New methods:
      * Enumerable#chunk
      * Enumerable#collect_concat
      * Enumerable#each_entry
      * Enumerable#flat_map
      * Enumerable#slice_before

Let’s take a closer look!

select! and keep_if

The select method (only choose the elements for which the block evaluates to true) got a mutator version select!. It does almost the same like the new keep_if method – with one subtle difference: select! returns nil if no changes were made, keep_if always returns the object.

more combination and permutation

Ruby 1.9 introduced the useful methods combination and permutation. Now there are also repeated_combination and repeated_permutation:

 1
2
3
4
5
6
7
8
>> [1,2,3].combination(2).to_a
=> [[1, 2], [1, 3], [2, 3]]
>> [1,2,3].repeated_combination(2).to_a
=> [[1, 1], [1, 2], [1, 3], [2, 2], [2, 3], [3, 3]]
>> [1,2,3].permutation(2).to_a
=> [[1, 2], [1, 3], [2, 1], [2, 3], [3, 1], [3, 2]]
>> [1,2,3].repeated_permutation(2).to_a
=> [[1, 1], [1, 2], [1, 3], [2, 1], [2, 2], [2, 3], [3, 1], [3, 2], [3, 3]]

rotate / rotate!

This method removes the first element and appends it. You can pass an integer, how many steps it should “cycle” (negative values are possible). It does something like:

 1
2
3
4
5
6
class Array
  def rotate2(n=1)
    new_start = n % self.size
    self[new_start..-1] + self[0...new_start]
  end
end

small changes: sort_by, uniq / uniq!, product

  • sort_by (the block defines, how the Array should be sorted) also got a mutator version: sort_by!.
  • product (combine all elements from both Arrays) now also accepts a block which yields every result, instead of returning it.
  • uniq (remove duplicates) can now take a block, example from the docs:
 1
2
c = [ "a:def", "a:xyz", "b:abc", "b:xyz", "c:jkl" ]
c.uniq {|s| s[/^\w+/]} #=> [ "a:def", "b:abc", "c:jkl" ]

flat_map and its alias collect_concat

This method works like map, but if an element is an Enumerable itself, the applied block is also run for each of its child elements (but not recursively).

 1
[[1,2],[3,[4,5]]].flat_map{|i|i} #=> [1, 2, 3, [4,5]]

I do not know (yet), if this is useful (and if it wouldn’t be cooler if it did something like .flatten.map), but we will see…

each_entry

Each entry is like each, but it treats multiple yield arguments as a single array (so yield 1,2 implicitly becomes yield [1,2]). Mostly, the result is similar to each’s, but there are some occasions where it does matter.

chunk

This one is interesting, but it is a little bit strange to use. It splits self into multiple Enumerators, using the rule given in the block. It keeps together those parts that “match” in series. It passes the result of the “filter” rule and an Enumerator of the successive elements – Look at these two examples:

 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(1..42).chunk{|n| n%11 == 0}.each{|result, elements|
  puts "#{result}: #{elements*' - '}"
}
# outputs:
# false: 1 - 2 - 3 - 4 - 5 - 6 - 7 - 8 - 9 - 10
# true: 11
# false: 12 - 13 - 14 - 15 - 16 - 17 - 18 - 19 - 20 - 21
# true: 22
# false: 23 - 24 - 25 - 26 - 27 - 28 - 29 - 30 - 31 - 32
# true: 33
# false: 34 - 35 - 36 - 37 - 38 - 39 - 40 - 41 - 42

# you could (ab)use this to order numbers in quotient rings:
a=(0..4).map{[]}
(1..42).chunk{|n| n%5}.each{|remainder, elements| a[remainder] += elements}
p a
# outputs:
# [[5, 10, 15, 20, 25, 30, 35, 40], [1, 6, 11, 16, 21, 26, 31, 36, 41], [2, 7, 12, 17, 22, 27, 32, 37, 42], [3, 8, 13, 18, 23, 28, 33, 38], [4, 9, 14, 19, 24, 29, 34, 39]]

The chunk block can also return some special values: Symbols that begin with an underscore. Currently, there are two special symbols supported:

  • :_separator (or nil) – the element is dropped
  • :_alone – the element always gets its single chunk

Some advanced examples can be found in the docs.

slice_before

This method also lets you split the Enumerable. You simply specify a pattern/a block which has to match/be true and it splits before that element:

 1
2
3
4
5
6
7
8
9
10
11
%w|Ruby is 2 parts Perl, 1 part Python, and 1 part Smalltalk|.slice_before(/\d/).to_a
#=> [["Ruby", "is"], ["2", "parts", "Perl,"], ["1", "part", "Python,", "and"], ["1", "part", "Smalltalk"]]

# a more complex version from the docs (slightly modified):
a = [0,2,3,4,6,7,9]
prev = a[0]
a.slice_before {|cur|
  prev, prev2 = cur, prev  # one step further
  prev2 + 1 != prev        # two ago != one ago ? --> new slice
}.to_a
# => [[0], [2, 3, 4], [6, 7], [9]]

What’s next?

It is quite interesting to observe, in which direction Ruby moves with this methods. Some of them where just “missing” previously (e.g. select!). However, some others seem to be for a very special purpose only (chunk) – but maybe it is just a false interpretation on the face of it ;)

Creative Commons License

Sohan | July 05, 2010

DrinkRails.com linked to your post as one of the top ruby on rails related blogs of the day.

J-_-L | July 05, 2010

Thank you, Sohan.

zhan | July 06, 2010

cool!!

Michael | July 06, 2010

I believe Listing 5 line 2 should read:
puts "#{result}: #{elements*' - '}"

For sake of perfect code clarity. :-)

J-_-L | July 06, 2010

thx, fixed it.

Anonymous | July 06, 2010

Are 1.9.2 docs online anywhere or do you have to generate it from the installed code? https://ruby-doc.org/ruby-1.9/index.html is outdated.

J-_-L | July 06, 2010

Me neither. I have used ri for my researches. As you have mentioned, you can easily create an offline version with one command: https://ruby.janlelis.de/4-create-an-offline-version-of-the-ruby-docs

Jeremy Voorhis | July 06, 2010

flat_map / collect_concat seems to be equivalent to concatMap in Haskell. If you're familiar with Haskell types, the function type of concatMap might suggest some uses. https://www.haskell.org/ghc/docs/6.12.1/html/libraries/base/Data-List.html#v%3AconcatMap

Anonymous | July 13, 2010

Good post. Thanks.

Stefan Kanev | July 13, 2010

I've missed collect_concat quite a number of times. The problem with flatten.map is that it flattens nested arrays, i.e., would have different behavior with your example.

Juan | April 29, 2011

Thank you, It's a very interesting article. I've finally understood the chunk method