The title is gracefully stolen from Oratio in Catilinam Prima in Senatu Habita by Marcus Tullius Cicero.
In real life we often deal with time slots. Dentist appointments, hotel room reservations, even daily lunch break: scheduling all these is a task of fitting a time slot in a series of other time slots.
Consider a dentist appointment. I know I need 1 hour for the yearly routine inspection. I can visit a doctor during my lunch time, or after my working hours. The doctor has other patients. Here my busy hours are shown in purple, the doctor’s ones in red, non-working hours in gray, and the happily found slot when we both are free in green.
A cursory glance at this chart is enough to spot the appointment time to be scheduled. Tomorrow, lunch time. Easy, huh?
Unfortunately, none of languages I am familiar with provides the functionality to determine this programatically. Last several years I am married to Elixir, hence I decided to write the library solving this particular problem.
Welcome Tempus
!
Implementation details
The unit this library is built around is Slot
. It represents the time slot in a most natural and simple way: struct, having from
and to
fields, both of type DateTime
. The series of slots are organized in Slots
. Under the hood it’s implemented as AVLTree
. This choice was made to keep the underlying list of slots consistent (ordered and not overlapped) for lowest cost to optimize read-access. The common use-case would be to fill the tree and store it somewhere for querying later.
Slots.add/2
function would add the slots list with the new slot, joining slots as needed. That makes it possible to simply insert new slots into the structure without bothering of the order and of the overlapping. Also, the helper function Slots.merge/2
is provided to merge two slot series. The latter is explicitly handy when one needs e. g. to find the empty slot in both series, as in the example with dentist appointment above.
One might also test any input against the slot with functions in Slot
module: whether the slot covers the DateTime
instance given, whether slots are disjoint or not, etc.
Tempus
module
The main module exports functions to work with slots as with an intermittent timeline. One might add any arbitrary time interval to the origin, taking the slots into consideration; check whether the origin is free, or already taken by slots, get next free, or next busy slot, inverse slots, and more.
Here is the simple example from tests:
slots =
[
Tempus.Slot.wrap(~D|2020-08-07|), # whole day
%Tempus.Slot{
from: ~U|2020-08-08 01:01:00Z|, # one minute
to: ~U|2020-08-08 01:02:00Z|
},
%Tempus.Slot{
from: ~U|2020-08-08 01:03:00Z|, # one minute
to: ~U|2020-08-08 01:04:00Z|
}
]
|> Enum.into(%Tempus.Slots{})
Now adding 0
seconds to the occupied by slot time would return the first free time after this slot.
Tempus.add(slots, ~U|2020-08-08 01:01:30Z|, 0, :second)
#⇒ ~U[2020-08-08 01:02:00Z]
Adding 70 seconds five seconds before the first occupied minute ~U[2020-08-08 01:00:55Z]
would return the time five seconds after the second occupied minute
Tempus.add(slots, ~U|2020-08-08 01:00:55Z|, 70, :second)
#⇒ ~U[2020-08-08 01:04:05Z]
And so on. Negative values are surely allowed as well.
Merging slot series
Slots.merge/2
accepts a stream as the second argument. At the moment the library does not support merging and using stream of slots as is, but merging the stream into existing time slots is possible. This might be useful when one has the short list of, say, holidays, and wants to merge some reoccuring slots, like weekends.
All the functions returning Slots
and/or Slot
return valid objects (meaning Slot
would be normalized and Slots
would be ordered and joined as necessary.)
Future development
Current implementation covers our internal needs only, so I would be happy to hear about what this library lacks and what should be added to it.
Happy timeslotting!