Today we're going to talk about slice_before
, slice_when
and slice_after
. These are super-useful methods when you need to group items in an array or other enumerable based on arbitrary criteria.
You're probably familiar with Array#slice
. It lets you pull out a subset of an array based on a range of indices:
a = ["a", "b", "c"]
a.slice(1, 2)
# => ["b", "c"]
This is useful, but it can't be used with enumerables, because enumerables don't have indices.
The slice_before
, slice_when
and slice_after
methods don't rely on indices - so you can use them with any enumerable. Let's take a look!
Using Enumerable#slice_before
Enumerable#slice_before
splits and enumerable into groups at the point before a match is made.
The match is made via the ===
operator, which means that you can match all kinds of things.
Value Matches
You can match a single value. That should be obvious. :)
a = ["a", "b", "c"]
a.slice_before("b").to_a
# => [["a"], ["b", "c"]]
Regular Expression Matches
You can use regular expressions for more complex textual matching.
a = ["000", "b", "999"]
a.slice_before(/[a-z]/).to_a
# => [["000"], ["b", "999"]]
Range Matches
If you're working with numbers, you can slice the array based on a range.
a = [100, 200, 300]
a.slice_before(150..250).to_a
# => [[100], [200, 300]]
Class matches
This one may seem a little strange to you, but it's fully in keeping with the behavior of the ===
operator.
a = [1, "200", 1.3]
a.slice_before(String).to_a
# => [[1], ["200", 1.3]]
Using a block
If none of the other options are flexible enough for you can always to find a match programmatically with a block.
a = [1, 2, 3, 4, 5]
a.slice_before do |item|
item % 2 == 0
end
# => [[1], [2, 3], [4, 5]]
Using Enumerable#slice_after
Enumerable#slice_after
works exactly like Enumerable#slice_before
except that the slice happens after the match. Go figure. :-)
a = ["a", "b", "c"]
a.slice_after("b").to_a
# => [["a", "b"], ["c"]]
Of course, you can match using regular expressions, ranges, and blocks. I'm not going to show examples of those here because it would be tedious.
Using Enumerable#slice_when
Enumerable#slice_when
is a different beast from slice_before
and slice_after
. Instead of matching a single item in the array, you match a pair of adjacent items.
This means that you can group items based on the "edges" between them.
For example, here we group items based on "nearness" to their adjacent items.
a = [1, 2, 3, 100, 101, 102]
# Create a new group when the difference
# between two adjacent items is > 10.
a.slice_when do |x, y|
(y - x) > 10
end
# => [[1, 2, 3], [100, 101, 102]]
If you're interested in learning more, check out the Ruby Docs for slice_when
. They have several great code examples.
Arrays vs Enumerables
I've used arrays in most of the examples above because arrays are easy to understand. You should keep in mind though that you can use slice_before
, slice_when
and slice_after
with any enumerable.
For example, if you had a file containing a bunch of emails, you could split out the individual emails using slice_before
. The code below is taken from the docs.
open("mbox") { |f|
f.slice_before { |line|
line.start_with? "From "
}.each { |mail|
puts mail
}
And be sure to notice that the slice methods don't return arrays. They return enumerables. That means that you can use map
, each
and all your other favorite enumerable methods on the. Heck, you could even do another split. :)