Enumerable: Any Way You Slice It...
This is part of the Back to Basics series.
See the introductory article for an explanation of why this series is important.
Last time, on Enumerable: Go Fetch!, we learned how to fetch simple sequences of items from the front or back of a collection.
This time we are going to cover more complicated slicing of collections into sets of smaller sequences.
Here is a quick summary:
each_slice(n)
slice_when { |item_before, item_after| ... }
slice_after { |item| ... }
slice_before { |item| ... }
chunk { |item| ... }
chunk_while { |item_before, item_after| ... }
Remember when we looked at each_cons(n)
back in Scratching that Each?
This method was like a sliding window n
items wide that moved one item at a time down the list.
The problem is that this window covers the same items multiple times (and different numbers of times near the start and end of the list).
What if instead we wanted to simply process the list n
items at a time with each item appearing only once?
Enter each_slice(n)
, which does just that.
%w[a b c d e f g h].each_slice(3) do |slice|
p slice
end
["a", "b", "c"]
["d", "e", "f"]
["g", "h"]
Note that the last slice was smaller than our requested length, because we ran out of items in the list.
Note that the return value from the rest of the methods is an “enumerator” (which responds to
next
but also implements Enumerable), not a simple array! You will need to call another Enumerable method likeeach
orto_a
to do something useful with this value.
Let’s say you wanted to break a stream of numbers into smaller sets of non-decreasing series.
You could use slice_when { |item_before, item_after| ... }
to accomplish this:
[1, 1, 2, 3, 5, 2, 4, 6, 8, 0, -10, -5]
.slice_when { |number_before, number_after| number_before > number_after }
.each { |sequence| p sequence }
[1, 1, 2, 3, 5],
[2, 4, 6, 8],
[0],
[-10, -5]
Next, let’s break up a stream of characters at line breaks, including the newline character at the end.
We can use slice_after { |item| ... }
for this purpose:
poem = <<~POEM
Haikus are easy
But sometimes they don't make sense..
Refrigerator
POEM
poem.chars.slice_after { |char| char == "\n" }.each { |line| puts line.join }
Haikus are easy
But sometimes they don't make sense..
Refrigerator
Just for fun, let’s try doubling up the blank spaces between the lines.
We can use slice_before { |item| ... }
to accomplish this:
poem = <<~POEM
Haikus are easy
But sometimes they don't make sense..
Refrigerator
POEM
poem.chars.slice_before { |char| char == "\n" }.each { |line| puts line.join }
Haikus are easy
But sometimes they don't make sense..
Refrigerator
What is happening here is that the \n
from the end of each line is ending up at at the front of the next line (or by itself as the “last” line), so we get two newlines in a row with puts
(which adds a newline at the end of a string if one is missing).
We could call print
(which doesn’t modify the string) instead to see the original.
poem = <<~POEM
Haikus are easy
But sometimes they don't make sense..
Refrigerator
POEM
poem.chars.slice_before { |char| char == "\n" }.each { |line| print line.join }
Haikus are easy
But sometimes they don't make sense..
Refrigerator
Let’s group runs of words that start with the same letter together.
We can use chunk { |item| ... }
for this purpose.
%w[apple cucumber clementine banana celery apricot avocado]
.chunk { |item| item.chars.first }
.each { |letter, words| puts "#{letter}: #{words.join(', ')}"}
a: apple
c: cucumber, clementine
b: banana
c: celery
a: apricot, avocado
As you can see, we get two values back for each chunk: the value from the block, and the sequence of items that all produced that value.
However, what we really wanted was all the words that started with each character together, so let’s try sorting the list first:
%w[apple cucumber clementine banana celery apricot avocado]
.sort # new
.chunk { |item| item.chars.first }
.each { |letter, words| puts "#{letter}: #{words.join(', ')}"}
a: apple, apricot, avocado
b: banana
c: celery, clementine, cucumber
The opposite of slice_when
is chunk_while { |item_before, item_after| ... }
.
Let’s reimplement our example above:
[1, 1, 2, 3, 5, 2, 4, 6, 8, 0, -10, -5]
# .slice_when { |number_before, number_after| number_before > number_after }
.chunk_while { |number_before, number_after| number_before <= number_after }
.each { |sequence| p sequence }
[1, 1, 2, 3, 5],
[2, 4, 6, 8],
[0],
[-10, -5]
All we had to do was swap the boolean logic to get the same result.
To read the rest of the series: click here