Getting Unavailable dates for renting a product that has stocks
Asked Answered
P

1

2

Database queries, normally so simple, yet sometimes so difficult. (brain trainer)

So I have products, stocks and rentStockOrders. These products can be rented for a set of days. The stocks also have a date when they are available. If a new product (stock) can be rented depends on the already rented stocks of that product.

  • A stock item cannot be rented before it's available date.
  • A rentStockOrder (linked between order and stocks) contains the bookings, thus rentStartDate and rentEndDate.
  • A product can be rented for a set of days, where the start date is not given. The product is selected, and after that a date time picker is used to select a starting day for renting.
  • An overall minimum and maximum date is applied (about a year ahead).

The idea is that the user didn't select the start date yet, before the user is able to do that I want to disable certain dates in a datetimepicker that cannot be used as starting date because there are no stocks available for the product's renting period.

To put it in a context: One product is selected, the user is given the option to specify a length in days he wants to rent this product (1 week, 2 weeks or 3 weeks). When the user has selected that, they must select a start date. Instead of every time showing an error that this date is not available, I rather disable start dates before hand.

Since it is more often that a product is available for renting then not, I think it is better to send a list of unavailable select dates to my website instead of a whole list of available dates. So the days that are not available cannot be clicked in a date time picker.

Most examples I have found so far include a input parameter for start and end date which I don't have, all I have a length in days a product wants to be rented and how many stocks are already rented for certain time frames.

EDIT:

As requested, test data and tables:

Stocks

+---------+-----------+-------------------+
| stockId | productId | availableFromDate |
+---------+-----------+-------------------+
|       1 |         1 | 1-01-2016         |
|       2 |         1 | 1-01-2016         |
+---------+-----------+-------------------+

RentStockOrders

+------------------+---------+----------------+----------------+
| rentStockOrderId | stockId | beginRentDate  |  endRentDate   |
+------------------+---------+----------------+----------------+
|                1 |       1 | 15-1-2016      | 14-2-2016      |
|                2 |       2 | 30-1-2016      | 20-2-2016      |
|                3 |       2 | 26-2-2016      | 7-3-2016       |
|                4 |       1 | 29-2-2016      | 14-3-2016      |
+------------------+---------+----------------+----------------+

Based on these records, I want to generate a list of unavailable dates. I've left out some columns for simplicity

Input is a day and a productId. So if I would input for days: 14 and for productId: 1 I would have some of the following expected results:

  • 25-01-2016 (stockId 1 is already booked, and stock 2 is booked soon, 14 days not possible.
  • 30-01-2016 (both booked)
  • 13-02-2016 (stock 1 is not back yet)
  • 17-02-2016 (stock 2 already booked, stock 1 will be rented in 13 days, not enough for 14).
  • ..and a lot more where both stocks are already rented.

What I would NOT expect is for example 15-02-2016, because Stock 1 would be available for the next 14 days.

If it is to difficult, then perhaps getting the available dates is simpler and I will switch this around in code. In this example it would be less data to pull from the database, but in reality there are about 250 items of one product so getting the unavailable dates perhaps better.

I've tried this answer to get the available dates, with no success yet, no errors, just returns no data.

declare @startDate datetime, @endDate datetime, @days int
select @startDate = '2016/01/01', @endDate='2016/03/31', @days=2

select stockId, min(endRentDate)
from
    (
    select  stockId ,endRentDate,
            (select top 1 endRentDate
            from RentStockOrders sInner
            where sInner.endRentDate > sOuter.beginRentDate
                    and sInner.stockId = sOuter.stockId
                    and sInner.endRentDate between @startDate and @endDate
            order by sInner.endRentDate) as nextAvailableDate
    from    RentStockOrders sOuter
    where sOuter.beginRentDate between @startDate and @endDate
    ) sub
group by stockId, nextAvailableDate
having dateDiff(d, min(endRentDate), isNull(nextAvailableDate,dateAdd(d,1,@endDate))) >= @days
Pianist answered 2/12, 2015 at 22:1 Comment(7)
Bad habits to kick : using old-style JOINs - that old-style comma-separated list of tables style was replaced with the proper ANSI JOIN syntax in the ANSI-92 SQL Standard (more than 20 years ago) and its use is discouragedAtkins
Well thanks for that, was always wondering why.. Got a solution for this question though? :)Pianist
On the right there is a related question: #15797346Safari
The explanation was great, but to help you with sql question we need db schema, sample data and desire output. Othewise we had to lost time constructing something and not always get it right .Ballistics
Alright, I will update my question, I'm trying to check @Safari 's reference to see if that can get me going.Pianist
I've updated my answer @JuanCarlosOropeza :)Pianist
I think a SQL only solution is impossible. It depends on you desired output though. I second Juan Carlos Oropeza comment to supply desired output. This helps enormously. If you want a recordset with all individual occupied dates I don't think a SQL only solution is possible. Just use a stored procedure, in your case TSQL. I made a pretty performant/optimized Oracle PLSQL version. See my answer below.Sublingual
S
1

