Union arrays of date time object

Viewed 132

I have two arrays of date time and I want to union them.

list1 = [
    {start: '2018-08-28 03:00:00', end: '2018-08-28 08:00:00'},
    {start: '2018-08-28 11:00:00', end: '2018-08-28 13:00:00'},
    {start: '2018-08-28 15:00:00', end: '2018-08-28 16:00:00'},
    {start: '2018-08-28 16:15:00', end: '2018-08-28 17:00:00'},
    {start: '2018-08-28 18:00:00', end: '2018-08-28 19:00:00'},
]

list2 = [
    {start: '2018-08-28 03:00:00', end: '2018-08-28 09:00:00'},
    {start: '2018-08-28 11:30:00', end: '2018-08-28 12:30:00'},
    {start: '2018-08-28 15:15:00', end: '2018-08-28 16:15:00'},
]

The result is like

list3 = [
    {start: '2018-08-28 03:00:00', end: '2018-08-28 09:00:00'},
    {start: '2018-08-28 11:00:00', end: '2018-08-28 13:00:00'},
    {start: '2018-08-28 15:00:00', end: '2018-08-28 17:00:00'},
    {start: '2018-08-28 18:00:00', end: '2018-08-28 19:00:00'},
]

I tried in Ruby

list3 = list1.concat(list2).sort{|a, b| DateTime.parse(a[:start]) - DateTime.parse(b[:start]) }
puts result

and JavaScript but not success.

2 Answers

Here's one solution:

parse = ->(r) { DateTime.parse(r[:start])..DateTime.parse(r[:end]) }
ranges = list1.map(&parse) + list2.map(&parse)

merged = ranges.sort_by(&:begin).reduce([]) do |acc, range|
  if acc.any? && acc.last.cover?(range.begin)
    last = acc.pop
    acc.push last.begin..range.end
  else
    acc.push range
  end
  acc
end

Your solution doesn't "merge" the time ranges, because it just sorts the list of times by ascending distance between start times. This will obviously not produce any useful merging of times!

I'm using Ruby's Range construct to create a time range from each start/end pair, then I sort all the ranges by their beginning time, then reduce the list of individual ranges with the algorithm:

if the current range covers the next range's start date:
  extend the current range to the next range's end date
else
  add the next range into the return list

It's worth noting that I'm just merging all the ranges via any intersections; there's no need to split them into two lists, because if there is an overlapping range in one list or the other, they would union together anyhow.

You don't have to use ranges, but they map onto this concept cleanly, and you can then use them to produce intervals or whatever you're looking to do.

Edit: As Cary pointed out, if the data contains an inverted range, the results get a little funky. Here's an alternate parsing scheme which addresses that issue:

ranges = [list1, list2].flatten.map(&:values).map do |v| 
  Range.new *v.map {|d| DateTime.parse d}.sort.take(2)
end

You can obtain the array of non-overlapping ranges as follows.

(list1 + list2).sort_by { |h| h[:start] }.each_with_object([]) do |h,a|
  if a.empty? || h[:start] > a.last[:end]
    a << { start: h[:start], end: h[:end] }
  else
    a[-1][:end] = [a.last[:end], h[:end]].max
  end
end
  #=> [{:start=>"2018-08-28 03:00:00", :end=>"2018-08-28 09:00:00"},
  #    {:start=>"2018-08-28 11:00:00", :end=>"2018-08-28 13:00:00"},
  #    {:start=>"2018-08-28 15:00:00", :end=>"2018-08-28 17:00:00"},
  #    {:start=>"2018-08-28 18:00:00", :end=>"2018-08-28 19:00:00"}]

Note that, because of the date-time format (year-month-day-hour-minute-second), there is no need to convert date-time strings to DateTime objects, perform calculations, and then convert the resulting DateTime objects back to strings. That is, if s1 and s2 are two date-time strings, the time represented by s1 occurs before the time represented by s2 if and only if s1 < s2.

The initial step is as follows.

list = (list1 + list2).sort_by { |h| h[:start] }
   #=> [{:start=>"2018-08-28 03:00:00", :end=>"2018-08-28 09:00:00"},
   #    {:start=>"2018-08-28 03:00:00", :end=>"2018-08-28 08:00:00"},
   #    {:start=>"2018-08-28 11:00:00", :end=>"2018-08-28 13:00:00"},
   #    {:start=>"2018-08-28 11:30:00", :end=>"2018-08-28 12:30:00"},
   #    {:start=>"2018-08-28 15:00:00", :end=>"2018-08-28 16:00:00"},
   #    {:start=>"2018-08-28 15:15:00", :end=>"2018-08-28 16:15:00"},
   #    {:start=>"2018-08-28 16:15:00", :end=>"2018-08-28 17:00:00"},
   #    {:start=>"2018-08-28 18:00:00", :end=>"2018-08-28 19:00:00"}]
Related