Weeks between two dates in Postgres - sql

I want to calculate the number of weeks that a customer has been a customer. The goal is to compare this to the distinct number of weeks the customer has made a purchase to determine what % of weeks the customer made a purchase.
With timestamp customer.created_at what is the best way to find the number of weeks a customer has been a customer? Said differently, the difference in weeks between the current week and the week in which the customer signed up.
Calendar weeks starting Monday to be apples-to-apples with distinct calendar weeks a customer has made a purchase.

AFAIK this should work in all cases
-Your calendar week starts on Monday
-Both dates start at least from 05.01.1970 (Monday)
Postgres:
SELECT
(
extract(epoch from date_trunc('week',now()::date))
-
extract(epoch from date_trunc('week','2019-12-20'::date))
)
/604800
(where 604800 is the number of seconds in a week)

SELECT (EXTRACT(days FROM (now() - customer.created_at)) / 7)::int;
First find the difference between current time and customer's created_at time stamp.
Then you tell Postgres to give you the difference in days and then you can divide that by 7 and round it to an integer. Note that if you do not want an integer, you can cast appropriately.

Try this
select (CURRENT_DATE - customer.created_at)/7

This is the code I used. Works.
SELECT
(
(
(
CURRENT_DATE::DATE - (EXTRACT('dow' FROM CURRENT_DATE::DATE) * INTERVAL '1 day')
)::DATE -
(
customers.created_at::DATE - (EXTRACT('dow' FROM customers.created_at::DATE) * INTERVAL '1 day')
)::DATE
) / 7
) + 1

I think you can have that to get the calendar weeks
select to_char(i::date, 'IYYYIW') AS mdate from generate_series('2017-01-01', current_date, '1 week'::interval) i;
and to count
select count(to_char(i::date, 'IYYYIW')) from generate_series('2017-01-01', current_date, '1 week'::interval) i;
and you can replace the date with customer date

#zach's answer is (perhaps) valid, however, it is hard to follow.
I found these two procedures that abstract equivalent logic:
CREATE OR REPLACE FUNCTION first_of_week(date) returns date AS $$
SELECT ($1::date-(extract('dow' FROM $1::date)*interval '1 day'))::date;
$$ LANGUAGE SQL STABLE STRICT;
CREATE OR REPLACE FUNCTION weeks_in_range(date,date) returns int AS $$
SELECT ((first_of_week($2)-first_of_week($1))/7)+1
$$ LANGUAGE SQL STABLE STRICT;
Source:
https://gist.github.com/tkellen/2003309#file-gistfile1-sql-L17-L23
Example:
SELECT weeks_in_range('1900-01-01',CURRENT_DATE);
weeks_in_range
----------------
5854
(1 row)
Note, that the above example assumes Sunday to be the start of the week. Alternatively, if you want Monday as a start of the week, then use date_trunc, e.g.
CREATE OR REPLACE FUNCTION weeks_in_range(date,date) returns int AS $$
SELECT (((EXTRACT(DAY FROM (date_trunc('week', $2::date) - date_trunc('week', $1::date))))/7)+1)::int
$$ LANGUAGE SQL STABLE STRICT;
first_of_week is unnecessary in this case.

select ((in_FinishDate - in_StartDate) / 7 +
case
when ((date_part('dow', in_StartDate) = 6) and (date_part('dow', in_FinishDate) != 6)) or
((date_part('dow', in_FinishDate) = 0) and (date_part('dow', in_StartDate) != 0)) then 1
when (date_part('dow', in_StartDate) = 5) and (date_part('dow', in_FinishDate) in (1, 2, 3, 4)) then 1
when (date_part('dow', in_StartDate) = 4) and (date_part('dow', in_FinishDate) in (1, 2, 3)) then 1
when (date_part('dow', in_StartDate) = 3) and (date_part('dow', in_FinishDate) in (1, 2)) then 1
when (date_part('dow', in_StartDate) = 2) and (date_part('dow', in_FinishDate) in (1)) then 1
else 0
end)::int;
This should do the trick.

