PostgreSQL custom week number - first week containing Feb 1st - postgresql

I'm new to SQL functions and trying to create a calendar table that displays custom week numbers, with each week starting with Saturday and ending on Friday. The first week of each year always contains Feb.1st of that year.
For example, if the day of the week of Feb. 1st for a particular year is Tuesday, then the first week for that year is from Jan. 29 to Feb. 4.
I've been struggling with this problem for a couple days and the only solution I can come up with is as follows:
First, I created a calendar table with a column called "CustomizedWeekNo" to reflect the year cycle starting from the week containing Feb. 1st. But the first day of each week is Monday.
Create Table Calendar
(CalendarDate Date, WeekNo smallInt, WeekDayNo text, CustomizedWeekNo smallInt)
Create or Replace Function CustomizeWeekNumber()
Returns void
as $$
Declare beginDate Date :='2015-01-31'; endDate Date := '2017-01-27';
Begin
While beginDate <= endDate loop
Insert Into Calendar (CalendarDate, WeekNo, WeekDayNo, CustomizedWeekNo)
Select
beginDate As CalendarDate
,DATE_PART('week', beginDate::timestamp)::smallint As WeekNo
,(Case When DATE_PART('isodow', beginDate::timestamp)::smallint = 6
Then 'Sat'
When DATE_PART('isodow', beginDate::timestamp)::smallint = 7
Then 'Sun'
When DATE_PART('isodow', beginDate::timestamp)::smallint = 1
Then 'Mon'
When DATE_PART('isodow', beginDate::timestamp)::smallint = 2
Then 'Tue'
When DATE_PART('isodow', beginDate::timestamp)::smallint = 3
Then 'Wed'
When DATE_PART('isodow', beginDate::timestamp)::smallint = 4
Then 'Thur'
Else 'Fri'
End) As WeekDayNo;
,(Case When beginDate < '2016-01-04'
Then DATE_PART('week', beginDate::timestamp)::smallint - 5
When beginDate >= '2016-01-04' and beginDate < '2016-01-30'
Then (date_part('week', '2016-01-03'::timestamp)::smallint - 5 + date_part('week', beginDate::timestamp)::smallint)
When beginDate >= '2016-01-30' and beginDate < '2017-01-02'
Then date_part('week', beginDate::timestamp)::smallint - 4
Else
date_part('week', '2017-01-01'::timestamp)::smallint - 4 + date_part('week', beginDate::timestamp)::smallint
End) As CustomizedWeekNo;
Select (beginDate + interval'1 day') into beginDate;
End loop;
End; $$
language plpgsql;
# Run the function
select CustomizeWeekNumber()
Next, I update the "CustomizedWeekNo" column
-- My customized week starts from every Saturday and ends on every Friday
update calendar
set CustomizedWeekNo = CustomizedWeekNo + 1
where WeekDayNo in ('Sat', 'Sun');
Lastly, I create another function to return the information I need. I also reformat the value of the "CustomizedWeekNo" to include the specific year.
create or replace function update_CustomizedWeek(date, date)
returns table(Calendar_Date Date, Week_No int, WeekDay_No text, Customized_Week_No int)
as $$
begin
return query
select t.CalendarDate, t.WeekNo, t.WeekDayNo,
case when t.CustomizedWeekNo <= 9
then (date_part('year', t.CalendarDate::timestamp)::text||'0'||t.CustomizedWeekNo::text)::int
else (date_part('year', t.CalendarDate::timestamp)::text||t.CustomizedWeekNo::text)::int
end
from Calendar t
where t.CalendarDate >= $1 and t.CalendarDate <= $2
order by t.CalendarDate;
end; $$
language plpgsql;
--Example
select * from update_CustomizedWeek('2015-01-30', '2015-02-10')
The final result will look like:
Calendar_Date | Week_No | WeekDay_No | Customized_Week_No
------------- | ------- | ---------- | -------------------
2015-01-31 | 5 | Sat | 201501
2015-02-01 | 5 | Sun | 201501
2015-02-02 | 6 | Mon | 201501
2015-02-03 | 6 | Tue | 201501
2015-02-04 | 6 | Wed | 201501
2015-02-05 | 6 | Thur | 201501
2015-02-06 | 6 | Fri | 201501
2015-02-07 | 6 | Sat | 201502
2015-02-08 | 6 | Sun | 201502
2015-02-09 | 7 | Mon | 201502
2015-02-10 | 7 | Tue | 201502
As you can see, I used a lot of "hard coding" here. I would like to be able to generate a date range along with the customized week number for any year, not just 2016 or 2017. Any help is really appreciated.

