Fred Willmore
Fred Willmore

Reputation: 4614

Split an array of sequential dates into groups that are more than ten days apart

I would like to split the following array by introducing a split where the difference in dates is > 10 days:

dates = [Date.parse('2017-06-26'), Date.parse('2017-07-04'), Date.parse('2017-11-30')]
#=> [Mon, 26 Jun 2017, Tue, 04 Jul 2017, Thu, 30 Nov 2017]

the result should be the following:

[[Mon, 26 Jun 2017, Tue, 04 Jul 2017], [Thu, 30 Nov 2017]]

So far I've got a very procedural method. It accepts as parameters the array to be split, the minimum difference between members of different groups, and the attribute to evaluate to get the difference. (In the case of my array of Date objects, I would leave off the last parameter, as the value used to determine diffs would just be the Date itself)

def split_by_attribute_diff array, split_size, attribute = :itself    
  groups = []
  current_group = []
  previous = current = nil
  array.sort_by(&attribute).each do |e|
    previous = current
    current = e
    if previous && current.send(attribute) - previous.send(attribute) > split_size
      if current_group.count > 0
        groups << current_group
        current_group = []
      end
    end
    current_group << current
  end
  if current_group.count > 0
    groups << current_group
  end

  groups
end

The things I like about this method are that 1) it works, 2) the algorithmic complexity is just that of the sort_by - after the array is sorted, it is traversed just once. I guess the only thing I don't like is that it looks like it should be simpler. Is there a more Ruby-ish way to accomplish what I'm doing here?

Upvotes: 0

Views: 553

Answers (2)

Cary Swoveland
Cary Swoveland

Reputation: 110725

My preference would be to use Enumerable#chunk_while (new in v2.3) or Enumerable#slice_when (new in v2.2) for this and similar problems. If, however, Ruby versions prior to 2.2 must be supported, an approach similar to the following can be used.

require 'date'

def group_em(dates)
  fmt = '%Y-%m-%d'
  dates.each_with_object([[]]) do |d,a|
    if a.last.empty? || Date.strptime(d,fmt) <= Date.strptime(a.last.last,fmt) + 10
      a.last << d
    else
      a << [d]
    end
  end
end

group_em %w| 2017-06-26 2017-07-04 2017-11-21 2017-11-30 2017-12-30 |
  #=> [["2017-06-26", "2017-07-04"], ["2017-11-21", "2017-11-30"], ["2017-12-30"]]

Upvotes: 1

tadman
tadman

Reputation: 211690

It doesn't have to be hard if you properly leverage the Enumerable library in Ruby and in particular chunk_while which is specifically for carving up an array into little chunks based on a logical test:

require 'date'

dates = %w[
  2017-06-26
  2017-07-04
  2017-11-21
  2017-11-30
  2017-12-30
].map { |d| Date.parse(d) }

r = dates.chunk_while do |a,b|
  a + 10 > b
end

r.to_a.map { |a| a.map { |d| d.strftime('%Y-%m-%d') } }
# => [["2017-06-26", "2017-07-04"], ["2017-11-21", "2017-11-30"], ["2017-12-30"]]

Upvotes: 4

Related Questions