Enumerating Enumerable: Enumerable#cycle

by Joey deVilla on July 6, 2008

Welcome to another installment of Enumerating Enumerable, my series of articles in which I attempt to improve upon RubyDoc.org’s documentation for the Enumerable module. So far, I’ve covered the following methods in this series:

  1. all?
  2. any?
  3. collect / map
  4. count

In this installment, I’m going to cover a method added into Enumerable in Ruby 1.9: Enumerable#cycle. It’s particularly poorly-documented in RubyDoc.org, and there isn’t much written about it anywhere else.

Enumerable#cycle Quick Summary

Graphic representation of the \"cycle\" method in Ruby\'s \"Enumerable\" class.

In the simplest possible terms Given a collection, creates an infinitely-repeating ordered source of its items.
Ruby version 1.9 only
Expects An optional block to act on the items in the infinitely repeating until the break statement is encountered.
Returns An Enumerable::Enumerator that acts as the infinite source of the collection’s items.
RubyDoc.org’s entry Enumerable#cycle

Enumerable#cycle and Arrays

When used on an array and a block is provided, cycle passes each item to the block. Once the last array item has been passed to the block, cycle starts over again at the beginning of the array. cycle goes through the array forever unless it encounters a break statement in the block.

# This example is a slightly fancier version of the example
# you'll see at RubyDoc.org
days_of_week = %w{Monday Tuesday Wednesday Thursday Friday Saturday Sunday}
=> ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]

# This will print the days of the week over and over,
# forever until you stop it with control-c.
days_of_week.cycle {|day| puts day}
=> Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
Sunday
Monday
Tuesday
...

# The cycle can be broken with the "break" statement
days_of_week.cycle do |day|
    puts day
    break if day == "Friday"
end
=> Monday
Tuesday
Wednesday
Thursday
Friday
=> nil

The better use for cycle is when you use it to create an object that spits out the next item in a repeating sequence.

# Let's create an enumerator that we can use to give us
# days of the week in a repeating sequence.
days = days_of_week.cycle
=> #

# Enumerator's "next" method gives us the next day of the week
days.next
=> "Monday"

days.next
=> "Tuesday"

days.next
=> "Wednesday"

# Enumerator's "rewind" method resets the enumerator back
# to the first item
days.rewind
=> # "Monday"

...

# If you keep going, the enumerator will "wrap around" back
# to the beginning
days.next
=> "Saturday"

days.next
=> "Sunday"

days.next
=> "Monday"

How about one more example? We’ll use Enumerable‘s zip method, an array of dinner items and the “days of the week” cycle object to create a meal plan:

# Here's an array of international cuisine
dinners = ["Jerk chicken", "Lamb vindaloo", "Chicken fried steak", \
"Yeung Chow fried rice", "Tonkatsu", "Coq au Vin", "Chunky bacon", \
"Pierogies", "Salisbury steak", "Bibim Bap", \
"Roast beef", "Souvlaki"]
=> ["Jerk chicken", "Lamb vindaloo", "Chicken fried steak",
"Yeung Chow fried rice", "Tonkatsu", "Coq au Vin", "Chunky bacon",
"Pierogies", "Salisbury steak", "Bibim Bap",
"Roast beef", "Souvlaki"]

# Let's draw up a mean plan!
days.zip(dinners) {|daily_meal| p daily_meal}
=> ["Monday", "Jerk chicken"]
["Tuesday", "Lamb vindaloo"]
["Wednesday", "Chicken fried steak"]
["Thursday", "Yeung Chow fried rice"]
["Friday", "Tonkatsu"]
["Saturday", "Coq au Vin"]
["Sunday", "Chunky bacon"]
["Monday", "Pierogies"]
["Tuesday", "Salisbury steak"]
["Wednesday", "Bibim Bap"]
["Thursday", "Roast beef"]
["Friday", "Souvlaki"]

# If we want to store our meal plan, we can do it this way
meal_plan = []
=> []

days.zip(dinners) {|daily_meal| meal_plan << daily_meal}
=> nil

meal_plan
=> [["Monday", "Jerk chicken"], ["Tuesday", "Lamb vindaloo"],
["Wednesday", "Chicken fried steak"], ["Thursday", "Yeung Chow fried rice"],
["Friday", "Tonkatsu"], ["Saturday", "Coq au Vin"],
["Sunday", "Chunky bacon"], ["Monday", "Pierogies"],
["Tuesday", "Salisbury steak"], ["Wednesday", "Bibim Bap"],
["Thursday", "Roast beef"], ["Friday", "Souvlaki"]]