Rebuttal to #maleckicoa. The following demonstrates why one cannot user a constant of 52 weeks in a year for difference between dates spanning the Dec-to-Jan time frame.
Instead of hard coding Dec 2019 - Jan 2020 (which works just fine) the following generates the annual dates 29-Dec to 4-Jan for the period 2019 through 2030. In most the maleckicoa's algorithum works, but ...
with recursive annual_dates(dec_dt, jan_dt) as
( select make_date(2019,12,29), make_date(2020,01,04)
union all
select (dec_dt+interval '1 year')::date, (jan_dt+interval '1 year')::date
from annual_dates
where extract(isoyear from dec_dt)::integer < 2030
) --select * from annual_dates;
select dec_dt, jan_dt,
extract( isoyear from jan_dt)*52 + extract( week from jan_dt)
-
extract( isoyear from dec_dt)*52 - extract( week from dec_dt) "Weeks Between"
from annual_dates;
with recursive annual_dates(dec_dt, jan_dt) as
( select make_date(2019,12,29), make_date(2020,01,04)
union all
select (dec_dt+interval '1 year')::date, (jan_dt+interval '1 year')::date
from annual_dates
where extract(isoyear from dec_dt)::integer < 2030
) --select * from annual_dates;
select 'Week for ' || dec_dt || ' is ' || extract( week from dec_dt) || ' And '
'Week for ' || jan_dt || ' is ' || extract( week from jan_dt) || ' Giving Difference of '
|| extract( isoyear from jan_dt)*52 + extract( week from jan_dt)
-
extract( isoyear from dec_dt)*52 - extract( week from dec_dt) || ' Weeks Between'
from annual_dates;

This should work in Postgres for most cases (Week starts with Monday, the year has 52 weeks)
Example for year end 2019:
select
extract( isoyear from '2020-01-04'::date)*52 + extract( week from '2020-01-04'::date)
-
extract( isoyear from '2019-12-29'::date)*52 - extract( week from '2019-12-29'::date)

Related

Is there a way to use SYSDATE with a weekly date?

So I've been trying to fetch some daily data with SYSDATE on a date type YYYYMMDD as following:
SELECT dates, trunc(calendar_date, 'DD') calendar_dates, weekday_nbr
FROM db.date
WHERE dates BETWEEN to_char(TRUNC(SYSDATE)-2, 'YYYYMMDD') AND to_char(TRUNC(SYSDATE)-1, 'YYYYMMDD')
But now I'm trying to use the same but on a YYYY+MM+Week date type with not much success
I tried using:
SELECT T time, period, fiscal_week
FROM db.time
WHERE time BETWEEN to_char(TRUNC(SYSDATE)-2, 'W') AND to_char(TRUNC(SYSDATE)-1,'W')
With time as a 7 digit number, and period and fiscal week as a 2 digit number
Knowing that there's no way I can truncate such date type, how can TRUNC SYSDATE YYYY+MM+Week to get the data on the last 2 weeks?
Also I was thinking about maybe getting the totals from a set day and then dropping all but the last 2 weeks, but on the long run maybe that would be time consuming.
Knowing that there's no way I can truncate such date type, how can TRUNC SYSDATE YYYY+MM+Week to get the data on the last 2 weeks?
Assuming that your fiscal weeks are from Monday-Sunday then you can truncate to the start of the ISO week (which is always Midnight on Monday) and use that for the basis of the comparison:
SELECT *
FROM db.time
WHERE dates >= TRUNC(SYSDATE, 'IW') - INTERVAL '14' DAY
AND dates < TRUNC(SYSDATE, 'IW')
If you have a column that is for weeks then you should still use a DATE data type and add a CHECK constraint (and can use virtual columns to generate the week and the year):
CREATE TABLE time (
dt DATE
CHECK (dt = TRUNC(dt, 'IW')),
year NUMBER(4,0)
GENERATED ALWAYS AS (EXTRACT(YEAR FROM dt)),
month NUMBER(2,0)
GENERATED ALWAYS AS (EXTRACT(MONTH FROM dt)),
week NUMBER(1,0)
GENERATED ALWAYS AS (FLOOR((dt - TRUNC(dt, 'MM'))/7) + 1),
time VARCHAR2(7)
GENERATED ALWAYS AS (
CAST(
TO_CHAR(dt, 'YYYYMM')
|| (FLOOR((dt - TRUNC(dt, 'MM'))/7) + 1)
AS VARCHAR2(7)
)
)
-- ...
);
fiddle
Then you can use the logic above on the date column.
If you do not have a DATE column then you will need to convert your YYYYMMW number into a DATE and then use the logic above.
For example, if the logic for your fiscal weeks (which you have not described) is that the first week of each month starts on the first Monday of the month then you can convert the YYYYMMW number to a DATE using:
SELECT NEXT_DAY(
TO_DATE(SUBSTR(time, 1, 6), 'YYYYMM') - INTERVAL '1' DAY,
'MONDAY'
) + INTERVAL '7' DAY * (SUBSTR(time, 7, 1) - 1) AS week_start
FROM db.time
and then could use it to filter the table using:
SELECT *
FROM (
SELECT t.*,
NEXT_DAY(
TO_DATE(SUBSTR(time, 1, 6), 'YYYYMM') - INTERVAL '1' DAY,
'MONDAY'
) + INTERVAL '7' DAY * (SUBSTR(time, 7, 1) - 1) AS week_start
FROM db.time t
)
WHERE week_start >= TRUNC(SYSDATE, 'IW') - INTERVAL '14' DAY
AND week_start < TRUNC(SYSDATE, 'IW')
If you have different logic for calculating when fiscal weeks start then you will need to apply that logic to the conversion.

