ghammell'sBlog

The #cycle Method

Ruby's enumerable module includes a nifty little method called #cycle. It's just like the #each method, only it won't stop iterating unless you tell it too. Once it iterates throught the last object, it will start right back at the beginning and keep going, unless you've specified a specific amount of cycles for it to run, or a specific breaking point. Lets look at an example. If we ran the code:

[1,2,3,4,5].each {|num| p num}
puts
[1,2,3,4,5].cycle(1) {|num| p num}

We'd get:

12345
12345

The same results in both scenarios. How about if we adust the parameter on the #cycle method?

[1,2,3,4,5].each {|num| p num}
puts
[1,2,3,4,5].cycle(3) {|num| p num}

We'd get:

12345
123451234512345

See? The #cycle method is just like the #each method, only it performs the code in the block as many times as designated.

Another interesting use of the #cycle method is that it can be used in conjunction with break to only stop running when a specific event has occured. As a simple example, lets try running something like:

i = 0
(1..10).to_a.cycle {|num| print "#{num} "; i += 1; break if i == 15}

The output:

1 2 3 4 5 6 7 8 9 10 1 2 3 4 5

Notice how it stops printing numbers halfway through the second cycle?

Here, we quickly converted the range 1-10 to an array, and ran #cycle on it. But we didn't include a parameter to tell the code when to stop running. The only reason this sucker stops at all is because of the break we input at the end of the block. By including a counter "i" and increasing the value of it at every iteration through "i += 1", we can tell the code exactly when we want it to stop running, even if its only partially through the current cycle.

There are a lot of simple ways that #cycle can be used to quickly perform some tasks. I won't get too into those because there are other great resources explaining them. If you're interested, try:
Global Nerdy

Instead, I figured I'd dive into what I think would be practical applications of the #cycle method.

Let's say you and you're friends are lazy and want to split up the studying for a really boring class that you're all taking. You might decide to divvy up the chapters of the book by cycling through your group of friends - each taking one chapter, and then starting from the first person again. It may behoove you to write some code to figure this out. You might start with an array including your groups names:

team = [:mike, :adam, :emily, :sarah, :ken, :barbie]

Let's assume there are 100 chapters to allocate. You might then write:

i = 1
counts = {}

team.cycle {|name| counts[name] == nil ? counts[name] = [i] : counts[name] << i; i += 1; break if i == 101}

What's really happening here? Well, each time we iterate over a name:

Now, if we ran:

p counts

We'd get:

{:adam=>[2, 8, 14, 20, 26, 32, 38, 44, 50, 56, 62, 68, 74, 80, 86, 92, 98], :emily=>[3, 9, 15, 21, 27, 33, 39, 45, 51, 57, 63, 69, 75, 81, 87, 93, 99], :sarah=>[4, 10, 16, 22, 28, 34, 40, 46, 52, 58, 64, 70, 76, 82, 88, 94, 100], :ken=>[5, 11, 17, 23, 29, 35, 41, 47, 53, 59, 65, 71, 77, 83, 89, 95], :barbie=>[6, 12, 18, 24, 30, 36, 42, 48, 54, 60, 66, 72, 78, 84, 90, 96], :mike=>[1, 7, 13, 19, 25, 31, 37, 43, 49, 55, 61, 67, 73, 79, 85, 91, 97]}

We might clean up the output a little by using:

counts.each {|k, v| p k, v}

:adam
[2, 8, 14, 20, 26, 32, 38, 44, 50, 56, 62, 68, 74, 80, 86, 92, 98]
:emily
[3, 9, 15, 21, 27, 33, 39, 45, 51, 57, 63, 69, 75, 81, 87, 93, 99]
:sarah
[4, 10, 16, 22, 28, 34, 40, 46, 52, 58, 64, 70, 76, 82, 88, 94, 100]
:ken
[5, 11, 17, 23, 29, 35, 41, 47, 53, 59, 65, 71, 77, 83, 89, 95]
:barbie
[6, 12, 18, 24, 30, 36, 42, 48, 54, 60, 66, 72, 78, 84, 90, 96]
:mike
[1, 7, 13, 19, 25, 31, 37, 43, 49, 55, 61, 67, 73, 79, 85, 91, 97]

