Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Union arrays of date time object

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.

like image 816
karlos Avatar asked Sep 02 '25 08:09

karlos


1 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
like image 169
Chris Heald Avatar answered Sep 04 '25 22:09

Chris Heald