Re: How to find earlest possible start times for given duration excluding reservations

From: Thom Brown <thom(at)linux(dot)com>
To: Andrus <kobruleht2(at)hot(dot)ee>
Cc: PGSQL Mailing List <pgsql-general(at)postgresql(dot)org>
Subject: Re: How to find earlest possible start times for given duration excluding reservations
Date: 2014-10-28 16:42:24
Message-ID: CAA-aLv6NDTUwV=P3z_aO88cwqLUfTdb8ttS9rjgrQYby7pyJaQ@mail.gmail.com
Views: Raw Message | Whole Thread | Download mbox | Resend email
Thread:
Lists: pgsql-general

On 28 October 2014 15:10, Andrus <kobruleht2(at)hot(dot)ee> wrote:

> Hi!
>
> I'm looking for finding ealiest possible start times from reservations
> table.
>
> People work from 10:00AM to 21:00PM in every week day except Sunday and
> public holidays.
>
> Jobs for them are reserved at 15 minute intervals and whole job must fit
> to single day.
> Job duration is from 15 minutes to 4 hours.
>
> Reservat table contains reservations, yksus2 table contains workes and
> pyha table contains public holidays. Table structures are below. Reservat
> structure can changed if this helps.
>
> How to first earliest 30 possible start times considering existing
> reservations ?
>
> For example, Mary has already reservation at 12:30 .. 16:00 and
> John has already reservation at 12:00 to 13:00
>
> In this case query for job with duration of 1.5 hours should return
>
> John 2014-10-28 10:00
> Mary 2014-10-28 10:00
> John 2014-10-28 10:30
> Mary 2014-10-28 10:30
> Mary 2014-10-28 11:00
> John 2014-10-28 13:00
> Mary 2014-10-28 16:00
> Mary 2014-10-28 16:30
> ... etc and also starting from next days
>
> I tried query based on answer in http://stackoverflow.com/
> questions/13433863/how-to-return-only-work-time-from-
> reservations-in-postgresql below but it returns wrong result:
>
> MARY 2014-10-28 13:00:00
> MARY 2014-10-29 22:34:40.850255
> JOHN 2014-10-30 22:34:40.850255
> MARY 2014-10-31 22:34:40.850255
> MARY 2014-11-03 22:34:40.850255
>
> Also sliding start times 10:00, 10:30 etc are not returned.
>
> How to get proper first reservations ?
>
> Query which I tried is
>
> insert into reservat (objekt2, during) values
> ('MARY', '[2014-10-28 11:30:00,2014-10-28 13:00:00)'),
> ('JOHN', '[2014-10-28 10:00:00,2014-10-28 11:30:00)');
>
> with gaps as (
> select
> yksus,
> upper(during) as start,
> lead(lower(during),1,upper(during)) over (ORDER BY during) -
> upper(during) as gap
> from (
> select
> yksus2.yksus,
> during
> from reservat join yksus2 on reservat.objekt2=yksus2.yksus
> where upper(during)>= current_date
> union all
> select
> yksus2.yksus,
> unnest(case
> when pyha is not null then array[tsrange1(d, d +
> interval '1 day')]
> when date_part('dow', d) in (0, 6) then
> array[tsrange1(d, d + interval '1 day')]
> when d::date = current_Date then array[
> tsrange1(d, current_timestamp ),
> tsrange1(d + interval '20 hours', d +
> interval '1 day')]
> else array[tsrange1(d, d + interval '8 hours'),
> tsrange1(d + interval '20 hours', d +
> interval '1 day')]
> end)
> from yksus2, generate_series(
> current_timestamp,
> current_timestamp + interval '1 month',
> interval '1 day'
> ) as s(d)
> left join pyha on pyha = d::date
> ) as x
> )
>
> select yksus, start
> from gaps
> where gap >= interval'1hour 30 minutes'
> order by start
> limit 30
>
>
> Schema:
>
> CREATE EXTENSION btree_gist;
> CREATE TABLE Reservat (
> id serial primary key,
> objekt2 char(10) not null references yksus2 on update cascade
> deferrable,
> during tsrange not null check(
> lower(during)::date = upper(during)::date
> and lower(during) between current_date and current_date+
> interval'1 month'
>
> and (lower(during)::time >= '10:00'::time and
> upper(during)::time < '21:00'::time)
> AND EXTRACT(MINUTE FROM lower(during)) IN (0, 15, 30,45)
> AND EXTRACT(MINUTE FROM upper(during)) IN (0, 15, 30, 45)
> and (date_part('dow', lower(during)) in (1,2,3,4,5,6)
> and date_part('dow', upper(during)) in (1,2,3,4,5,6))
> ),
>
> EXCLUDE USING gist (objekt2 WITH =, during WITH &&)
> );
>
> create or replace function holiday_check() returns trigger language
> plpgsql stable as $$
> begin
> if exists (select * from pyha where pyha in
> (lower(NEW.during)::date, upper(NEW.during)::date)) then
> raise exception 'public holiday %', lower(NEW.during) ;
> else
> return NEW;
> end if;
> end;
> $$;
>
> create trigger holiday_check_i before insert or update on Reservat for
> each row execute procedure holiday_check();
>
> CREATE OR REPLACE FUNCTION public.tsrange1(start timestamp with time
> zone,
> finish timestamp with time zone ) RETURNS tsrange AS
> $BODY$
> SELECT tsrange(start::timestamp without time zone, finish::timestamp
> without time zone );
> $BODY$ language sql immutable;
>
>
> -- Workers
> create table yksus2( yksus char(10) primary key);
> insert into yksus2 values ('JOHN'), ('MARY');
>
> -- public holidays
> create table pyha( pyha date primary key);
>
>
> I posted it also in http://stackoverflow.com/questions/26608683/how-to-
> find-first-free-start-times-from-reservations-in-postgres
>
> Andrus.

