Return number of weeks in Postgres - sql

This is what I am using right now and it returns the number of days between beginDate and endDate
date( ' $(#endDate)') - date('$(#beginDate)') as weekNumber
How can I get it to return the number of weeks between the two?

The simplest way would be to divide the number of days by seven, although that would not take into account the starting day of the week.
You could use this instead:
extract (week from date '$(#endDate)') -extract (week from date '$(#startDate)')
which would use iso8601 week numbers. But beware of spanning years!

Version 1:
/*
Design:
Week begins on day of start date
Options:
1) Count only whole weeks
2) Count partial weeks on the right side (end date side)
*/
select
sum(case when ('2013-02-08'::date - ind::date) >= 7 then 1 else 0 end) as whole_weeks,
count(*) as partial_right
from
generate_series('2013-01-01'::date /*dow=2*/,'2013-02-05'::date /*dow=2*/,'7 days') ind
Version 2:
/*
Design:
Week begins on specific day of week (5 chosen in this example)
Options:
1) Count only whole weeks
2) Count partial weeks on the right side (end date side)
3) Count partial weeks on the left side (start date side)
4) Count partial weeks on both sides
*/
select
sum(case when days = 7 then 1 else 0 end) as whole_weeks,
sum(case when days = 7 or max_ind = week_start then 1 else 0 end) as partial_right,
sum(case when days = 7 or week_start < min_ind then 1 else 0 end) as partial_left,
count(*) as partial_both_sides
from
(
select
ind - (case when dow < bow then dow + 7 - bow else dow - bow end)::int as week_start,
count(*) as days,
min(ind) as min_ind,
max(ind) as max_ind
from
(select
ind::date as ind,
extract(isodow from ind) as dow,
5::int as bow
from
generate_series('2013-01-01'::date /*dow=2*/,'2013-02-08'::date /*dow=5*/,'1 day') ind
) inp
group by
week_start
) t

Related

How to only add business days to a date in BigQuery?

For a given date I want to add business days to it. For example, if today is 10-17-2022 and I have a field that is 8 business days. How can I add 8 business days to 10-17-2022 which would be 10-27-2022.
Current Data:
BUSINESS_DAYS
Date
8
10-11-2022
10
10-13-2022
9
10-12-2022
Desired Output Data
BUSINESS_DAYS
Date
FINAL_DATE
8
10-11-2022
10-21-2022
10
10-13-2022
10-27-2022
9
10-12-2022
10-25-2022
As you can see we are skipping all weekends. We can ignore holidays for now.
Update:
Using
The suggest logic I got the following answer. I changed the names up.
I used:
DATE_ADD(A.PO_SENT_DATE , INTERVAL
(CAST(PREDICTED_LEAD_TIME AS INT64)
+ (date_diff(A.PO_SENT_DATE , DATE_ADD(A.PO_SENT_DATE , INTERVAL CAST(PREDICTED_LEAD_TIME AS INT64) DAY), week)* 2))
DAY) as FINAL_DATE
Update2: Using the following:
DATE_ADD(`Date`, INTERVAL
(BUSINESS_DAYS
+ (date_diff( DATE_ADD(`Date`, INTERVAL BUSINESS_DAYS DAY),`Date`, week) * 2))
DAY) as FINAL_DATE
There are instances where the result falls on the weekend. See screenshot below. 10-22-2022 falls on a Saturday.
Consider below simple solution
select *,
( select day
from unnest(generate_date_array(date, date + (div(business_days, 5) + 1) * 7)) day
where not extract(dayofweek from day) in (1, 7)
qualify row_number() over(order by day) = business_days + 1
) final_date
from your_table
if applied to sample data in your question
with your_table as (
select 8 business_days, date '2022-10-11' date union all
select 10, '2022-10-13' union all
select 9, '2022-10-12'
)
output is
The solution from #mikhailberlyant is really really cool, and very innovative. However if you have a lot of rows in your table and value of "business_days" column varies a lot, query will be less efficient especially for larger "business_days" values as implementation needs to generate entire range of array for each row, unnest it, and then do manipulation in that array.
This might help you do calculation without any array business:
select day, add_days as add_business_days,
DATE_ADD(day, INTERVAL cast(add_days +2*ceil((add_days -(5-(
(case when EXTRACT(DAYOFWEEK FROM day) = 7 then 1 else EXTRACT(DAYOFWEEK FROM day) end)
-1)))/5)+(case when EXTRACT(DAYOFWEEK FROM day) = 7 then 1 else 0 end) as int64) DAY) as final_day
from
(select parse_date('%Y-%m-%d', "2022-10-11") as day, 8 as add_days)

Get average duration per week-day from a list of records with start and end date

I have a input table with three columns :
id => string
start_date => timestamptz
end_date => timestamptz
I want to get the average duration in seconds (end_date - start_date) per week-day number over records.
My problem is : If I have a record where interval between start_date and end_date is 4 days, I want to get the result per day, not only at the start_date or end_date, and if I have no records between 3 weeks for example, take no value for a weekday as 'zero' value in the average.
Example :
id
start_date
end_date
1 (Friday to Sunday)
2021-03-12T01:00:00.000Z
2021-03-14T01:00:00.000Z
2 (Friday)
2021-03-12T01:00:00.000Z
2021-03-12T05:00:00.000Z
3 (Wed.)
2021-03-03T16:00:00.000Z
2021-03-03T17:00:00.000Z
Expected result (european weekday here for example, sunday is 7) :
weekday
avg_duration_seconds
1
0
2
0
3
1800
4
0
5
48600
6
86400
7
3600
Thank's for your help !
Note: the following works on Postgres as you tagged that as well. I have no idea if this works on CockroachDB as well.
You can "expand" the start/end timestamps to days by using generate_series(). To calculate the effective duration on each day, the full days need to be treated differently than the partial days at the start and end. Once those timestamps are calculated it's easy to get the duration per day. The do a left join on all weekdays and group by them:
select x.weekday,
avg(extract(epoch from real_end - real_start)) as duration
from generate_series(1,7) as x(weekday)
left join (
select t.id,
extract(isodow from g.dt) as weekday,
case
when start_date < g.dt then date_trunc('day', g.dt)
else start_date
end as real_start,
case
when end_date::date > g.dt then date_trunc('day', g.dt::date + 1)
else end_date
end as real_end
from the_table t
cross join generate_series(start_date, end_date, interval '1 day') as g(dt)
) t on x.weekday = t.weekday
group by x.weekday
order by x.weekday;
I am not 100% my expressions for "real_start" and "real_end" cover all corner cases, but it should be enough to get you started.
This gives a slightly different result than your expected one, because you have the weekdays wrong for 2021-03-02 and 2021-03-11.
Online example