Next business day (Monday - Friday) in Oracle?

I need to calculate the next business day (business days being Monday - Friday) in Oracle. At the moment in our reporting program we use a variable it supports <|Tomorrow|> but the problem is that customers do not receive the require details for say a Monday's delivery until a Sunday which is pointless since no one is there.
So we need to calculate Monday on the previous Friday, Tuesday on Monday, Wednesday on Tuesday, Thursday on Wednesday and Friday on Thursday.
What is the best method to do it within directly Oracle since our reporting program does not seem to have a next business day variable. Basically we need to change Where ORDER_HEADER.DELIVERY_DATE = '<|Tomorrow|>' to Where ORDER_HEADER.DELIVERY_DATE = ** next business day **
This is Oracle 10g.
Select ORDER_HEADER.DELIVERY_DATE As "Delivery Date",
ORDER_HEADER.ORDER_NO As "Document No",
ORDER_HEADER.CUSTOMER_ORDER_NO As "Customer Order No",
ORDER_TOTALS.ORDER_TOTAL_QUANTITY As "Total Items",
ORDER_TOTALS.ORDER_TOTAL_NET As "Total Net"
From ORDER_HEADER
Inner Join ORDER_TOTALS On ORDER_HEADER.ORDER_NO = ORDER_TOTALS.ORDER_NO
Where ORDER_HEADER.DELIVERY_DATE = '<|Tomorrow|>' And ORDER_HEADER.CUSTOMER_NO = :Param1
Order By "Document No"
These things are normally done with a table that lists all the working days in a year rather than a naive formula like:
CASE WHEN (1 + TRUNC (SYSDATE) - TRUNC (SYSDATE, 'IW')) < 5
THEN 1 + (1 + TRUNC (SYSDATE) - TRUNC (SYSDATE, 'IW'))
ELSE 1
END --(on monday to thursday this returns 2 to 5, on fri/sat/sun this returns 1)
CASE WHEN (1 + TRUNC (SYSDATE) - TRUNC (SYSDATE, 'IW')) < 5
THEN TRUNC (SYSDATE) + 1
ELSE TRUNC (SYSDATE + 4), 'IW')
END --(on monday to thursday this returns tomorrow's date, on fri/sat/sun it returns next monday's date)
Using a table allows you to factor in things like bank holidays, national holidays, religious festivals, voting days, mourning days, ad hoc days off that may occur for company, or political reasons etc etc. Maintaining the table then becomes an important task. Retrieving the next working day is essentially a case of querying the table for the MIN(date) WHERE date > current_date
A typical method would be:
WHERE ( (to_char(sysdate, 'dy') in ('sun', 'mon', 'tue', 'wed', 'thu') and oh.DELIVERY_DATE = TRUNC(SYSDATE) + 1
) or
(to_char(sysdate, 'dy') in ('sat') and oh.DELIVERY_DATE = TRUNC(SYSDATE) + 2
) or
(to_char(sysdate, 'dy') in ('fri') and oh.DELIVERY_DATE = TRUNC(SYSDATE) + 3
)
)
I strongly agree with the other answer that you should have a calendar table for your business. The next business day is not only about weekends but also about holidays.
If you looking for only week days, then one way is create your own function which returns next business day:
create or replace function calc_date(dt date)
return date as ret date;
begin
SELECT min(dt) into ret from (
SELECT ( dt + 1 ) AS dt FROM DUAL
UNION
SELECT ( dt + 2 ) AS dt FROM DUAL
UNION
SELECT ( dt + 3 ) AS dt FROM DUAL
) t
where TO_CHAR(dt, 'D') not in (1,7);
return ret;
end;
Call
select calc_date('2017-09-08') from dual;

Extract Month in sql and adding 1