There you have it - a simple way of allocating all 100 chapters of the book to you and your team. A way that you can easily adjust by adding or removing members, or by changing the number of chapters - all because we're using the #cycle method.

What if you wanted to assign random chapters to you and your friends? This gets a bit more complicated, and before I take you through this let me preface this by saying that there are likely better ways of getting to the result in this example. I'd love to hear your feedback if you're aware of any!

Here is the ending code:

def randassign(team, count)
numbers = []
i = 0
count.times {|num| numbers[i] = i+1; i += 1}

rands = {}

j = 0
team.cycle do |x|

position = rand(count-j)

if rands[x] == nil
rands[x] = [numbers[position]]
else
rands[x] << numbers[position]
end

j+=1
numbers.delete_at(position)
break if numbers.count == 0
end

rands.each {|name, numbers| p name, numbers.sort}

end

randassign(team,50)

:adam
[2, 5, 10, 14, 16, 25, 44, 48, 50]
:emily
[1, 4, 7, 26, 28, 33, 38, 40]
:sarah
[17, 21, 22, 23, 32, 42, 43, 47]
:ken
[3, 13, 15, 19, 27, 29, 37, 39]
:barbie
[6, 11, 12, 18, 20, 36, 41, 45]
:mike
[8, 9, 24, 30, 31, 34, 35, 46, 49]

Ran again, the output is:

:adam
[1, 17, 21, 23, 27, 30, 44, 47, 48]
:emily
[3, 7, 11, 19, 22, 38, 41, 43]
:sarah
[2, 5, 12, 16, 20, 25, 36, 45]
:ken
[8, 10, 14, 18, 28, 34, 35, 49]
:barbie
[4, 9, 31, 32, 37, 39, 42, 46]
:mike
[6, 13, 15, 24, 26, 29, 33, 40, 50]

Alright, lets break this down.

def randassign(team, count)
numbers = []
i = 0
count.times {|num| numbers[i] = i+1; i += 1}

Here, on the first line, I am defining a method called 'randassign', that takes two paramaters as inputs - an array (which is meant to include the team names), and a count (which would be the total chapters to assign). The next three lines build an array called 'numbers' with the numbers 1 through 'count' as objects.

rands = {}

This line just establishes an empty hash that will ultimately hold each team member name as a key, with corresponding arrays of chapter numbers as values.

Now comes our implementation of #cycle:

j = 0
team.cycle do |x|

position = rand(count-j)

if rands[x] == nil
rands[x] = [numbers[position]]
else
rands[x] << numbers[position]
end

j+=1
numbers.delete_at(position)
break if numbers.count == 0
end

What are we trying to do here? Well, the idea is that, every time we allocate a random number to a team member, that number will be deleted from the 'numbers' array, so that when we move to the next team member we have a smaller base to choose from and will avoid allocating the same number. Lets start at the top:

j = 0
team.cycle do |x|

position = rand(count-j)

We've established a counter 'j' which will be incremented every time we iterate over a team member name using the #cycle method. With every iteration, we establish a position, which is set to a random number between 0 and 'count' using rand(count-j). As you can see, as 'j' is incremented, the random number being generated will have a smaller and smaller range of possible values.

Next, we have:

if rands[x] == nil
rands[x] = [numbers[position]]
else
rands[x] << numbers[position]
end

We're asking, does the rands hash include a key with the current name, and is its value 'nil'? If so, set the value of that key equal to an array that includes the object from the 'numbers' array at the position calculated with the rand function. If not (meaning this key already has a value that is an array), then add the object from the 'numbers' array to the value.

The next 3 lines are really important:

j+=1
numbers.delete_at(position)
break if numbers.count == 0
end

Here we increase 'j' so that in our next iteration, the 'rand' function has fewer numbers to select from. Then we delete the object in the numbers array at the current position. This is crucial because it means that chapter number is no longer available to be allocated to another team member. Lastly, we break the #cycle method once there are no more objects in the numbers array.

Finally, the last line of code just outputs the completed rand hash in a clean, sorted manner:

rands.each {|name, numbers| p name, numbers.sort}

Try it for yourself!

I had a lot of fun working through this exercise and learning about #cycle. Feel free to tell me my example is terrible and that there are better ways of doing that. Hopefully you learned something from this post!


Copyright: Gary Hammell 2014