date_trunc() truncates to the previous Monday. You can still use it by adding 2 days to the input (the difference between Sat and Mon), then subtract 2 days from the output. Works perfectly.
This query produces your desired output exactly:
SELECT d::date AS "Calendar_Date"
, EXTRACT('WEEK' FROM d)::int AS "Week_No"
, to_char(d, 'Dy') AS "WeekDay_No"
, base_nr + (rn::int - 1) / 7 AS "Customized_Week_No"
FROM (
SELECT date_trunc('week', feb3) - interval '2 days' AS day1 -- subtract 2 days
, EXTRACT('year' FROM feb3)::int * 100 + 1 AS base_nr
FROM (SELECT timestamp '2015-02-03') input(feb3) -- add 2 days, so Feb 3 (!)
) t, generate_series (day1
, day1 + interval '1 year - 1 day'
, interval '1 day') WITH ORDINALITY AS d(d, rn);
Just provide Feb 3 of the respective year: timestamp '2015-02-03'.

Related

Week Day Starting from a Certain Day (01 Jan 2021) in Postgres

I am trying to get week numbers in a Year starting from a certain day
I've checked the stack but quite confused.
SELECT EXTRACT(WEEK FROM TIMESTAMP '2021-01-01'),
extract('year' from TIMESTAMP '2021-01-01')
The output is 53|2021
I want it to be 01|2021
I understand the principle of the isoweek but I want the year to start in 01-01-2021
The aim is to use intervals from this day to determine week numbers
Week N0| End Date
1 | 01-01-2021
2 | 01-08-2021
5 | 01-29-2021
...
This is really strange way to determine the week number, but in the end it's a simple math operation: the number of days since January first divided by 7.
You can create a function for this:
create function custom_week(p_input date)
returns int
as
$$
select (p_input - date_trunc('year', p_input)::date) / 7 + 1;
$$
language sql
immutable;
So this:
select date, custom_week(date)
from (
values
(date '2021-01-01'),
(date '2021-01-08'),
(date '2021-01-29')
) as v(date)
yields
date | custom_week
-----------+------------
2021-01-01 | 1
2021-01-08 | 2
2021-01-29 | 5

How to assign pay period numbers for given start date and end date