This question is pretty difficult if you want to just use SQL if not impossible. I have made an pretty optimized/performant solution using Oracle PL/SQL and SQL. You can easily translate it to TSQL. The function returns a set/collection of dates. I also made another version which returns one big string with space seperated date values like "01-01-2016 02-01-2016 03-01-2016" etc... You could also make a version which return all seperate dates as date periodstrings for example "01-01-2016/10-01-2016 15-01-2016/25-01-2016" which you can then easily send to- and parse- in your application.

set serveroutput on;

drop table Product cascade constraints;
drop table Stocks cascade constraints;
drop table RentStockOrders cascade constraints;

create table Product (
  productId     number primary key,
  description   varchar2(255)
);

create table Stocks (
  stockId           number primary key,
  productId         number references Product(productId),
  availableFromDate date
);

create table RentStockOrders (
  rentStockOrderId  number primary key,
  stockId           number references Stocks(stockId),
  beginRentDate     date,
  endRentDate       date
);

insert into Product values (1,'product 1');
insert into Product values (2,'product 2');

insert into Stocks values (1,1,to_date('01-01-2016','dd-mm-yyyy'));
insert into Stocks values (2,1,to_date('01-01-2016','dd-mm-yyyy'));
insert into Stocks values (3,2,to_date('01-01-2016','dd-mm-yyyy'));
insert into Stocks values (4,2,to_date('01-01-2016','dd-mm-yyyy'));

insert into RentStockOrders values (1,1,to_date('15-01-2016','dd-mm-yyyy'),to_date('14-02-2016','dd-mm-yyyy'));
insert into RentStockOrders values (2,2,to_date('30-01-2016','dd-mm-yyyy'),to_date('20-02-2016','dd-mm-yyyy'));
insert into RentStockOrders values (3,2,to_date('26-02-2016','dd-mm-yyyy'),to_date('07-03-2016','dd-mm-yyyy'));
insert into RentStockOrders values (4,1,to_date('29-02-2016','dd-mm-yyyy'),to_date('14-03-2016','dd-mm-yyyy'));

--insert into RentStockOrders values (5,3,to_date('15-01-2016','dd-mm-yyyy'),to_date('14-02-2016','dd-mm-yyyy'));
insert into RentStockOrders values (6,4,to_date('20-01-2016','dd-mm-yyyy'),to_date('25-01-2016','dd-mm-yyyy'));
--insert into RentStockOrders values (7,4,to_date('01-01-2016','dd-mm-yyyy'),to_date('01-04-2016','dd-mm-yyyy'));
insert into RentStockOrders values (8,3,to_date('17-01-2016','dd-mm-yyyy'),to_date('25-01-2016','dd-mm-yyyy'));


--stocks with productId X which are rented for coming year from date Y with rentPeriode Z
select *
from RentStockOrders rso, Stocks s
where rso.stockId=s.stockId
and s.productId=1
and rso.beginRentDate>=to_date('01-01-2016','dd-mm-yyyy')-14
and rso.endRentDate<=to_date('01-01-2016','dd-mm-yyyy')+365
order by beginRentDate;


create or replace package my_globals
as
  --type has to be globally declared to be used as a return type
  type t_dates is table of date INDEX BY pls_integer;

  cursor c_searchRentData(p_productid number, p_beginDate date, p_endDate date, p_rentPeriod pls_integer) is
    select beginRentDate,endRentDate
    from RentStockOrders rso, Stocks s
    where rso.stockId=s.stockId
    and s.productId=p_productid
    and rso.beginRentDate>=p_beginDate-p_rentPeriod
    and rso.endRentDate<=p_endDate
    order by beginRentDate;
end;

/

--helper function tot return more future (or larger) date of two dates
create or replace function maxDate (p_date1 date, p_date2 date)
return date
is
begin
  if p_date1>=p_date2 then
    return p_date1;
  else
    return p_date2;
  end if;
end;

/

create or replace function getBlockedDates (p_productid number, p_beginDate date, p_endDate date, p_rentPeriod pls_integer)
return my_globals.t_dates
as
  v_dates     my_globals.t_dates;
  v_begindate date;
  v_enddate   date;
  i           pls_integer;
begin
  i:=1; --collection counts from 1
  v_enddate:=p_beginDate-1;
  for r_date in my_globals.c_searchRentData(p_productid, p_beginDate, p_endDate, p_rentPeriod)
  loop
    if (v_enddate < r_date.beginRentDate) or (v_enddate < r_date.endRentDate)
    then
      --if previous enddate is bigger use that one
      v_begindate:=maxDate(r_date.beginRentDate-p_rentPeriod,v_enddate+1); --first date of blocked period
      v_enddate:=maxDate(r_date.endRentDate,v_enddate+1); --last date of blocked period

      for j in 1..v_enddate-v_begindate+1 loop
        v_dates(i):=v_begindate+j-1;
        i:=i+1;
      end loop;
    end if;
  end loop;
  return v_dates;