If I add 1 to extract(month from date), then does the result become 13 or 1 (January of next year)
I have the below code:
(extract(day from sysdate) >=1 and extract(month from sysdate)=12) and (targstartdate >= to_date(((extract(month from sysdate))|| '-1-' || (extract(year from sysdate)+1)) , 'mm-dd-yyyy') and targstartdate <= to_date(((extract(month from sysdate)+1)|| '-1-' ||(extract(year from sysdate)+1)) , 'mm-dd-yyyy')
You can use MOD
MOD(extract(month from sysdate)+1,12) + 1
If you are trying to get the month number of the next month, flip your logic. Instead of extracting the month number and adding 1, add one month to the date then extract the month. See the difference below. The second adds one month, then extracts the month of 1 (for January).
select extract( month from to_date('12/15/2017','MM/DD/YYYY')) + 1 from dual
union all
select extract (month from ADD_MONTHS(to_date('12/15/2017','MM/DD/YYYY'),1)) from dual;

Defining a business month: first Sunday of a month until first Saturday of next month

I have a table with a timestamp field. In Postgresql, I have to define a 'business month' as between the first Sunday of the current month until first Saturday of the following month (eg. 2014-04-06 until 2014-05-03 will be 'April 2014').
I'm not quite sure where to begin. What I have so far is getting the first Sunday from this forum post (with T-SQL).
--first sunday of this month....
-1=sunday,-2=saturday,-3-friday etc
SELECT
datename(dw,dateadd(dd,-1,DATEADD(wk,
DATEDIFF(wk,0,dateadd(dd,7-datepart(day,getdate()),getdate())), 0))),
dateadd(dd,-1,DATEADD(wk,
DATEDIFF(wk,0,dateadd(dd,7-datepart(day,getdate()),getdate())), 0))
I will probably have to build a calendar table and manually define the months. But I would like to explore other solutions.
SQL Fiddle
The function
create or replace function business_month("month" date)
returns table (first_day date, last_day date) as $$
with first_month_day as (
select date_trunc('month', month)::date as first_day
), first_business_day as (
select
first_day
+ (extract(dow from first_day)::int > 0)::int * 7
- extract(dow from first_day)::int
as first_day
from first_month_day
), first_next_month_day as (
select (first_day + interval '1 month')::date as first_day
from first_month_day
), last_business_day as (
select
first_day
+ 6
- extract(dow from first_day)::int
as last_day
from first_next_month_day
)
select
(select first_day from first_business_day) as first_day,
(select last_day from last_business_day) as last_day
;
$$ language sql stable;
Test it
select
to_char(d, 'YYYY Month') as month,
to_char(first_day, 'YYYY-MM-DD Day') as first_day,
to_char(last_day, 'YYYY-MM-DD Day') as last_day
from
generate_series ('2010-01-01'::date, '2014-12-31', '1 month') g (d)
,
business_month(d::date)
;

How do I determine the last day of the previous month using PostgreSQL?

I need to query a PostgreSQL database to determine records that fall within today's date and the last day of the previous month. In other words, I'd like to retrieve everything that falls between December 31, 2011 and today. This query will be re-used each month, so next month, the query will be based upon the current date and January 31, 2012.
I've seen this option, but I'd prefer to avoid using a function (if possible).
Both solutions include the last day of the previous month and also include all of "today".
For a date column:
SELECT *
FROM tbl
WHERE my_date BETWEEN date_trunc('month', now())::date - 1
AND now()::date
You can subtract plain integer values from a date (but not from a timestamp) to subtract days. This is the simplest and fastest way.
For a timestamp column:
SELECT *
FROM tbl
WHERE my_timestamp >= date_trunc('month', now()) - interval '1 day'
AND my_timestamp < date_trunc('day' , now()) + interval '1 day'
I use the < operator for the second condition to get precise results (read: "before tomorrow").
I do not cast to date in the second query. Instead I add an interval '1 day', to avoid casting back and forth.
Have a look at date / time types and functions in the manual.
For getting date of previous/last month:
SELECT (date_trunc('month', now())::date - 1) as last_month_date
Result: 2012-11-30
For getting number of days of previous/last month:
SELECT DATE_PART('days', date_trunc('month', now())::date - 1) last_month_days
Result: 30
Try this:
SELECT ...
WHERE date_field between (date_trunc('MONTH', now()) - INTERVAL '1 day')::date
and now()::date
...
Try
select current_date - cast((date_part('day', current_date) + 1) as int)
take from http://wiki.postgresql.org/wiki/Date_LastDay, and modified to return just the days in a month
CREATE OR REPLACE FUNCTION calc_days_in_month(date)
RETURNS double precision AS
$$
SELECT EXTRACT(DAY FROM (date_trunc('MONTH', $1) + INTERVAL '1 MONTH - 1 day')::date);
$$ LANGUAGE 'sql' IMMUTABLE STRICT;
select calc_days_in_month('1999-05-01')
returns 31
Reference is taken from this blog:
You can use below function:
CREATE OR REPLACE FUNCTION fn_GetLastDayOfMonth(DATE)
RETURNS DATE AS
$$
SELECT (date_trunc('MONTH', $1) + INTERVAL '1 MONTH - 1 day')::DATE;
$$ LANGUAGE 'sql'
IMMUTABLE STRICT;
Sample executions:
SELECT *FROM fn_GetLastDayOfMonth(NOW()::DATE);