I have a table with the start and end dates of pay periods.
The period length (days) isn't static. It ranges from 0 to 24.
The number of pay periods could be 24-28 for a given year.
The only thing fixed is the first start date is 12/31/2000, and the next
start date is right after the previous end date, e.g. 1st end date =
1/13/2001, 2nd start date = 1/14/2001.
When the time progresses, new pay periods are added to the table.
I need to assign pay period number for a given calendar year. The rule is the period include January 1 is period 1 even though its start date is December in the previous year. For example, 1/31/2000-1/13/2001 should be period 1 for the year 2001. When next year's January 1 is included in the pay period, period 1 of next year starts (12/30/2001-1/12/2002 is Pay Period 1 for 2002).
I need to create a query for PostgreSQL or PostgreSQL function. I want to assign period 1 to the youngest date in the target year and increment pay period number till pay period reaches 28 or calendar year ends (before 1/1 of next year). I'm not sure about my logic and the part in a SELECT statement where I used calendar_pp which doesn't exist in the table.
Start End
1/14/2001 1/27/2001<br/>
1/28/2001 2/10/2001<br/>
2/11/2001 2/24/2001<br/>
2/25/2001 3/10/2001<br/>
3/11/2001 3/24/2001<br/>
3/25/2001 4/7/2001<br/>
4/8/2001 4/21/2001<br/>
4/22/2001 5/5/2001<br/>
5/6/2001 5/19/2001<br/>
5/20/2001 6/2/2001<br/>
6/3/2001 6/16/2001<br/>
6/17/2001 6/30/2001<br/>
7/1/2001 7/14/2001<br/>
7/15/2001 7/28/2001<br/>
7/29/2001 8/11/2001<br/>
8/12/2001 8/25/2001<br/>
8/26/2001 9/8/2001<br/>
9/9/2001 9/22/2001<br/>
9/23/2001 9/30/2001<br/>
10/1/2001 10/6/2001<br/>
10/7/2001 10/20/2001<br/>
10/21/2001 11/3/2001<br/>
11/4/2001 11/17/2001<br/>
11/18/2001 12/1/2001<br/>
12/2/2001 12/15/2001<br/>
12/16/2001 12/29/2001<br/>
12/30/2001 1/12/2002<br/>
CREATE OR REPLACE FUNCTION calendar_pp(end_date DATE)
RETURNS TABLE (
start_date_col DATE,
end_date_col DATE,
calendar_pp INTEGER)
AS $$
DECLARE
calendar_pp INTEGER ;
counter INTEGER := 0 ;
start_date DATE := TO_CHAR(TO_DATE(begin_payperiod_date,'MM/DD/YY'),'MM/DD') ;
end_date_year INTEGER := CAST((TO_CHAR(TO_DATE(min(end_payperiod_date),'MM/DD/YY'),'YYYY')) AS INTEGER) ;
target_year INTEGER := 2001 ; --1st pay period = 12/31/2000-1/13/2001
BEGIN
WHILE counter <= 28 LOOP --max pay period = 28
counter := counter + 1 ;
calendar_pp := calendar_pp + 1 ;
end_date_year := end_date_year + 1 ;
target_year := target_year + 1 ;
--assign pay period starting with 1 till end_date becomes next year
RETURN QUERY
SELECT TO_DATE(begin_payperiod_date,'MM/DD/YY'), TO_DATE(end_payperiod_date,'MM/DD/YY'), calendar_pp
FROM actacc.payperiod_conversion_all_years
WHERE end_date_year = target_year AND calendar_pp is null
order by TO_DATE(begin_payperiod_date,'MM/DD/YY')
END LOOP;
END; $$
LANGUAGE 'plpgsql' ;
This is the expected results.
Start End Pay Period
1/14/2001 1/27/2001 2<br/>
1/28/2001 2/10/2001 3<br/>
2/11/2001 2/24/2001 4<br/>
2/25/2001 3/10/2001 5<br/>
3/11/2001 3/24/2001 6<br/>
3/25/2001 4/7/2001 7<br/>
4/8/2001 4/21/2001 8<br/>
4/22/2001 5/5/2001 9<br/>
5/6/2001 5/19/2001 10<br/>
5/20/2001 6/2/2001 11<br/>
6/3/2001 6/16/2001 12<br/>
6/17/2001 6/30/2001 13<br/>
7/1/2001 7/14/2001 14<br/>
7/15/2001 7/28/2001 15<br/>
7/29/2001 8/11/2001 16<br/>
8/12/2001 8/25/2001 17<br/>
8/26/2001 9/8/2001 18<br/>
9/9/2001 9/22/2001 19<br/>
9/23/2001 9/30/2001 20<br/>
10/1/2001 10/6/2001 21<br/>
10/7/2001 10/20/2001 22<br/>
10/21/2001 11/3/2001 23<br/>
11/4/2001 11/17/2001 24<br/>
11/18/2001 12/1/2001 25<br/>
12/2/2001 12/15/2001 26<br/>
12/16/2001 12/29/2001 27<br/>
12/30/2001 1/12/2002 1<br/>
I'm not sure about the 28 requirement. Are you saying that the pay period should reset even if we aren't near Jan 1? Assuming that's not true, I think this should work:
with dates as (SELECT start_date, end_date
FROM (VALUES
('12/31/2000'::date,'1/13/2001'::date),
('1/14/2001','1/27/2001'),
('1/28/2001','2/10/2001'),
('2/11/2001','2/24/2001'),
('2/25/2001','3/10/2001'),
('3/11/2001','3/24/2001'),
('3/25/2001','4/7/2001'),
('4/8/2001','4/21/2001'),
('4/22/2001','5/5/2001'),
('5/6/2001','5/19/2001'),
('5/20/2001','6/2/2001'),
('6/3/2001','6/16/2001'),
('6/17/2001','6/30/2001'),
('7/1/2001','7/14/2001'),
('7/15/2001','7/28/2001'),
('7/29/2001','8/11/2001'),
('8/12/2001','8/25/2001'),
('8/26/2001','9/8/2001'),
('9/9/2001','9/22/2001'),
('9/23/2001','9/30/2001'),
('10/1/2001','10/6/2001'),
('10/7/2001','10/20/2001'),
('10/21/2001','11/3/2001'),
('11/4/2001','11/17/2001'),
('11/18/2001','12/1/2001'),
('12/2/2001','12/15/2001'),
('12/16/2001','12/29/2001'),
('12/30/2001','1/12/2002')
) v (start_date, end_date)
)
select start_date, end_date, rank() OVER (partition by date_trunc('year', end_date) ORDER BY end_date) FROM dates;
start_date | end_date | rank
------------+------------+------
2000-12-31 | 2001-01-13 | 1
2001-01-14 | 2001-01-27 | 2
2001-01-28 | 2001-02-10 | 3
2001-02-11 | 2001-02-24 | 4
2001-02-25 | 2001-03-10 | 5
2001-03-11 | 2001-03-24 | 6
2001-03-25 | 2001-04-07 | 7
2001-04-08 | 2001-04-21 | 8
2001-04-22 | 2001-05-05 | 9
2001-05-06 | 2001-05-19 | 10
2001-05-20 | 2001-06-02 | 11
2001-06-03 | 2001-06-16 | 12
2001-06-17 | 2001-06-30 | 13
2001-07-01 | 2001-07-14 | 14
2001-07-15 | 2001-07-28 | 15
2001-07-29 | 2001-08-11 | 16
2001-08-12 | 2001-08-25 | 17
2001-08-26 | 2001-09-08 | 18
2001-09-09 | 2001-09-22 | 19
2001-09-23 | 2001-09-30 | 20
2001-10-01 | 2001-10-06 | 21
2001-10-07 | 2001-10-20 | 22
2001-10-21 | 2001-11-03 | 23
2001-11-04 | 2001-11-17 | 24
2001-11-18 | 2001-12-01 | 25
2001-12-02 | 2001-12-15 | 26
2001-12-16 | 2001-12-29 | 27
2001-12-30 | 2002-01-12 | 1
(28 rows)

