简体   繁体   中英

Compare two lists to find date and time overlaps in Elixir

As part of some code that runs a booking system, we have a list of time_slots , which are tuples containing {start_time, end_time} . These are the available time slots that can be booked:

time_slots = [
  {~T[09:00:00], ~T[13:00:00]},
  {~T[09:00:00], ~T[17:00:00]},
  {~T[09:00:00], ~T[21:00:00]},
  {~T[13:00:00], ~T[17:00:00]},
  {~T[13:00:00], ~T[21:00:00]},
  {~T[17:00:00], ~T[21:00:00]}
]

Then we also have a list of bookings, which contains lists of tuples containing each {booking_start, booking_end} .

bookings = [
  [
    {~N[2019-06-13 09:00:00], ~N[2019-06-13 17:00:00]},
    {~N[2019-06-13 17:00:00], ~N[2019-06-13 21:00:00]}
  ],
  [{~N[2019-06-20 09:00:00], ~N[2019-06-20 21:00:00]}],
  [
    {~N[2019-06-22 13:00:00], ~N[2019-06-22 17:00:00]},
    {~N[2019-06-22 17:00:00], ~N[2019-06-22 21:00:00]}
  ]
]

In this case, we would want the results to be the two bookings with all of their time_slots filled up:

  • 2019-06-13
  • 2019-06-20

As they have all of their time slots filled up, and then return these results as Date s.


To provide a bit more information:

  • For a time slot to be filled up it would require either a booking's start or finish to overlap inside of it (regardless of how small that overlap is):
    • Eg a booking of 0900–1000 would fill the 0900–1300 , 0900–1700 and 0900–2100 time slots
  • A time slot can be filled with more than one booking:
    • Eg we can have bookings of 0900–1000 and 1000–1200 , which would both fit inside the 0900–1300 time slot.
  • If there is a booking that extends beyond the largest time slot, it counts as being filled:
    • Eg a booking of 0800—2200 would fill the 0900–2100 time slot (along with all the others)

So my understanding of the question is: for a list of bookings, do all time slots conflict with at least one booking?

A conflicting booking can be answered by checking two things:

  1. If the booking starts BEFORE the time slot starts, it conflicts if the booking finishes AFTER the time slot starts.

  2. If the booking starts ON OR AFTER the time slot starts, it conflicts if the BOOKING starts before the time slot finishes.

A working code therefore would look like:

time_slots = [
  {~T[09:00:00], ~T[13:00:00]},
  {~T[09:00:00], ~T[17:00:00]},
  {~T[09:00:00], ~T[21:00:00]},
  {~T[13:00:00], ~T[17:00:00]},
  {~T[13:00:00], ~T[21:00:00]},
  {~T[17:00:00], ~T[21:00:00]}
]

bookings = [
  [
    {~N[2019-06-13 09:00:00], ~N[2019-06-13 17:00:00]},
    {~N[2019-06-13 17:00:00], ~N[2019-06-13 21:00:00]}
  ],
  [{~N[2019-06-20 09:00:00], ~N[2019-06-13 21:00:00]}],
  [
    {~N[2019-06-22 13:00:00], ~N[2019-06-22 17:00:00]},
    {~N[2019-06-22 17:00:00], ~N[2019-06-22 21:00:00]}
  ]
]

bookings
|> Enum.filter(fn booking ->
  Enum.all?(time_slots, fn {time_start, time_end} ->
    Enum.any?(booking, fn {booking_start, booking_end} ->
      if Time.compare(booking_start, time_start) == :lt do
        Time.compare(booking_end, time_start) == :gt
      else
        Time.compare(booking_start, time_end) == :lt
      end
    end)
  end)
end) 
|> Enum.map(fn [{booking_start, _} | _] -> NaiveDateTime.to_date(booking_start) end)

PS: note you should not compare time/date/datetime with > , < and friends. Always use the relevant compare functions.