Enumerable#cycle and Hashes

When used on a hash and a block is provided, collect and map pass each key/value pair in the hash to the block, which you can “catch” as either:

  1. A two-element array, with the key as element 0 and its corresponding value as element 1, or
  2. Two separate items, with the key as the first item and its corresponding value as the second item.

Each key/value pair is passed to the block, where the operation in the block is performed on the item. Once the last hash item has been passed to the block, cycle starts over again at the beginning of the hash. cycle goes through the hash forever unless it encounters a break statement in the block.

# Here we'll take the RubyDoc.org example for "cycle"
# but apply it to a hash of the crew of the Enterprise-D
crew = {:captain => "Picard", :first_officer => "Riker", \
:science_officer => "Data", :tactical_officer => "Worf"}
=> {:captain=>"Picard", :first_officer=>"Riker",
:science_officer=>"Data", :tactical_officer=>"Worf"}

# This will print the crew's rank and name over and over,
# forever until you stop it with control-c.
crew.cycle {|crewmember| p crewmember}
=> [:captain, "Picard"]
[:first_officer, "Riker"]
[:science_officer, "Data"]
[:tactical_officer, "Worf"]
[:captain, "Picard"]
[:first_officer, "Riker"]
[:science_officer, "Data"]
[:tactical_officer, "Worf"]
[:captain, "Picard"]
[:first_officer, "Riker"]
[:science_officer, "Data"]
[:tactical_officer, "Worf"]
[:captain, "Picard"]
[:first_officer, "Riker"]
...

# The cycle can be broken with the "break" statement
crew.cycle do |rank, name|
    puts "Rank: #{rank} - Name: #{name}"
    break if rank == :science_officer
end
=> Rank: captain - Name: Picard
Rank: first_officer - Name: Riker
Rank: science_officer - Name: Data
=> nil

We can use the same technique that we used with arrays and use cycle to create an object that spits out the next item in a cycling hash. Each time we get an item from the object, it comes in the form of a two-element array where the first element is the key and the second element is the corresponding value.

# Let's create an enumerator that we can use to give us
# the Enterprise-D crew's rank and name in a repeating sequence.
crewmembers = crew.cycle
=> #

# numerator’s “next” method gives us the next crewmember
crewmembers.next
=> [:captain, "Picard"]

crewmembers.next
=> [:first_officer, "Riker"]

crewmembers.next
=> [:science_officer, "Data"]

# Enumerator’s “rewind” method resets the enumerator back
# to the first item
crewmembers.rewind
=> #

crewmembers.next
=> [:captain, "Picard"]

...

# If you keep going, the enumerator will “wrap around” back
# to the beginning
crewmembers.next
=> [:tactical_officer, "Worf"]

crewmembers.next
=> [:captain, "Picard"]

crewmembers.next
=> [:first_officer, "Riker"]

Let’s try one more example! In this one, we’ll use cycle to create three different enumerators — two made from arrays, one from a hash — to assign cooking chores for the Enterprise-D crew for the next ten days.

# Create an enumerator for days
days = %w{Monday Tuesday Wednesday Thursday Friday Saturday Sunday}.cycle
=> #

# Create an enumerator for dinners
dinners = ["Jerk chicken", "Lamb vindaloo", "Chicken fried steak", \
"Yeung Chow fried rice", "Tonkatsu", "Coq au Vin", "Chunky bacon", \
"Pierogies", "Salisbury steak", "Bibim Bap", \
"Roast beef", "Souvlaki"].cycle
=> #

# Make sure we're starting from the beginning of the crew hash
crewmembers.rewind
=> #

# Let's assign dinner-cooking duties to the crew!
10.times do
    day = days.next
    dinner = dinners.next
    chef = crewmembers.next[1]
    puts "On #{day}, Crewman #{chef} will prepare #{dinner}."
end
=> On Monday, Crewman Picard will prepare Jerk chicken.
On Tuesday, Crewman Riker will prepare Lamb vindaloo.
On Wednesday, Crewman Data will prepare Chicken fried steak.
On Thursday, Crewman Worf will prepare Yeung Chow fried rice.
On Friday, Crewman Picard will prepare Tonkatsu.
On Saturday, Crewman Riker will prepare Coq au Vin.
On Sunday, Crewman Data will prepare Chunky bacon.
On Monday, Crewman Worf will prepare Pierogies.
On Tuesday, Crewman Picard will prepare Salisbury steak.
On Wednesday, Crewman Riker will prepare Bibim Bap.

Previous post:

Next post: