Could you criticize my code that generates time slots?

The following code generates a list of time slots for a period and a duration.

If a period starts at 9:00, finishes at 10:00 and the duration is 30 minutes, then I have two slots, the first starting at 9:00 and the second at 9:30.

For the same period but a duration of 15 minutes. I have 4 slots, at 9:00, 9:15, 9:30, and 9:45.

# slot.exs

defmodule Slot do
  def to_list(_current, _last, _minutes, false), do: []

  def to_list(start, last, minutes, true) do
    stop = NaiveDateTime.add(start, minutes * 60)
    fits = fits?(stop, minutes, last)
    [start | to_list(stop, last, minutes, fits)]
  end

  def fits?(start, duration, last) do
    comparison =
      start
      |> NaiveDateTime.add(duration * 60)
      |> NaiveDateTime.compare(last)

    case comparison do
      :gt -> false
      :eq -> true
      :lt -> true
    end
  end
end

start = ~N[2019-09-11 09:00:00]
finish = ~N[2019-09-11 10:00:00]
duration = 15

IO.inspect(start, label: "start")
IO.inspect(finish, label: "finish")
IO.inspect(duration, label: "duration")

slots =
  Slot.to_list(
    start,
    finish,
    duration,
    Slot.fits?(start, duration, finish)
  )

IO.inspect(slots, label: "slots")

How would you improve the code?

Could it be simpler?

What do you think about the variable and function names? (I am not a native english speaker)

I’ll appreciate all your comments.

Your code is not bad. Here’s my version of it simplified. Not sure exactly what you need but I get the same results by using Enum.reduce and the div function. I don’t use a finish naive date time as div should give me the correct number of slots to generate. I can generate time slots with a function called slots that takes two parameters, start and duration

defmodule Slot do
  def slots(start, duration) do
    number = div(60, duration) - 1
    Enum.reduce(1..number, [start], &create_slots(&1, &2, duration))
    |> Enum.reverse()
  end

  defp create_slots(_number, list, duration) do
    [h | _t] = list
    [NaiveDateTime.add(h, duration * 60) | list]
  end
end
start = ~N[2019-09-11 09:00:00]
duration = 15

Slot.slots(start, duration)

[~N[2019-09-11 09:00:00], ~N[2019-09-11 09:15:00],
 ~N[2019-09-11 09:30:00], ~N[2019-09-11 09:45:00]

Usually te best approach to generate possibly infinite sequences would be Stream, not List.

defmodule Slot do
  def slots(start, finish, duration) do
    start
    |> Stream.iterate(&NaiveDateTime.add(&1, duration * 60, :second))
    |> Stream.take_while(&NaiveDateTime.diff(finish, &1) >= 0)
  end
end

start = ~N[2019-09-11 09:00:00]
finish = ~N[2019-09-11 10:00:00]
duration = 15

Slot.slots(start, finish, duration)
#⇒ #Stream<[
#   enum: #Function<65.35756501/2 in Stream.unfold/2>,
#   funs: [#Function<62.35756501/1 in Stream.take_while/2>]
# ]>

start |> Slot.slots(finish, duration) |> Enum.to_list()
#⇒ [~N[2019-09-11 09:00:00], ~N[2019-09-11 09:15:00], ~N[2019-09-11 09:30:00],
#   ~N[2019-09-11 09:45:00], ~N[2019-09-11 10:00:00]]

By moving the latter Stream.take_while/2 into a separate function you could have literally infinite stream and then you might drop_while, or lazily process slots, or whatever.

2 Likes

Sidenote:

def fits?(start, duration, last) do
  start
  |> NaiveDateTime.add(duration * 60)
  |> NaiveDateTime.compare(last)
  |> Kernel.!=(:gt)
end
1 Like

There’s a wonderful package called calendar_interval by @wojtekmach that makes it easy to work with intervals. For example, to generate a stream of 30 minute slots:

iex> stream = Stream.iterate ~I"2019-01-01 21:00/29", &CalendarInterval.next/1
#Function<65.35756501/2 in Stream.unfold/2>
iex> Enum.take stream, 5
[~I"2019-01-01 21:00/29", ~I"2019-01-01 21:30/59", ~I"2019-01-01 22:00/29",
 ~I"2019-01-01 22:30/59", ~I"2019-01-01 23:00/29"]
5 Likes