Getting free periods from booked periods

  • I have this issue. I have a table with booked items, simplified here:

    declare @t table(dBookedFrom smalldatetime, dBookedTill smalldatetime)

    insert into @t
    values('20180101 12:00', '20180101 20:00')
    , ('20180103 08:00', '20180105 06:00')

    When I run a "select all" query I get 2 rows with the booked period:
    row 1: 2018-01-01 12:00:00 - 2018-01-01 20:00:00
    row 2: 2018-01-03 08:00:00 - 2018-01-05 06:00:00

    But I need the "free" periods now, so I need this result:
    row 1: null - 2018-01-01 12:00:00
    row 2: 2018-01-01 20:00:00 - 2018-01-03 08:00:00
    row 3: 2018-01-05 06:00:00 - null

    Anyone knows how to do this?
    Thanks!

  • SELECT
         dBookedTill AS FreeFrom
    ,    LEAD(dBookedFrom,1) OVER (ORDER BY dBookedFrom) AS FreeTo
    FROM @t
    UNION ALL
    SELECT
         NULL
    ,    MIN(dBookedFrom)
    FROM @t
    ORDER BY FreeFrom

    John

  • Cool...
    But when there are overlaps, the result is cot correct.
    Please try this:

    declare @t table(dBookedFrom smalldatetime, dBookedTill smalldatetime)

    insert into @t
    values('20180101 12:00', '20180101 20:00')
    , ('20180102 08:00', '20180104 06:00')
    , ('20180103 08:00', '20180105 06:00')

  • Technically you should not book overlaps.  The same book cannot be booked twice.  You need to add logic to prevent double booking (unless you are an airline). For example, run the query to show available free periods prior t inserting a booking.  If you have multiple books you would include that criteria in the PARTITION clause.

  • Good remark, but when there are adjacent bookings, where "till" date from booking 2 is same as "from" date of booking 3 then I also get an unwanted row (row 3), and these are common situations.
    Try this:
    declare @t table(dBookedFrom smalldatetime, dBookedTill smalldatetime)

    insert into @t
    values('20180101 12:00', '20180101 20:00')
    , ('20180102 08:00', '20180103 08:00')
    , ('20180103 08:00', '20180105 06:00')

  • NULL    2018-01-01 12:00:00
    2018-01-01 20:00:00    2018-01-02 08:00:00
    2018-01-03 08:00:00    2018-01-03 08:00:00  This row should not be there (for my result)
    2018-01-05 06:00:00    NULL

  • Try this.  It involves a triangular join, so performance may not be good on large data sets.

    WITH BeginningsandEndings AS (
        SELECT
             dBookedFrom
        ,    dBookedTill
        FROM @t
        UNION ALL
        SELECT NULL, '19000101' -- lower limit of smalldatetime
        UNION ALL
        SELECT '20790606', NULL -- upper limit of smalldatetime
        )
    , Joined AS (
        SELECT
             b1.dBookedTill AS FreeFrom
        ,    b2.dBookedFrom AS FreeTo
        ,    ROW_NUMBER() OVER (PARTITION BY b1.dBookedTill ORDER BY b2.dBookedFrom) AS RowNoFrom
        ,    ROW_NUMBER() OVER (PARTITION BY b2.dBookedFrom ORDER BY b1.dBookedTill DESC) AS RowNoTo
        FROM BeginningsandEndings b1
        JOIN BeginningsandEndings b2
        ON b1.dBookedTill < b2.dBookedFrom
        )
    SELECT
         FreeFrom
    ,    FreeTo
    FROM Joined
    WHERE RowNoFrom = 1
    AND RowNoTo = 1;

    John

  • Yes, this works like I want it!
    Thank you so much.🙂

  • SELECT

    RangeStart = MAX(CASE WHEN dir = 1 THEN dt ELSE NULL END),

    RangeEnd = MAX(CASE WHEN dir = 0 THEN dt ELSE NULL END)

    FROM (

    SELECT d.dir, d.dt,

    grp = SUM(dir) OVER(ORDER BY dt)

    FROM #t

    CROSS APPLY (VALUES (0, dBookedFrom), (1, dBookedTill)) d (dir, dt)

    ) d

    GROUP BY grp

    “Write the query the simplest way. If through testing it becomes clear that the performance is inadequate, consider alternative query forms.” - Gail Shaw

    For fast, accurate and documented assistance in answering your questions, please read this article.
    Understanding and using APPLY, (I) and (II) Paul White
    Hidden RBAR: Triangular Joins / The "Numbers" or "Tally" Table: What it is and how it replaces a loop Jeff Moden

  • marc.corbeel - Thursday, May 17, 2018 6:57 AM

    Good remark, but when there are adjacent bookings, where "till" date from booking 2 is same as "from" date of booking 3 then I also get an unwanted row (row 3), and these are common situations.
    Try this:
    declare @t table(dBookedFrom smalldatetime, dBookedTill smalldatetime)

    insert into @t
    values('20180101 12:00', '20180101 20:00')
    , ('20180102 08:00', '20180103 08:00')
    , ('20180103 08:00', '20180105 06:00')

    SELECT

    RangeStart = MAX(CASE WHEN dir = 1 THEN dt ELSE NULL END),

    RangeEnd = MAX(CASE WHEN dir = 0 THEN dt ELSE NULL END)

    FROM (

    SELECT d.dir, d.dt,

    grp = SUM(dir) OVER(ORDER BY dt)

    FROM #t

    CROSS APPLY (VALUES (0, dBookedFrom), (1, dBookedTill)) d (dir, dt)

    ) d

    GROUP BY grp

    HAVING CASE WHEN MAX(CASE WHEN dir = 1 THEN dt ELSE NULL END) = MAX(CASE WHEN dir = 0 THEN dt ELSE NULL END) THEN 1 ELSE 0 END <> 1

    “Write the query the simplest way. If through testing it becomes clear that the performance is inadequate, consider alternative query forms.” - Gail Shaw

    For fast, accurate and documented assistance in answering your questions, please read this article.
    Understanding and using APPLY, (I) and (II) Paul White
    Hidden RBAR: Triangular Joins / The "Numbers" or "Tally" Table: What it is and how it replaces a loop Jeff Moden

  • Chris, this returns also the unwanted 3rd row

  • All right, with the HAVING clause it works like it should.
    Thanks

  • My thought would be to pre-allocate periods, using the ISO half open interval model. Then each client would be given one or more periods. The the unallocated periods would be very easy to find. The trade-off is that you have to decide how you want to do your intervals. Psychiatrist have the 50 minute hour. Most places find that cutting the day into 15 minute chunks works..Here is a quick skeleton: 

    CREATE TABLE Periods
    (period_name CHAR(??)  NOT NULL,
     start_timestamp DATETIME2(0) NOT NULL, 
     end_timestamp DATETIME2(0) NOT NULL,
    PRIMARY KEY (period_name, start_timestamp),
     CHECK (start_timestamp <  end_timestamp_timestamp));

    CREATE TABLE Appointments
    (client_id CHAR(10) NOT NULL,
     period_name CHAR(??)  NOT NULL
     REFERENCES Periods  (period_name),
      etc); 

    Please post DDL and follow ANSI/ISO standards when asking for help. 

Viewing 13 posts - 1 through 12 (of 12 total)

You must be logged in to reply to this topic. Login to reply