PostgreSQL: Query to find number of free days?

I have table as given below. This table is showing that which vehicle will be free/available for users from start date to end date.
Suppose
There is vehicle id = 1 is available for user from 2018-01-15 to 2020-02-28 (in yyyy-mm-dd format). In this period any user can take a vehicle at rent.
What i want:
I want to calculate the no of free days in particular period.
Here period: Jan-2018 means (1-Jan-2018 to 31-Jan-2018).
Calculation criteria for free days:
For vehicle Id = 1 --> start date = 2018-01-15 and end Date = 2020-02-28
For Jan-2018 = 16 days
(as total days in Jan is 31 but our start date is starting from 2018-01-15 for vehicle id =1)
For Feb-2018 = 28 days (between 2018-01-15 to 2020-02-28)
Select enddate, startdate, CASE
WHEN startdate<= '2017-01-01' and enddate>= '2017-01-31' THEN ('2017-01-31'::date - '2017-01-01'::date)+1
WHEN startdate<= '2017-01-01' and enddate< '2017-01-31' and enddate>= '2017-01-01' THEN (enddate::date - '2017-01-01') +1
WHEN startdate> '2017-01-01' and enddate>= '2017-01-31' and startdate<= '2017-01-31' THEN (('2017-01-31'::date - '2017-01-01'::date)+1) - EXTRACT(DAY FROM startdate::date))+1
WHEN startdate> '2017-01-01' and enddate< '2017-01-31' and startdate<= '2017-01-31' and enddate>= '2017-01-01' THEN (enddate::date - startdate::date)+1
end
as td from table1
You can use minus operator and daterange data type for these two dates.
postgres=# select *,upper(intersection_range)-lower(intersection_range) as available_days from (select *, daterange(start_date,end_date) * daterange('2018-01-01','2018-01-31') as intersection_range from vehicle) a;
vehicle_id | start_date | end_date | intersection_range | available_days
------------+------------+------------+-------------------------+----------------
1 | 2017-12-31 | 2020-02-28 | [2018-01-01,2018-01-31) | 30
2 | 2017-11-30 | 2018-02-28 | [2018-01-01,2018-01-31) | 30
3 | 2017-07-31 | 2019-02-28 | [2018-01-01,2018-01-31) | 30
(3 rows)
Best regards.