Would you be able to adapt this to your needs?:

CREATE TABLE yksus2 (yksus char(10) PRIMARY KEY);
INSERT INTO yksus2 VALUES ('JOHN'),('MARY');

CREATE TABLE reservat
(
id serial primary key,
objekt2 char(10) REFERENCES yksus2 (yksus),
during tstzrange
);

ALTER TABLE reservat ADD CONSTRAINT time_between_1000_and_2100
CHECK (lower(during) >= (lower(during)::date + '10:00'::time)::timestamptz
AND upper(during) < (upper(during)::date + '21:00+1'::time)::timestamptz);

ALTER TABLE reservat ADD CONSTRAINT time_at_15_min_offset
CHECK (extract(epoch from lower(during)::time)::int % (60*15) = 0);

ALTER TABLE reservat ADD CONSTRAINT duration_between_15min_and_4hours
CHECK (upper(during) - lower(during) between '15 mins'::interval and '4
hours'::interval);

INSERT INTO reservat (objekt2, during)
VALUES ('MARY','[2014-10-28 12:30+0,2014-10-28 16:00+0)'::tstzrange);
INSERT INTO reservat (objekt2, during)
VALUES ('JOHN','[2014-10-28 12:00+0,2014-10-28 13:00+0)'::tstzrange);

SELECT yksus2.yksus, times.period
FROM generate_series('2014-10-28 10:00+0'::timestamptz, '2014-10-28
21:00+0', '15 mins'::interval) times(period)
CROSS JOIN yksus2
LEFT JOIN reservat ON tstzrange(times.period,times.period + '1 hour 30
mins'::interval, '[)') && reservat.during AND yksus2.yksus =
reservat.objekt2
WHERE reservat.during IS NULL
ORDER BY 2, 1
LIMIT 30;

yksus | period
------------+------------------------
JOHN | 2014-10-28 10:00:00+00
MARY | 2014-10-28 10:00:00+00
JOHN | 2014-10-28 10:15:00+00
MARY | 2014-10-28 10:15:00+00
JOHN | 2014-10-28 10:30:00+00
MARY | 2014-10-28 10:30:00+00
MARY | 2014-10-28 10:45:00+00
MARY | 2014-10-28 11:00:00+00
JOHN | 2014-10-28 13:00:00+00
JOHN | 2014-10-28 13:15:00+00
JOHN | 2014-10-28 13:30:00+00
JOHN | 2014-10-28 13:45:00+00
JOHN | 2014-10-28 14:00:00+00
JOHN | 2014-10-28 14:15:00+00
JOHN | 2014-10-28 14:30:00+00
JOHN | 2014-10-28 14:45:00+00
JOHN | 2014-10-28 15:00:00+00
JOHN | 2014-10-28 15:15:00+00
JOHN | 2014-10-28 15:30:00+00
JOHN | 2014-10-28 15:45:00+00
JOHN | 2014-10-28 16:00:00+00
MARY | 2014-10-28 16:00:00+00
JOHN | 2014-10-28 16:15:00+00
MARY | 2014-10-28 16:15:00+00
JOHN | 2014-10-28 16:30:00+00
MARY | 2014-10-28 16:30:00+00
JOHN | 2014-10-28 16:45:00+00
MARY | 2014-10-28 16:45:00+00
JOHN | 2014-10-28 17:00:00+00
MARY | 2014-10-28 17:00:00+00
(30 rows)

--
Thom

In response to

Responses

Browse pgsql-general by date

  From Date Subject
Next Message Michael P. Soulier 2014-10-28 17:25:02 cannot drop user
Previous Message Andrus 2014-10-28 15:10:30 How to find earlest possible start times for given duration excluding reservations