Datetime 在Elixir中构建包含排除项的日期时间系列

Datetime 在Elixir中构建包含排除项的日期时间系列,datetime,functional-programming,elixir,Datetime,Functional Programming,Elixir,我想生成两个日期之间的可用时间段列表,同时删除已预订的时间段 首先,有一个时间段列表,这些时间段是{start_time,end_time}的元组,可以在任何给定的一天预订: time_slots = [ {~T[09:00:00], ~T[13:00:00]}, {~T[13:00:00], ~T[17:00:00]}, {~T[17:00:00], ~T[21:00:00]} ] 然后是预订列表,其中包含{booking_start,booking_end}元组: 还有一个元组

我想生成两个日期之间的可用时间段列表,同时删除已预订的时间段

首先,有一个时间段列表,这些时间段是{start_time,end_time}的元组,可以在任何给定的一天预订:

time_slots = [
  {~T[09:00:00], ~T[13:00:00]},
  {~T[13:00:00], ~T[17:00:00]},
  {~T[17:00:00], ~T[21:00:00]}
]
然后是预订列表,其中包含{booking_start,booking_end}元组:

还有一个元组,其中包含{start_date,end_date},我们希望在这两个元组之间生成所有可用的时隙

start_end = {~N[2019-06-13 01:00:00Z], ~N[2019-06-16 23:00:00Z]}
在这种情况下,我们希望生成所有可用的时隙,并返回:

available_slots = [
  {~N[2019-06-13 09:00:00Z], ~N[2019-06-13 13:00:00Z]},
  {~N[2019-06-13 13:00:00Z], ~N[2019-06-13 17:00:00Z]},
  {~N[2019-06-13 17:00:00Z], ~N[2019-06-13 21:00:00Z]},
  {~N[2019-06-14 17:00:00Z], ~N[2019-06-14 21:00:00Z]},
  {~N[2019-06-15 13:00:00Z], ~N[2019-06-15 17:00:00Z]},
  {~N[2019-06-15 17:00:00Z], ~N[2019-06-15 21:00:00Z]},
  {~N[2019-06-16 09:00:00Z], ~N[2019-06-16 13:00:00Z]}
]
对于要占用的时段,无论重叠的程度有多小,都需要预订的开始或结束在其内部重叠: e、 g.预订0900-1000将填满0900-1300、0900-1700和0900-2100的时段 一个时间段可以有多个预订: e、 g.我们可以有0900-1000和1000-1200的预订,这两个时间段都适合0900-1300。
下面是一个可能的解决方案,将代码粘贴到文件timeslots.exs中,并使用elixir timeslots.exs运行

所采取的步骤是:

建立一个列表,列出所有可用的“开始”和“结束”时间段 删除与预订重叠的插槽 有重叠吗?检查有点棘手,可能需要更多的测试。当预订开始时间在该时段之前,预订结束时间在该时段之后时,它还会删除一个被遮挡的时段

defmodule TimeSlots do
  def available_time_slots(bookings, time_slots, start_end) do
    time_slots_in_range(time_slots, start_end)
    |> remove_booked_slots(bookings)
  end

  # Build a list with all time_slots between start_date_time and end_date_time
  defp time_slots_in_range(time_slots, {start_date_time, end_date_time}) do
    start_date = NaiveDateTime.to_date(start_date_time)
    end_date = NaiveDateTime.to_date(end_date_time)

    Date.range(start_date, end_date)
    |> Enum.map(fn date -> daily_time_slots(date, time_slots) end)
    |> List.flatten
    |> Enum.filter(fn {slot_start_date_time, slot_end_date_time} ->
      NaiveDateTime.compare(start_date_time, slot_start_date_time) != :gt &&
      NaiveDateTime.compare(end_date_time, slot_end_date_time) != :lt
    end)
  end

  defp daily_time_slots(date, time_slots) do
    Enum.map(time_slots, &(create_time_slot(date, &1)))
  end

  defp create_time_slot(date, {start_time, end_time}) do
    {:ok, start_date_time} = NaiveDateTime.new(date, start_time)
    {:ok, end_date_time} = NaiveDateTime.new(date, end_time)
    {start_date_time, end_date_time}
  end

  defp remove_booked_slots(time_slots, bookings) do
    Enum.reject(time_slots, fn time_slot ->
      Enum.reduce(bookings, false, fn booking, acc ->
        acc or has_overlap?(booking, time_slot)
      end)
    end)
  end

  # (slot_start <= booking_start < slot_end)
  # or (slot_start < booking_end <= slot_end)
  # or (booking_start <= slot_start and slot_end <= booking_end)
  defp has_overlap?({booking_start, booking_end}, {slot_start, slot_end}) do
    (NaiveDateTime.compare(slot_start, booking_start) != :gt &&
     NaiveDateTime.compare(booking_start, slot_end) == :lt) ||
    (NaiveDateTime.compare(slot_start, booking_end) == :lt &&
     NaiveDateTime.compare(booking_end, slot_end) != :gt) ||
    (NaiveDateTime.compare(booking_start, slot_start) != :gt &&
     NaiveDateTime.compare(slot_end, booking_end) != :gt)
  end
end


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