Count From Time Range In Postgresql

I have a query that count the total number of users registered in our service per day, per hour. My problem is that i want to appear the hour in range style. You can see below:
Query:
SELECT
case
when extract(dow from us.created::timestamp) = 0 then 'Sunday'
when extract(dow from us.created::timestamp) = 1 then 'Monday'
when extract(dow from us.created::timestamp) = 2 then 'Tuesday'
when extract(dow from us.created::timestamp) = 3 then 'Wednesday'
when extract(dow from us.created::timestamp) = 4 then 'Thursday'
when extract(dow from us.created::timestamp) = 5 then 'Friday'
when extract(dow from us.created::timestamp) = 6 then 'Saturday'
end as wday,
extract(hour from us.created::timestamp) as whour,
count(us.id)
FROM users us
GROUP BY wday,whour order by wday, whour
Query Result:
wday whour count
Friday 0 364
Friday 1 156
Friday 2 79
Friday 3 39
Friday 4 55
Friday 5 32 ....
I want to count and appear the results in this format:
wday whour count
Friday 0-1 364
Friday 1-2 156
Friday 2-3 79
Friday 3-4 39
Friday 4-5 55
Friday 5-6 32 ....
How can i do this?
try smth like :
WITH a as (
SELECT
to_char(us.created::timestamp,'Day') as wday,
extract(hour from us.created::timestamp) as whour,
count(us.id)
FROM users us
GROUP BY wday,whour
)
select
wday
, coalesce(whour,'-',lead(whour) over (partition by wday order by whour)) whour
, count
from a
order by wday, whour
here's the logic sample:
t=# with v(i,e) as (
values
('Friday',2),('Friday',3),('Saturday',4),('Saturday',5),('Saturday',6),('Friday',4),('Friday',5),('Friday',0),('Friday',1)
)
select i,e,concat(e,'-',lead(e) over (partition by i order by e))
from v;
i | e | concat
----------+---+--------
Friday | 0 | 0-1
Friday | 1 | 1-2
Friday | 2 | 2-3
Friday | 3 | 3-4
Friday | 4 | 4-5
Friday | 5 | 5-
Saturday | 4 | 4-5
Saturday | 5 | 5-6
Saturday | 6 | 6-
(9 rows)

Break into multiple rows based on date range of a single row