Calculate due date and exclude working/business days SQL

I would like to calculate a due date.
If the start date is 30/12/2020, I need to count 20 working days from start date and display the date for the 20th working date from start date.
For example if the start date is 30/12/2020 must give me 28/01/2021 (excludes saturdays and sundays and finds the 20th working day from 30/12/2020).
But I am unable to exclude the weekends.
SELECT
DATEADD(DAY,20,CAST(CAST('2020-12-30' AS DATE) AS DATETIME))
-(CASE WHEN DATENAME(dw,'2020-12-30') = 'Sunday' THEN 1 ELSE 0 END)
-(CASE WHEN DATENAME(dw,'2020-12-30') = 'Saturday' THEN 1 ELSE 0 END) AS DueDate
thanks
Your best pick would be to build a calendar table, with a boolean flag that indicates whether each day is a working day or not. Then you would just do something like:
select dt
from calendar
where dt >= '20201231' and is_working_day = 1
order by dt
offset 19 rows fetch next 1 row only
The way your question is asked, however, one option enumerates the days in a subquery until the count of working days is reached:
declare #dt date = '20201231';
declare #no_days int = 20;
with cte as (
select #dt as dt, #no_days as no_days
union all
select dateadd(day, 1, dt),
case when datename(weekday, dt) in ('Saturday', 'Sunday')
then no_days
else no_days - 1
end
from cte
where no_days > 1
)
select max(dt) as res from cte
If you don't care about holidays, then the 20th working day is exactly 28 days later. So you can use:
dateadd(day, 28, '2020-12-30')
Note: This assumes that the starting date is a working day.

Group by date intervall Oracle

I want to retrieve sum of weight data from a table over a whole month.
what I need help with is that I want to group the result into 2 parts
sum of 1-15 of the month
and second line 16-31 of the month.
SELECT(SUM(B.SCALE_WEIGHT) FROM TRACKING.DATALOG_TAB B WHERE B.MATERIALID= 1 AND B.SCALE_EVENTDATE BETWEEN TO_DATE(TRUNC(TO_DATE('2020-10-1', 'YYYY-MM-DD'),'MONTH')) AND TO_DATE(TRUNC(TO_DATE('2020-10-1', 'YYYY-MM-DD'), 'MONTH')+30)
GROUP BY(somthing like this - 1-15 and 16-31)
Here is one option:
select
1 + floor(extract(day from scale_eventdate) / 16) as fortnight,
sum(b.scale_weight) as sum_scale_weight
from tracking.datalog_tab b
where
materialid = 1
and scale_eventdate >= date '2020-10-01'
and scale_eventdate < date '2020-11-01'
group by 1 + floor(extract(day from scale_eventdate) / 16)
This extracts the day number from the date, and then use artithmetics: every day from the 1 to to the 15th of the month included goes to fortnight number 1, and everything afterwards goes to bucket 2.
We could also do this with to_char() and a case expression, which is somewhat more expressive:
select
case when to_char(scale_eventdate, 'dd') <= '15' then 1 else 2 end as fortnight,
sum(b.scale_weight) as sum_scale_weight
from tracking.datalog_tab b
where
materialid = 1
and scale_eventdate >= date '2020-10-01'
and scale_eventdate < date '2020-11-01'
group by case when to_char(scale_eventdate, 'dd') <= '15' then 1 else 2 end
Note that I changed the date filtering logic to use standard date literals, which makes the query shorter and more readable.

Customizing the range of a week with date_trunc

I've been trying for hours now to write a date_trunc statement to be used in a group by where my week starts on a Friday and ends the following Thursday.
So something like
SELECT
DATE_TRUNC(...) sales_week,
SUM(sales) sales
FROM table
GROUP BY 1
ORDER BY 1 DESC
Which would return the results for the last complete week (by those standards) as 09-13-2019.
You can subtract 4 days and then add 4 days:
SELECT DATE_TRUNC(<whatever> - INTERVAL '4 DAY') + INTERVAL '4 DAY' as sales_week,
SUM(sales) as sales
FROM table
GROUP BY 1
ORDER BY 1 DESC
The expression
select current_date - cast(cast(7 - (5 - extract(dow from current_date)) as text) || ' days' as interval);
should always give you the previous Friday's date.
if by any chance you might have gaps in data (maybe more granular breakdowns vs just per week), you can generate a set of custom weeks and left join to that:
drop table if exists sales_weeks;
create table sales_weeks as
with
dates as (
select generate_series('2019-01-01'::date,current_date,interval '1 day')::date as date
)
,week_ids as (
select
date
,sum(case when extract('dow' from date)=5 then 1 else 0 end) over (order by date) as week_id
from dates
)
select
week_id
,min(date) as week_start_date
,max(date) as week_end_date
from week_ids
group by 1
order by 1
;