Here is a functionally equivalent (and IMO easier to read) version of mendosi's code for MariaDB/MySQL, with some additional exposition and slightly simplified logic in some areas.
Here's a sufficient schema for an Appointment
table. In a moment, we'll also add an INSERT
trigger which will ensure that new entries don't clash with existing entries.
CREATE TABLE Appointment (
doctorID INT UNSIGNED NOT NULL,
`date` DATE NOT NULL,
startTime TIME(0) NOT NULL,
endTime TIME(0) NOT NULL,
CONSTRAINT PRIMARY KEY (doctorID, `date`, startTime),
CONSTRAINT mustStartOnTenMinuteBoundary CHECK (
EXTRACT(MINUTE FROM startTime) % 10 = 0
AND EXTRACT(SECOND FROM startTime) = 0
),
CONSTRAINT mustEndOnTenMinuteBoundary CHECK (
EXTRACT(MINUTE FROM endTime) % 10 = 0
AND EXTRACT(SECOND FROM endTime) = 0
),
CONSTRAINT cannotStartBefore0900 CHECK (
EXTRACT(HOUR FROM startTime) >= 9
),
CONSTRAINT cannotEndAfter1700 CHECK (
EXTRACT(HOUR FROM (startTime - INTERVAL 1 SECOND)) < 17
),
CONSTRAINT mustEndAfterStart CHECK (
endTime > startTime
)
);
First, we define a function to determine whether a given time slot can be allocated as a new appointment:
DELIMITER //
CREATE FUNCTION slotIsAvailable(
doctorID INT,
slotStartDateTime DATETIME,
slotEndDateTime DATETIME
) RETURNS BOOLEAN DETERMINISTIC
BEGIN
RETURN NOT EXISTS (
-- This table will contain records iff the slot clashes with an existing appointment
SELECT *
FROM Appointment AS a
WHERE
CONVERT(slotStartDateTime, TIME) < a.endTime -- These two conditions will both hold iff the slot overlaps
AND CONVERT(slotEndDateTime, TIME) > a.startTime -- with the existing appointment that it's being compared to
AND a.doctorID = doctorID
AND a.date = CONVERT(slotStartDateTime, DATE)
);
END;
END; //
DELIMITER ;
Now here's that INSERT
trigger mentioned earlier to ensure no clashing appointments are stored:
DELIMITER //
CREATE TRIGGER ensureNewAppointmentsDoNotClash
BEFORE INSERT ON Appointment
FOR EACH ROW
BEGIN
IF NOT slotIsAvailable(
NEW.doctorID,
CAST( CONCAT(NEW.date, ' ', NEW.startTime) AS DATETIME ),
CAST( CONCAT(NEW.date, ' ', NEW.endTime) AS DATETIME )
) THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Appointment clashes with an existing appointment!';
END IF;
END; //
DELIMITER ;
Now that the Appointment
table is properly set up, we can insert some valid sample entries:
INSERT INTO Appointment VALUES
(1, '2019-10-06', '09:20', '09:30'),
(1, '2019-10-06', '09:40', '09:50'),
(1, '2019-10-06', '11:00', '11:20'),
(1, '2019-10-06', '11:20', '11:40'),
(1, '2019-10-06', '11:40', '12:00'),
(1, '2019-10-06', '13:00', '14:00'),
(1, '2019-10-06', '16:00', '16:40');
If you try to insert an invalid appointment entry, an error will be thrown as a result of the ensureNewAppointmentsDoNotClash
trigger. In fact, this trigger will throw an error even before the primary key constraint is checked, so that could be deemed redundant; for my solution, I opted to have an ID field for the Appointment
table, rather than using a compound primary key.
Now here's the procedure to get a result set of available time slots of a given length, with a given doctor. Notice that we make use of our slotIsAvailable
function that we defined earlier and also used in our INSERT
trigger.
-- The ID of the doctor to book the appointment with.
SET @doctorID = 1;
-- The moment from which to start searching for availble time slots
SET @searchStart = CURRENT_TIMESTAMP;
-- The duration of the appointment to book, in minutes.
SET @duration = 20;
WITH
SlotStart AS (
-- This table will list all the 10-minute-aligned timestamps that occur after `@searchStart`
SELECT
CONVERT(@searchStart, DATE)
+ INTERVAL (EXTRACT(HOUR FROM @searchStart)) HOUR
+ INTERVAL ( EXTRACT(MINUTE FROM @searchStart) DIV 10 + seq + 1 ) * 10 MINUTE
AS startDateTime
FROM seq_0_to_1023
),
Slot AS (
SELECT
startDateTime,
startDateTime + INTERVAL @duration MINUTE AS endDateTime
FROM SlotStart
),
AvailableSlot AS (
SELECT
@doctorID AS doctorID,
startDateTime,
endDateTime
FROM Slot AS s
WHERE
slotIsAvailable(@doctorID, s.startDateTime, s.endDateTime)
AND EXTRACT(HOUR FROM s.startDateTime) >= 9
AND EXTRACT(HOUR FROM (s.endDateTime - INTERVAL 1 MINUTE)) <= 16
)
SELECT *
FROM AvailableSlot
WHERE
CONVERT(startDateTime, DATE) = CONVERT(@searchStart, DATE)
AND CONVERT(endDateTime, DATE) = CONVERT(@searchStart, DATE)
ORDER BY startDateTime ASC;
The above query, with the above sample records for Appointment
, and with @searchStart
equal to '2019-10-06 06:00'
, yields:
+----------+---------------------+---------------------+
| doctorID | startDateTime | endDateTime |
+----------+---------------------+---------------------+
| 1 | 2019-10-06 09:00:00 | 2019-10-06 09:20:00 |
| 1 | 2019-10-06 09:50:00 | 2019-10-06 10:10:00 |
| 1 | 2019-10-06 10:00:00 | 2019-10-06 10:20:00 |
| 1 | 2019-10-06 10:10:00 | 2019-10-06 10:30:00 |
| 1 | 2019-10-06 10:20:00 | 2019-10-06 10:40:00 |
| 1 | 2019-10-06 10:30:00 | 2019-10-06 10:50:00 |
| 1 | 2019-10-06 10:40:00 | 2019-10-06 11:00:00 |
| 1 | 2019-10-06 12:00:00 | 2019-10-06 12:20:00 |
| 1 | 2019-10-06 12:10:00 | 2019-10-06 12:30:00 |
| 1 | 2019-10-06 12:20:00 | 2019-10-06 12:40:00 |
| 1 | 2019-10-06 12:30:00 | 2019-10-06 12:50:00 |
| 1 | 2019-10-06 12:40:00 | 2019-10-06 13:00:00 |
| 1 | 2019-10-06 14:00:00 | 2019-10-06 14:20:00 |
| 1 | 2019-10-06 14:10:00 | 2019-10-06 14:30:00 |
| 1 | 2019-10-06 14:20:00 | 2019-10-06 14:40:00 |
| 1 | 2019-10-06 14:30:00 | 2019-10-06 14:50:00 |
| 1 | 2019-10-06 14:40:00 | 2019-10-06 15:00:00 |
| 1 | 2019-10-06 14:50:00 | 2019-10-06 15:10:00 |
| 1 | 2019-10-06 15:00:00 | 2019-10-06 15:20:00 |
| 1 | 2019-10-06 15:10:00 | 2019-10-06 15:30:00 |
| 1 | 2019-10-06 15:20:00 | 2019-10-06 15:40:00 |
| 1 | 2019-10-06 15:30:00 | 2019-10-06 15:50:00 |
| 1 | 2019-10-06 15:40:00 | 2019-10-06 16:00:00 |
| 1 | 2019-10-06 16:40:00 | 2019-10-06 17:00:00 |
+----------+---------------------+---------------------+