I have a table which captures appointments, some are single day appointments and some are multi day appointments, so the data looks like
AppointmentId StartDate EndDate
9 2017-04-12 2017-04-12
10 2017-05-01 2017-05-03
11 2017-06-01 2017-06-01
I want to split the multi day appointment as single days, so the result I am trying to achieve is like
AppointmentId StartDate EndDate
9 2017-04-12 2017-04-12
10 2017-05-01 2017-05-01
10 2017-05-02 2017-05-02
10 2017-05-03 2017-05-03
11 2017-06-01 2017-06-01
So I have split the appointment id 10 into multiple rows. I checked a few other questions like
here but those are to split just based on a single start date and end date and not based on table data
You can use a Calendar or dates table for this sort of thing.
For only 152kb in memory, you can have 30 years of dates in a table with this:
/* dates table */
declare #fromdate date = '20000101';
declare #years int = 30;
/* 30 years, 19 used data pages ~152kb in memory, ~264kb on disk */
;with n as (select n from (values(0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) t(n))
select top (datediff(day, #fromdate,dateadd(year,#years,#fromdate)))
[Date]=convert(date,dateadd(day,row_number() over(order by (select 1))-1,#fromdate))
into dbo.Dates
from n as deka cross join n as hecto cross join n as kilo
cross join n as tenK cross join n as hundredK
order by [Date];
create unique clustered index ix_dbo_Dates_date
on dbo.Dates([Date]);
Without taking the actual step of creating a table, you can use it inside a common table expression with just this:
declare #fromdate date = '20161229';
declare #thrudate date = '20170103';
;with n as (select n from (values(0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) t(n))
, dates as (
select top (datediff(day, #fromdate, #thrudate)+1)
[Date]=convert(date,dateadd(day,row_number() over(order by (select 1))-1,#fromdate))
from n as deka cross join n as hecto cross join n as kilo
cross join n as tenK cross join n as hundredK
order by [Date]
)
select [Date]
from dates;
Use either like so:
select
t.AppointmentId
, StartDate = d.date
, EndDate = d.date
from dates d
inner join appointments t
on d.date >= t.StartDate
and d.date <= t.EndDate
rextester demo: http://rextester.com/TNWQ64342
returns:
+---------------+------------+------------+
| AppointmentId | StartDate | EndDate |
+---------------+------------+------------+
| 9 | 2017-04-12 | 2017-04-12 |
| 10 | 2017-05-01 | 2017-05-01 |
| 10 | 2017-05-02 | 2017-05-02 |
| 10 | 2017-05-03 | 2017-05-03 |
| 11 | 2017-06-01 | 2017-06-01 |
+---------------+------------+------------+
Number and Calendar table reference:
Generate a set or sequence without loops - 1 - Aaron Bertrand
Generate a set or sequence without loops - 2 - Aaron Bertrand
Generate a set or sequence without loops - 3 - Aaron Bertrand
The "Numbers" or "Tally" Table: What it is and how it replaces a loop - Jeff Moden
Creating a Date Table/Dimension in sql Server 2008 - David Stein
Calendar Tables - Why You Need One - David Stein
Creating a date dimension or calendar table in sql Server - Aaron Bertrand
tsql Function to Determine Holidays in sql Server - Aaron Bertrand
F_table_date - Michael Valentine Jones
Clearly a Calendar/Tally table would be the way to go as SqlZim illustrated (+1), however you can use an ad-hoc tally table with a CROSS APPLY.
Example
Select A.AppointmentId
,StartDate = B.D
,EndDate = B.D
From YourTable A
Cross Apply (
Select Top (DateDiff(DD,A.StartDate,A.EndDate)+1) D=DateAdd(DD,-1+Row_Number() Over (Order By Number),A.StartDate)
From master..spt_values
) B
Returns
AppointmentId StartDate EndDate
9 2017-04-12 2017-04-12
10 2017-05-01 2017-05-01
10 2017-05-02 2017-05-02
10 2017-05-03 2017-05-03
11 2017-06-01 2017-06-01