Reputation: 3696
I have a sorted array full of objects which I'd like to group by comparing them to their surrounding elements.
a
is sorted by the start
attribute.
a = [{ name: "joe", start: "9am", end: "10am" },
{ name: "joe", start: "10am", end: "11am" },
{ name: "harry", start: "11am", end: "12pm" },
{ name: "harry", start: "12pm", end: "1pm" },
{ name: "harry", start: "2pm", end: "3pm" },
{ name: "joe", start: "3pm", end: "4pm" },
{ name: "joe", start: "4pm", end: "5pm" }]
I would like to group adjacent objects by the name
attribute but only if the start
and end
times are the same, producing:
a = [[{ name: "joe", start: "9am", end: "10am" }, { name: "joe", start: "10am", end: "11am" }],
[{ name: "harry", start: "11am", end: "12pm" }, { name: "harry", start: "12pm", end: "1pm" }],
[{ name: "harry", start: "2pm", end: "3pm" }],
[{ name: "joe", start: "3pm", end: "4pm" }, { name: "joe", start: "4pm", end: "5pm" }]]
There is no maximum to the amount of consecutive time periods.
I can group them by name
if adjacent as seen here: Ruby / Rails Groups only Adjacent Array Elements
a.chunk { |hash| hash[:name] }.map(&:last)
But it doesn't appear as though I can get the element index with chunk to do the start
end
time comparisons.
It looks like the answer is here: Grouping an array by comparing 2 adjacent elements
But I'm failing miserably at writing my own function. (I'm struggling to understand what slice_before
does.)
def self.group_by(data)
tmp = data.first
data.slice_before do |item|
tmp, prev = item, tmp
item.application == prev.application &&
item.start == prev.end
end.to_a
return data
end
Any help would be appreciated!
Upvotes: 0
Views: 1049
Reputation: 3298
Using slice_before:
y=a[0]
a.slice_before { |hash| x=y
y = hash
x[:name] != y[:name] || x[:end] != y[:start]
}.to_a
Upvotes: 0
Reputation: 1575
Here's one way using Enumerable#sort_by
and Enumerable#slice_when
. It requires Ruby 2.2+, though.
require 'time' # for sorting times
a = [{ name: "joe", start: "9am", end: "10am" },
{ name: "joe", start: "10am", end: "11am" },
{ name: "harry", start: "11am", end: "12pm" },
{ name: "harry", start: "12pm", end: "1pm" },
{ name: "harry", start: "2pm", end: "3pm" },
{ name: "joe", start: "3pm", end: "4pm" },
{ name: "joe", start: "4pm", end: "5pm" }]
a.sort_by { |h| [ h[:name], Time.parse(h[:start]) ] } # 1
.slice_when { |x, y| x[:end] != y[:start] || x[:name] != y[:name] }.to_a # 2
which yields
=> [[{:name=>"harry", :start=>"11am", :end=>"12pm"}, {:name=>"harry", :start=>"12pm", :end=>"1pm"}],
[{:name=>"harry", :start=>"2pm", :end=>"3pm"}],
[{:name=>"joe", :start=>"9am", :end=>"10am"}, {:name=>"joe", :start=>"10am", :end=>"11am"}],
[{:name=>"joe", :start=>"3pm", :end=>"4pm"}, {:name=>"joe", :start=>"4pm", :end=>"5pm"}]]
Here's a step-by-step explanation with intermediate results:
1) Sort the hashes by name and then by time within name. Note the use of Time.parse
to temporarily convert your time string into a Time
object for proper sorting:
=> [{:name=>"harry", :start=>"11am", :end=>"12pm"},
{:name=>"harry", :start=>"12pm", :end=>"1pm"},
{:name=>"harry", :start=>"2pm", :end=>"3pm"},
{:name=>"joe", :start=>"9am", :end=>"10am"},
{:name=>"joe", :start=>"10am", :end=>"11am"},
{:name=>"joe", :start=>"3pm", :end=>"4pm"},
{:name=>"joe", :start=>"4pm", :end=>"5pm"}]
2) Now slice this intermediate array when the end time of the former is not equal to the start time of the latter or when the names don't match using Daniel Polfer's solution. This returns an Enumerator
object, hence the final to_a
method call:
=> [[{:name=>"harry", :start=>"11am", :end=>"12pm"}, {:name=>"harry", :start=>"12pm", :end=>"1pm"}],
[{:name=>"harry", :start=>"2pm", :end=>"3pm"}],
[{:name=>"joe", :start=>"9am", :end=>"10am"}, {:name=>"joe", :start=>"10am", :end=>"11am"}],
[{:name=>"joe", :start=>"3pm", :end=>"4pm"}, {:name=>"joe", :start=>"4pm", :end=>"5pm"}]]
If your hashes are already presorted, then Daniel Polfer's solution should work fine. But if you have any data where names and/or start times are out of order like this:
b = [{:name=>"joe", :start=>"3pm", :end=>"4pm"},
{:name=>"bill", :start=>"2pm", :end=>"3pm"},
{:name=>"joe", :start=>"5pm", :end=>"6pm"},
{:name=>"joe", :start=>"4pm", :end=>"5pm"}]
Just using slice_when
returns
=> [[{:name=>"joe", :start=>"3pm", :end=>"4pm"}],
[{:name=>"bill", :start=>"2pm", :end=>"3pm"}],
[{:name=>"joe", :start=>"5pm", :end=>"6pm"}],
[{:name=>"joe", :start=>"4pm", :end=>"5pm"}]]
instead of
=> [[{:name=>"bill", :start=>"2pm", :end=>"3pm"}],
[{:name=>"joe", :start=>"3pm", :end=>"4pm"},
{:name=>"joe", :start=>"4pm", :end=>"5pm"},
{:name=>"joe", :start=>"5pm", :end=>"6pm"}]]
Upvotes: 2
Reputation: 61
Verbose, but gives result in order you presented, and works with older versions of Ruby...
a.inject([]) do |result,hash|
if (!result.empty? &&
(result.last.last[:name] == hash[:name]) &&
(result.last.last[:end] == hash[:start]))
result.last << hash
else
result << [hash]
end
result
end
Builds the result one hash at a time, choosing to add a hash to the end of the last array or "step" to a new final array entry.
Ruby 2.2+
a.slice_when{|h1,h2| (h1[:name]!=h2[:name]) || (h1[:end]!=h2[:start])}.to_a
Upvotes: 2