Although this might not cover all cases, given the sample data you provided this would work

defmodule BookingsTest do
  @slots [
    {~T[09:00:00], ~T[13:00:00]},
    {~T[09:00:00], ~T[17:00:00]},
    {~T[09:00:00], ~T[21:00:00]},
    {~T[13:00:00], ~T[17:00:00]},
    {~T[13:00:00], ~T[21:00:00]},
    {~T[17:00:00], ~T[21:00:00]}
  ]

  def booked_days(bookings, time_slots \\ @slots) do
    Enum.reduce(bookings, [], fn(day_bookings, acc) ->
      Enum.reduce(day_bookings, time_slots, fn({%{hour: s_time}, %{hour: e_time}}, ts) ->

          Enum.reduce(ts, [], fn
            ({%{hour: slot_s}, %{hour: slot_e}} = slot, inner_acc) ->

              case is_in_slot(s_time, e_time, slot_s, slot_e) do
                true -> inner_acc
                _ -> [slot | inner_acc]
              end

          end)
      end)
      |> case do
           [] -> [day_bookings | acc]
           _ -> acc
         end
    end)
    |> Enum.reduce([], fn([{arb, _} | _], acc) -> [NaiveDateTime.to_date(arb) | acc] end)
  end

  def is_in_slot(same_start, _, same_start, _), do: true
  def is_in_slot(s_time, e_time, slot_s, slot_e) when s_time < slot_s and e_time > slot_s, do: true
  def is_in_slot(s_time, e_time, slot_s, slot_e) when s_time > slot_s and s_time < slot_e, do: true
  def is_in_slot(_, _, _, _), do: false
end



> bookings = [
  [
    {~N[2019-06-13 10:00:00], ~N[2019-06-13 17:00:00]},
    {~N[2019-06-13 17:00:00], ~N[2019-06-13 21:00:00]}
  ],
  [{~N[2019-06-20 09:00:00], ~N[2019-06-20 21:00:00]}],
  [
    {~N[2019-06-22 13:00:00], ~N[2019-06-22 17:00:00]},
    {~N[2019-06-22 17:00:00], ~N[2019-06-22 21:00:00]}
  ]
]

> BookingsTest.booked_days(bookings)
[~D[2019-06-13], ~D[2019-06-20]]

The idea is, reduce through the bookings list accumulating into an empty list, each enumeration will be the list of occupied slots for the day.

Reduce through this list, accumulating with the list of all time slots available.

Inside this reduce through the timeslots accumulator into an empty list.

For each slot check if the start and end time of the current day booking slot overlaps into the slot. If it does just return the inner accumulator as is. If it doesn't, add the slot into this accumulator.

In the end of the day_bookings reduction, if you have an empty list it means no slot remains available for the day. So you add it to the outer accumulator, this will be the list of fully booked days.

In the end you reduce again the results so to invert them and in the process set each element to be the Date, instead of the list of bookings for the day.

Assuming you have a typo in the second booking and it does not start almost a week after it's own end, the solution might be much simpler than careful reducing.

The slots are filled when the booking starts and ends exactly at:

{start, end} =
  time_slots
  |> Enum.flat_map(&Tuple.to_list/1)
  |> Enum.min_max()          
#⇒ {~T[09:00:00], ~T[21:00:00]}

Which make the check almost trivial:

Enum.filter(bookings, fn booking ->
  {s, e} = {Enum.map(booking, &elem(&1, 0)), Enum.map(booking, &elem(&1, 1))}
  with {[s], [e]} <- {s -- e, e -- s} do
    same_date =
      [s, e]
      |> Enum.map(&NaiveDateTime.to_date/1)
      |> Enum.reduce(&==/2)
    full = Enum.map([s, e], &NaiveDateTime.to_time/1)

    same_date and full == [start, end]
  end
end)

Kernel.SpecialForms.with/1 guarantees that whatever not expected will be filtered out.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM