0

This is my current implementation

SELECT

    date_trunc('month', do_date::date)::date as starting_of_the_month,

    (date_trunc('month', do_date::date) + interval '1 month' - interval '1 day')::date as ending_of_the_month,

    case when 1 + FLOOR((EXTRACT(DAY FROM do_date) - 1) / 7) = 1
        THEN date_trunc('week', do_date)::date || ' - ' ||
                  (date_trunc('week', do_date) + '6 days') ::date end as week1,
    case when 1 + FLOOR((EXTRACT(DAY FROM do_date) - 1) / 7) = 2
             THEN date_trunc('week', do_date)::date || ' - ' ||
                  (date_trunc('week', do_date) + '6 days') ::date end as week2,
    case when 1 + FLOOR((EXTRACT(DAY FROM do_date) - 1) / 7) = 3
             THEN date_trunc('week', do_date)::date || ' - ' ||
                  (date_trunc('week', do_date) + '6 days') ::date end as week3,
    case when 1 + FLOOR((EXTRACT(DAY FROM do_date) - 1) / 7) = 4
             THEN date_trunc('week', do_date)::date || ' - ' ||
                  (date_trunc('week', do_date) + '6 days') ::date end as week4,
    case when 1 + FLOOR((EXTRACT(DAY FROM do_date) - 1) / 7) = 5
             THEN date_trunc('week', do_date)::date || ' - ' ||
                  (date_trunc('week', do_date) + '6 days') ::date end as week5

FROM sales_dos

WHERE date_trunc('month', do_date::date)::date >= '2021-02-01' AND date_trunc('month', do_date::date)::date <  '2021-02-28'

This is my output for now :

enter image description here

I want the output to display as below :

Week 1 : 2021-02-01 - 2021-02-07

Week 2 : 2021-02-08 - 2021-02-14

Week 3 : 2021-02-15 - 2021-02-21

Week 4 : 2021-02-22 - 2021-02-28

Week 5 : -

macDaemon
  • 23
  • 1
  • 4

3 Answers3

1

So what you are looking for is a hybrid ISO with standard Calendar. You are taking the ISO week starting and ending period, but instead of all weeks being exactly 7 days you potentially truncate the 1st and/or last weeks.
The change to need for this is not actually extensive. For initial query returns the in the ISO week begin date instead of the 1st of the month. Then the main query then checks for week 1 and if so produces the 1st of the month. The only twist is determining the ISO week begin date. For this I've just included a function I have had for some time specifically for that. The change to the week_days function are marked --<<<.

create or replace function iso_first_of_week(date_in date)
 returns date
 language sql
 immutable strict
 /* 
    Given a date return the 1st day of the week according to ISO-8601. 
    I.e. Return the Date if it is Monday otherwise return the preceding Monday
*/ 
AS $$
   with wk_adj(l_days) as (values  (array[0,1,2,3,4,5,6]))
   select date_in - l_days[ extract (isodow from date_in)::integer ]
     from wk_adj;
$$;

create or replace 
function week_dates( do_date_in date)     
 returns table (week_num integer, first_date date, last_date date)
 language sql 
 immutable strict
as $$
    with recursive date_list(week_num,first_date,terminate_date) as 
         ( select 1
                , iso_first_of_week(do_date_in)::timestamp   --<<<
                , (date_trunc('month', do_date_in) + interval '1 month' - interval '1 day')::timestamp
           union all 
           select week_num+1, (first_date+interval '7 day'), terminate_date
             from date_list
            where first_date+interval '6 day'  < terminate_date::timestamp    
         ) 
    select week_num
         , case when week_num = 1                            --<<<
                then date_trunc('month', do_date_in)::date   --<<<
                else first_date::date                        --<<<
           end                                               --<<<
         , case when (first_date+interval '6 day')::date > terminate_date 
                then terminate_date::date 
                else (first_date+interval '6 day')::date 
           end last_date
    from date_list;
$$; 

---------- Original Reply

You can use a recursive query CTE to get the week number and first date for each week of the month specified. The main query calculates the ending date, shorting the last if necessary. Then wrap that into a SQL function to return the week number and date range for each week. See example.

create or replace 
function week_dates( do_date_in date) 
 returns table (ween_num integer, first_date date, last_date date)
 language sql 
 immutable strict
as $$
    with recursive date_list(week_num,first_date,terminate_date) as 
         ( select 1
                , date_trunc('month', do_date_in)::timestamp
                , (date_trunc('month', do_date_in) + interval '1 month' - interval '1 day')::timestamp
           union all 
           select week_num+1, (first_date+interval '7 day'), terminate_date
             from date_list
            where first_date+interval '6 day'  < terminate_date::timestamp    
         ) 
    select week_num
         , first_date::date
         , case when (first_date+interval '6 day')::date > terminate_date 
                then terminate_date::date 
                else (first_date+interval '6 day')::date 
           end last_date
    from date_list;
$$; 
 

Response to: "How can i put the output in a single row with week1, week2, week3, week4 and week5". This is essentially the initial output that did not satisfy what you wanted. The term for this type action is PIVOT and is generally understood. It stems from transforming row orientation to column orientation. It is not overly difficult but it is messy.
IMHO this is something that belongs in the presentation layer and is not suitable for SQL. After all you are rearranging the data structure for presentation purposes. Let the database server use its natural format, use the presentation layer to reformat. This allows reuse of the queries instead of rewriting when the presentation is changed or another view of the same data is required.

If you actually want this then just use your initial query, or see the answer from @Bohemian. However the below shows how this issue can be handled with just SQL (assuming the function week_dates was created).

select week1s  
     , case when week5e is null 
            then week4e
            else week5e
       end "end of month"
     , week1s  || ' -  ' || week1e  
     , week2s  || ' -  ' || week2e  
     , week3s  || ' -  ' || week3e  
     , week4s  || ' -  ' || week4e  
     , week5s  || ' -  ' || week5e  
  from ( select  max(case when (week_num=1) then first_date else NULL end) as week1s 
              ,  max(case when (week_num=1) then last_date  else NULL end) as week1e         
              ,  max(case when (week_num=2) then first_date else NULL end) as week2s
              ,  max(case when (week_num=2) then last_date  else NULL end) as week2e      
              ,  max(case when (week_num=3) then first_date else NULL end) as week3s
              ,  max(case when (week_num=3) then last_date  else NULL end) as week3e      
              ,  max(case when (week_num=4) then first_date else NULL end) as week4s
              ,  max(case when (week_num=4) then last_date  else NULL end) as week4e      
              ,  max(case when (week_num=5) then first_date else NULL end) as week5s
              ,  max(case when (week_num=5) then last_date  else NULL end) as week5e      
           from week_dates(current_date)
        ) w ; 

As before I have wrapped the above in a SQL function and provide an example here.

Belayer
  • 13,578
  • 2
  • 11
  • 22
  • See revises answer and/or the answer by @Bohemian. – Belayer Mar 01 '21 at 20:08
  • This query outputs incorrect week range when tested for April 2021 eg: ('2021-04-23') – macDaemon Mar 09 '21 at 05:05
  • The you need to specify what you expect the result to be, also properly define what you mean by week. Deducing that from your sample data is seems Week 1 of the month begins on the 1st of the month and is 7 days long with each week following the same except the last week which ends on the last day of the month. This is exactly what the query produces for Apr-2021. – Belayer Mar 09 '21 at 18:05
  • Sorry for not specifying the question more detail, yeah actually i need the beginning of the week for a month to start from the starting date of the month and ends at Sunday. Like in April 2021, week 1 should be (01-04-2021 to 04-04-2021), week 2 (05-04-2021 to 11-04-2021) and week 5 (26-04-2021 to 30-04-2021) – macDaemon Mar 11 '21 at 02:23
  • See revised answer. – Belayer Mar 11 '21 at 05:13
1

Here is another way to do it (example for January 2021).

with 
t as (select date_trunc('month', '2021-03-11'::date) as aday), -- any date in Jan-2021
s as
(
 select d::date, d::date + 6 ed, extract('isodow' from d) wd
 from t, generate_series (aday, aday + interval '1 month - 1 day', interval '1 day') d
)
select format ('Week %s', extract(day from d)::integer / 7 + 1) as weekname, d, ed 
from s 
where wd = 1;
Stefanov.sm
  • 11,215
  • 2
  • 21
  • 21
  • this query gives incorrect week when try with March 2019. expected output for Week 1 should be 01-03-2019 to 03-03-2019 but this query give results for week 1 as 04-03-2019 to 10-03-2019. – macDaemon Mar 04 '21 at 04:34
0

I would first simplify to:

extract(day from do_date)::int / 7 + 1 as week_in_month

then pivot on that using crosstab().

Bohemian
  • 412,405
  • 93
  • 575
  • 722