end;

/

create or replace function getBlockedDatesAsStr (p_productid number, p_beginDate date, p_endDate date, p_rentPeriod pls_integer)
return varchar2
as
  v_dates     varchar2(4096) := ''; --should be sufficient for one year of blocked dates
  v_begindate date;
  v_enddate   date;
  i           pls_integer;
begin
  i:=1; --collection counts from 1
  v_enddate:=p_beginDate-1;
  for r_date in my_globals.c_searchRentData(p_productid, p_beginDate, p_endDate, p_rentPeriod)
  loop
    if (v_enddate < r_date.beginRentDate) or (v_enddate < r_date.endRentDate)
    then
      --if previous enddate is bigger use that one
      v_begindate:=maxDate(r_date.beginRentDate-p_rentPeriod,v_enddate+1); --first date of blocked period
      v_enddate:=maxDate(r_date.endRentDate,v_enddate+1); --last date of blocked period

      for j in 1..v_enddate-v_begindate+1 loop
        v_dates:=v_dates||' '||to_char(v_begindate+j-1,'dd-mm-yyyy');
        i:=i+1;
      end loop;
    end if;
  end loop;
  return ltrim(v_dates);
end;

/

create or replace function FindAndSplit(haystack in out varchar2, needle in varchar2)
  return varchar2
is
  s2    varchar2(1000);
  idx   pls_integer;
begin
  --dbms_output.put_line('in:'||haystack);
  idx:=instr(haystack,needle);
  if (idx=0) then
    --return full haystack when needle not found
    s2:=haystack;
    --remaining haystack is empty
    haystack:='';
    return s2;
  end if;
  --find string left at idx
  s2:=substr(haystack,1,idx-1);
  --dbms_output.put_line('out:'||s2);
  --remaining haystack is string right at idx
  haystack:=substr(haystack,idx+1,length(haystack)-idx);
  --dbms_output.put_line('return:'||haystack);
  return s2;
end;

/

--testcases
declare
 v_dates   my_globals.t_dates;
 i          pls_integer;
begin
  --store the result of stored function in local collection
  v_dates:=getBlockedDates(1, to_date('01-01-2016','dd-mm-yyyy'), to_date('01-01-2016','dd-mm-yyyy')+365, 1);
  --iterate through collection
  FOR i IN 1 .. v_dates.count LOOP
      dbms_output.put_line('Blocked date: '||v_dates(i));
  end loop;

  dbms_output.put_line('');

  --store the result of stored function in local collection
  v_dates:=getBlockedDates(2, to_date('01-01-2016','dd-mm-yyyy'), to_date('01-01-2016','dd-mm-yyyy')+365, 1);
  --iterate through collection
  FOR i IN 1 .. v_dates.count LOOP
      dbms_output.put_line('Blocked date: '||v_dates(i));
  end loop;  
end;

/

declare
 v_dates    varchar2(4096);
 v_date     varchar2(10);
 i          pls_integer;
begin
  --store the result of stored function in local string
  v_dates:=getBlockedDatesAsStr(1, to_date('01-01-2016','dd-mm-yyyy'), to_date('01-01-2016','dd-mm-yyyy')+365, 1);
  dbms_output.put_line(v_dates);
  --iterate through string
  loop
      v_date:=FindAndSplit(v_dates,' ');
      dbms_output.put_line('Blocked date: '||v_date);
      exit when v_dates is null;
  end loop;

  dbms_output.put_line('');

  --store the result of stored function in local string
  v_dates:=getBlockedDatesAsStr(2, to_date('01-01-2016','dd-mm-yyyy'), to_date('01-01-2016','dd-mm-yyyy')+365, 1);
  --iterate through string
  loop
      v_date:=FindAndSplit(v_dates,' ');
      dbms_output.put_line('Blocked date: '||v_date);
      exit when v_dates is null;
  end loop;
end;
Sublingual answered 4/12, 2015 at 17:58 Comment(3)
Just to let you know that it is possible. This question was also posted on dba.stackexchange.com I wrote an answer there. It uses a Calendar table.Keramics
Nice solution although I like my solution better because it's faster, simpler and smaller footprint, no need for extra table, easier to understand and modify/iterate upon. You really only need the stored function getBlockedDates and 1 query, rest is create/insert/helper functions/testcases. But maybe I am biased :)Sublingual
usually explicit cursors and user-defined functions are substantially slower than set-based SQL (in SQL Server, don't know about Oracle), but you should check and compare with your real data and hardware.Keramics

© 2022 - 2024 — McMap. All rights reserved.