Sandwich Scheduling: a look at Ruby's Array cycle method
- By Tim Ash
- 28 September
How Ruby can help you schedule your sandwiches
Another way Ruby can help you keep track of your lockdown exercise routine
Like Charlotte in the previous post, Deborah has had to make some changes to her exercise routine due to the coronavirus lockdown. She can only exercise outside by herself, and can’t go to the gym at all. She’s been keeping a record of what she’s done each day in an Array:
daily_workout_log = ["Weights: squats", "weights: benchpresses", "weights: deadlifts", "Rest", "Rest", "Bike: 25km", "Run: 10km", "Run: 5km", "bike: 50km", "BIKE: 30km"]
Deborah would like to summarise this in an easy-to-read format, leaving out the details of each exercise session; something along the lines of: “Weights: 3 days; Rest: 2 days” etc
This looks like a job for Ruby’s Enumerable chunk method. This will group together consecutive items based on the value returned by the supplied block. If that doesn’t make much sense, an example might help:
daily_workout_log.chunk{ |item| item }.to_a
would result in:
[["Weights: squats", ["Weights: squats"]], ["Weights: benchpresses", ["Weights: benchpresses"]], ["Weights: deadlifts", ["Weights: deadlifts"]], ["Rest", ["Rest", "Rest"]], ["Bike: 25km", ["Bike: 25km"]], ["Run: 10km", ["Run: 10km"]], ["Run: 5km", ["Run: 5km"]], ["bike: 50km", ["bike: 50km"]], ["BIKE: 30km", ["BIKE: 30km"]]]
That might look a bit like an unhelpful mess of nested arrays when you first look at it, and I’d find it hard to disagree. Note, however, that the two days where Deborah recorded as “rest” have been combined. Our next step is going to be to remove the details of each exercise session, so that, for example, the three weight sessions are grouped together. We can do this by using split
to ignore anything after the colon:
daily_workout_log.chunk{ |item| item.split(":").first }.to_a
which will give us:
[["Weights", ["Weights: squats", "Weights: benchpresses", "Weights: deadlifts"]], ["Rest", ["Rest", "Rest"]], ["Bike", ["Bike: 25km"]], ["Run", ["Run: 10km", "Run: 5km"]], ["bike", ["bike: 50km"]], ["BIKE", ["BIKE: 30km"]]]
This is a bit of an improvement; the consecutive weight sessions have been grouped together, as have the consecutive running sessions. Deborah has been a bit sloppy when recording her bike sessions: she’s entered “bike: 50km”, and then “BIKE: 30km”, and our code is treating these as different types of exercise. That’s easily fixed with capitalize
:
daily_workout_log.chunk{ |item| item.split(":").first.capitalize }.to_a
resulting in:
[["Weights", ["Weights: squats", "Weights: benchpresses", "Weights: deadlifts"]], ["Rest", ["Rest", "Rest"]], ["Bike", ["Bike: 25km"]], ["Run", ["Run: 10km", "Run: 5km"]], ["Bike", ["bike: 50km", "BIKE: 30km"]]]
Next task will be to calculate the totals for each type of exercise:
daily_workout_log.chunk{ |item| item.split(":").first.capitalize }.to_a.map{ |type, list| "#{type}: #{list.size} days" }
giving us:
["Weights: 3 days", "Rest: 2 days", "Bike: 1 days", "Run: 2 days", "Bike: 2 days"]
And we can convert this into a report by simply using join
:
daily_workout_log.chunk{ |item| item.split(":").first.capitalize }.to_a.map{ |type, list| "#{type}: #{list.size} days" }.join("\n")
resulting in:
"Weights: 2 days
Rest: 3 days
Bike: 1 days
Run: 2 days
Bike: 2 days"
That’s looking pretty good, but let’s go a step further, and ensure that we produce “1 day” instead of the ugly “1 days”. We can do this in a concise manner using the ternary operator:
daily_workout_log.chunk{ |item| item.split(":").first.capitalize }.to_a.map{ |type, list| "#{type}: #{list.size} #{list.one? ? "days" : "day"}" }.join("\n")
giving us:
"Weights: 2 days
Rest: 3 days
Bike: 1 day
Run: 2 days
Bike: 2 days"
We’re done - Deborah will now be able to view an easy-to-read summary of her exercise activity.
Realistically, we probably wouldn’t want to do this in a single line: instead we should use helpfully named methods and intermediate variables to make it clearer what’s going on. For example:
def extract_exercise_type(item)
item.split(":").first.capitalize
end
def summary_line(type, list)
"#{type}: #{list.size} #{list.one? ? "days" : "day"}"
end
chunked_log = daily_workout_log.chunk{ |item| extract_exercise_type(item) }.to_a
chunked_log.map{ |type, list| summary_line(type, list) }.join("\n")
I’d love to hear your thoughts on chunk
, the uses you’ve found for it, and indeed, exercising during lockdown. Why not leave a comment below?
How Ruby can help you schedule your sandwiches
How Ruby can help you keep track of your lockdown exercise routine
How Ruby can help you mix the drinks and bust some moves at the charity ball
You won't believe what happens when all the animals run away from the zoo.
Home, home on the Range, where the Ewoks and the Wookies play
An example of Ruby metaprogramming, by way of juggling chainsaws.
We would love to hear from you so let's get in touch!