0

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!

6
  • 1
    So you want to group adjacent times for the same person? Commented Mar 29, 2015 at 0:02
  • Is the maximum two? Or are there potentially many? Commented Mar 29, 2015 at 0:05
  • No maximum, there's potentially many. Commented Mar 29, 2015 at 0:06
  • Many in a group you mean? Commented Mar 29, 2015 at 0:07
  • 1
    Yes, many consecutive time periods Commented Mar 29, 2015 at 0:09

3 Answers 3

2

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"}]]
Sign up to request clarification or add additional context in comments.

7 Comments

Just consecutive runs by name is what I'm after. Which is the result of step 4. Thanks for the explanation!
DanielPolfer answer seems nice and simple. a.slice_when{|h1,h2| (h1[:name]!=h2[:name]) || (h1[:end]!=h2[:start])}.to_a1 any reason not to go for that?
I don't think there as any reason not to go for that @RyanKing. That is a significant improvement over mine.
@RyanKing There's one tiny issue with Daniel's answer that I touch on above. Still think his is better, though.
so as long as sort the data by start first than Daniels answer should work fine, right?
|
2

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

4 Comments

Nice! Works a treat! The ruby 2.2 version is a bit cleaner though.
a.slice_when{|h1,h2| (h1[:name]!=h2[:name]) || (h1[:end]!=h2[:start])}.to_a leverages off the other answer for 2.2, but cuts it down a bit for same result. Just slices in place.
Ooo that's a pretty simple approach!
@DanielPolfer Nice! You should add that to your answer. That is a lot simpler than mine!
0

Using slice_before:

y=a[0]
a.slice_before { |hash| x=y 
                        y = hash
                        x[:name] != y[:name] || x[:end] != y[:start]
                        }.to_a

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.