bookings = [
  {~N[2019-06-14 09:00:00Z], ~N[2019-06-14 17:00:00Z]},
  {~N[2019-06-15 09:00:00Z], ~N[2019-06-15 13:00:00Z]},
  # some bookings may sit outside a slot range
  {~N[2019-06-16 15:00:00Z], ~N[2019-06-16 21:00:00Z]}
]

# I've changed the end date to 2019-06-16 to match the expected result
start_end = {~N[2019-06-13 01:00:00Z], ~N[2019-06-16 23:00:00Z]}

available_slots = [
  {~N[2019-06-13 09:00:00Z], ~N[2019-06-13 13:00:00Z]},
  {~N[2019-06-13 13:00:00Z], ~N[2019-06-13 17:00:00Z]},
  {~N[2019-06-13 17:00:00Z], ~N[2019-06-13 21:00:00Z]},
  {~N[2019-06-14 17:00:00Z], ~N[2019-06-14 21:00:00Z]},
  {~N[2019-06-15 13:00:00Z], ~N[2019-06-15 17:00:00Z]},
  {~N[2019-06-15 17:00:00Z], ~N[2019-06-15 21:00:00Z]},
  {~N[2019-06-16 09:00:00Z], ~N[2019-06-16 13:00:00Z]}
]

# Test it
IO.inspect TimeSlots.available_time_slots(bookings, time_slots, start_end)
defmodule TimeSlots do
  def available_time_slots(bookings, time_slots, start_end) do
    time_slots_in_range(time_slots, start_end)
    |> remove_booked_slots(bookings)
  end

  # Build a list with all time_slots between start_date_time and end_date_time
  defp time_slots_in_range(time_slots, {start_date_time, end_date_time}) do
    start_date = NaiveDateTime.to_date(start_date_time)
    end_date = NaiveDateTime.to_date(end_date_time)

    Date.range(start_date, end_date)
    |> Enum.map(fn date -> daily_time_slots(date, time_slots) end)
    |> List.flatten
    |> Enum.filter(fn {slot_start_date_time, slot_end_date_time} ->
      NaiveDateTime.compare(start_date_time, slot_start_date_time) != :gt &&
      NaiveDateTime.compare(end_date_time, slot_end_date_time) != :lt
    end)
  end

  defp daily_time_slots(date, time_slots) do
    Enum.map(time_slots, &(create_time_slot(date, &1)))
  end

  defp create_time_slot(date, {start_time, end_time}) do
    {:ok, start_date_time} = NaiveDateTime.new(date, start_time)
    {:ok, end_date_time} = NaiveDateTime.new(date, end_time)
    {start_date_time, end_date_time}
  end

  defp remove_booked_slots(time_slots, bookings) do
    Enum.reject(time_slots, fn time_slot ->
      Enum.reduce(bookings, false, fn booking, acc ->
        acc or has_overlap?(booking, time_slot)
      end)
    end)
  end

  # (slot_start <= booking_start < slot_end)
  # or (slot_start < booking_end <= slot_end)
  # or (booking_start <= slot_start and slot_end <= booking_end)
  defp has_overlap?({booking_start, booking_end}, {slot_start, slot_end}) do
    (NaiveDateTime.compare(slot_start, booking_start) != :gt &&
     NaiveDateTime.compare(booking_start, slot_end) == :lt) ||
    (NaiveDateTime.compare(slot_start, booking_end) == :lt &&
     NaiveDateTime.compare(booking_end, slot_end) != :gt) ||
    (NaiveDateTime.compare(booking_start, slot_start) != :gt &&
     NaiveDateTime.compare(slot_end, booking_end) != :gt)
  end
end


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

bookings = [
  {~N[2019-06-14 09:00:00Z], ~N[2019-06-14 17:00:00Z]},
  {~N[2019-06-15 09:00:00Z], ~N[2019-06-15 13:00:00Z]},
  # some bookings may sit outside a slot range
  {~N[2019-06-16 15:00:00Z], ~N[2019-06-16 21:00:00Z]}
]

# I've changed the end date to 2019-06-16 to match the expected result
start_end = {~N[2019-06-13 01:00:00Z], ~N[2019-06-16 23:00:00Z]}

available_slots = [
  {~N[2019-06-13 09:00:00Z], ~N[2019-06-13 13:00:00Z]},
  {~N[2019-06-13 13:00:00Z], ~N[2019-06-13 17:00:00Z]},
  {~N[2019-06-13 17:00:00Z], ~N[2019-06-13 21:00:00Z]},
  {~N[2019-06-14 17:00:00Z], ~N[2019-06-14 21:00:00Z]},
  {~N[2019-06-15 13:00:00Z], ~N[2019-06-15 17:00:00Z]},
  {~N[2019-06-15 17:00:00Z], ~N[2019-06-15 21:00:00Z]},
  {~N[2019-06-16 09:00:00Z], ~N[2019-06-16 13:00:00Z]}
]

# Test it
IO.inspect TimeSlots.available_time_slots(bookings, time_slots, start_end)
[
  {~N[2019-06-13 09:00:00], ~N[2019-06-13 13:00:00]},
  {~N[2019-06-13 13: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-14 17:00:00], ~N[2019-06-14 21:00:00]},
  {~N[2019-06-15 13:00:00], ~N[2019-06-15 17:00:00]},
  {~N[2019-06-15 17:00:00], ~N[2019-06-15 21:00:00]},
  {~N[2019-06-16 09:00:00], ~N[2019-06-16 13:00:00]}
]