Convert date to ISO week date - sql

How to convert dates to ISO week date in Impala SQL?
For example 2019-12-30 in the ISO week date calendar would be written as 2020-W01-1 or 2020W011
ANSWER:
Marked Gordon Linoff answer as correct, as it solves the essential part of the question, the deducing of the year part of ISO week date.
For the week part of the ISO week date there is a ready function, and the day part of the ISO week date can be easily converted from Sunday starting week to Monday starting week.
The query below contains all week dates from Monday to Sunday:
select datecol,
concat(cast(iso_year as string),'-W',lpad(cast(iso_week as string),2,'0'),'-',cast(iso_day as string)) as iso_Year_week_date_long,
concat(cast(iso_year as string),'W',lpad(cast(iso_week as string),2,'0'),cast(iso_day as string)) as iso_Year_week_date_short
from (
SELECT datecol,
(case when weekofyear(datecol) = 1 and
date_part('year',datecol) <> date_part('year',adddate(datecol,+7))
then date_part('year',datecol) + 1
when weekofyear(datecol) in (52, 53) and
date_part('year',datecol) <> date_part('year',adddate(datecol,-7))
then date_part('year',datecol) - 1
else date_part('year',datecol)
end) as iso_year,
weekofyear(datecol) as iso_week,
1+mod(dayofweek(datecol)+5,7) as iso_day
from (
select '2021-12-31' as datecol union
select '2020-12-31' as datecol union
select '2019-12-31' as datecol union
select '2018-12-31' as datecol union
select '2017-12-31' as datecol union
select '2016-12-31' as datecol union
select '2015-12-31' as datecol union
select '2014-12-31' as datecol union
select '2013-12-31' as datecol union
select '2012-12-31' as datecol union
select '2022-01-01' as datecol union
select '2021-01-01' as datecol union
select '2020-01-01' as datecol union
select '2019-01-01' as datecol union
select '2018-01-01' as datecol union
select '2017-01-01' as datecol union
select '2016-01-01' as datecol union
select '2015-01-01' as datecol union
select '2014-01-01' as datecol union
select '2013-01-01' as datecol
) as t1
) as t2
order by datecol;
and shows how January 1st belongs to
the new year, if January 1st is 1st, 2nd, 3rd or 4th day of the week, i.e., if there are at least 4 new year days in the week containing January 1st
the old year, if January 1st is 5th, 6th or 7th day of the week, i.e., if there are 3 or less new year days in the week containing January 1st
datecol |iso_year_week_date_long|iso_year_week_date_short|
----------|-----------------------|------------------------|
2014-12-31|2015-W01-3 |2015W013 |
2015-01-01|2015-W01-4 |2015W014 |
2015-12-31|2015-W53-4 |2015W534 |
2016-01-01|2015-W53-5 |2015W535 |
2016-12-31|2016-W52-6 |2016W526 |
2017-01-01|2016-W52-7 |2016W527 |
2017-12-31|2017-W52-7 |2017W527 |
2018-01-01|2018-W01-1 |2018W011 |
2018-12-31|2019-W01-1 |2019W011 |
2019-01-01|2019-W01-2 |2019W012 |
2019-12-31|2020-W01-2 |2020W012 |
2020-01-01|2020-W01-3 |2020W013 |
2020-12-31|2020-W53-4 |2020W534 |
2021-01-01|2020-W53-5 |2020W535 |

I think Impala returns the iso week for date_part() and extract() -- based on your previous question. There is no documentation to this effect.
If so, you can use conditional logic:
select (case when date_part(week, datecol) = 1 and
date_part(year, datecol) <> date_part(year, datecol + interval 1 week)
then date_part(year, datecol) + 1
when date_part(week, datecol) in (52, 53) and
date_part(year, datecol) <> date_part(year, datecol - interval 1 week)
then date_part(year, datecol) - 1
else date_part(year, datecol)
end) as iso_year,
date_part(week, datecol) as iso_week
Otherwise, you can get the first day of the iso year using:
select (case when to_char('DD', date_trunc(year, datecol), 'DD') in ('THU', 'FRI', 'SAT', 'SUN')
then next_day(date_trunc(year, date_trunc(year, datecol)), 'Monday')
else next_day(date_trunc(year, date_trunc(year, datecol)), 'Monday') - interval 7 day
end) as iso_year_start
You can then calculate the iso week from the start of the iso year using arithmetic.

For example 2019-12-30 in the ISO week date calendar would be written as 2020-W01-1 or 2020W011.
We could make use of string format:
select cast(cast('2019-12-30' as date format 'YYYY-MM-DD') as string format 'iyyy-iw-id')
Returns:
"2020-01-01"

Related

Getting a Monthly Date Range

How can you make a date range in a big query? A date range starts from 29th of the month and ends with 28th of the next month. It should be like this
Date | Starting Date | Ending Date
03-13-2020 | 02-29-2020 | 03-28-2021
06-30-2020 | 06-29-2020 | 07-28-2021
01-01-2021 | 12-29-2020 | 01-28-2021
11-11-2021 | 10-28-2021 | 11-29-2021
Actually, i make an article on it.
Check this out:
https://www.theaccountingtactics.com/2021/12/BigQueryBQ-DateProblems-DateSituations-that-are-Hard-to-Analyze-and-Takes-Time-ToCrack%20.html?m=1
Consider below approach
create temp function set_day(date date, day int64) as (
ifnull(
safe.date(extract(year from date), extract(month from date), day),
last_day(date)
)
);
select Date,
set_day(Starting_Date, 29) as Starting_Date,
set_day(Ending_Date, 28) as Ending_Date
from (
select *, if(extract(day from Date) < 29,
struct(date_sub(Date, interval 1 month) as Starting_Date, Date as Ending_Date),
struct(Date as Starting_Date, date_add(Date, interval 1 month) as Ending_Date)
).*
from your_table
)
if applied to sample data as in your question
with your_table as (
select date '2020-03-13' Date union all
select '2021-03-13' union all
select '2020-06-30' union all
select '2021-01-01' union all
select '2021-11-11'
)
output is
You can test whole stuff using below
create temp function set_day(date date, day int64) as (
ifnull(
safe.date(extract(year from date), extract(month from date), day),
last_day(date)
)
);
with your_table as (
select date '2020-03-13' Date union all
select '2021-03-13' union all
select '2020-06-30' union all
select '2021-01-01' union all
select '2021-11-11'
)
select Date,
set_day(Starting_Date, 29) as Starting_Date,
set_day(Ending_Date, 28) as Ending_Date
from (
select *, if(extract(day from Date) < 29,
struct(date_sub(Date, interval 1 month) as Starting_Date, Date as Ending_Date),
struct(Date as Starting_Date, date_add(Date, interval 1 month) as Ending_Date)
).*
from your_table
)

SQL Select only missing months

Notice the 2017-04-01, 2018-02-01, 2018-07-01, and 2019-01-01 months are missing in the output. I want to show only those months which are missing. Does anyone know how to go about this?
Query:
SELECT TO_DATE("Month", 'mon''yy') as dates FROM sample_sheet
group by dates
order by dates asc;
Output:
2017-01-01
2017-02-01
2017-03-01
2017-05-01
2017-06-01
2017-07-01
2017-08-01
2017-09-01
2017-10-01
2017-11-01
2017-12-01
2018-01-01
2018-03-01
2018-04-01
2018-05-01
2018-06-01
2018-08-01
2018-09-01
2018-10-01
2018-11-01
2018-12-01
2019-02-01
2019-03-01
2019-04-01
I don't know Vertica, so I wrote a working proof of concept in Microsoft SQL Server and tried to convert it to Vertica syntax based on the online documentation.
It should look like this:
with
months as (
select 2017 as date_year, 1 as date_month, to_date('2017-01-01', 'YYYY-MM-DD') as first_date, to_date('2017-01-31', 'yyyy-mm-dd') as last_date
union all
select
year(add_months(first_date, 1)) as date_year,
month(add_months(first_date, 1)) as date_month,
add_months(first_date, 1) as first_date,
last_day(add_months(first_date, 1)) as last_date
from months
where first_date < current_date
),
sample_dates (a_date) as (
select to_date('2017-01-15', 'YYYY-MM-DD') union all
select to_date('2017-01-22', 'YYYY-MM-DD') union all
select to_date('2017-02-01', 'YYYY-MM-DD') union all
select to_date('2017-04-15', 'YYYY-MM-DD') union all
select to_date('2017-06-15', 'YYYY-MM-DD')
)
select *
from sample_dates right join months on sample_dates.a_date between first_date and last_date
where sample_dates.a_date is null
Months is a recursive dynamic table that holds all months since 2017-01, with first and last day of the month. sample_dates is just a list of dates to test the logic - you should replace it with your own table.
Once you build that monthly calendar table all you need to do is check your dates against it using an outer query to see what dates are not between any of those periods between first_date and last_date columns.
You can build a TIMESERIES of all dates between the first input date and the last input date (The highest granularity of a TIMESERIES is the day.), and filter out only the months' first days out of that; then left join that created sequence of firsts of month with your input to find out where the join would fail, checking for NULLS from the input branch of the join:
WITH
-- your input
input(mth1st) AS (
SELECT DATE '2017-01-01'
UNION ALL SELECT DATE '2017-02-01'
UNION ALL SELECT DATE '2017-03-01'
UNION ALL SELECT DATE '2017-05-01'
UNION ALL SELECT DATE '2017-06-01'
UNION ALL SELECT DATE '2017-07-01'
UNION ALL SELECT DATE '2017-08-01'
UNION ALL SELECT DATE '2017-09-01'
UNION ALL SELECT DATE '2017-10-01'
UNION ALL SELECT DATE '2017-11-01'
UNION ALL SELECT DATE '2017-12-01'
UNION ALL SELECT DATE '2018-01-01'
UNION ALL SELECT DATE '2018-03-01'
UNION ALL SELECT DATE '2018-04-01'
UNION ALL SELECT DATE '2018-05-01'
UNION ALL SELECT DATE '2018-06-01'
UNION ALL SELECT DATE '2018-08-01'
UNION ALL SELECT DATE '2018-09-01'
UNION ALL SELECT DATE '2018-10-01'
UNION ALL SELECT DATE '2018-11-01'
UNION ALL SELECT DATE '2018-12-01'
UNION ALL SELECT DATE '2019-02-01'
UNION ALL SELECT DATE '2019-03-01'
UNION ALL SELECT DATE '2019-04-01'
)
,
-- need a series of month's firsts
-- TIMESERIES works for INTERVAL DAY TO SECOND
-- so build that timeseries, and filter out
-- the month's firsts
limits(mth1st) AS (
SELECT MIN(mth1st) FROM input
UNION ALL SELECT MAX(mth1st) FROM input
)
,
alldates AS (
SELECT dt::DATE FROM limits
TIMESERIES dt AS '1 day' OVER(ORDER BY mth1st::TIMESTAMP)
)
,
allfirsts(mth1st) AS (
SELECT dt FROM alldates WHERE DAY(dt)=1
)
SELECT
allfirsts.mth1st
FROM allfirsts
LEFT JOIN input USING(mth1st)
WHERE input.mth1st IS NULL;
-- out mth1st
-- out ------------
-- out 2017-04-01
-- out 2018-02-01
-- out 2018-07-01
-- out 2019-01-01

Date SQL to get 31st August

I am trying to get the last 31st August every year dynamically.
E.g if current date is today I would like to get 31st August 2019
next year, and I want this to be dynamic and get 31st August 2020?
I have tried Date_Sub and Date_Trunc and they are not working. Any ideas would be really helpful?
SELECT DATE_SUB(current_date(), INTERVAL 5 DAY) as five_days_ago
Below will always return last /latest August 31st
#standardSQL
SELECT IF(CURRENT_DATE() < last_august_31, DATE_SUB(last_august_31, INTERVAL 1 YEAR), last_august_31) AS last_august_31
FROM UNNEST([DATE(EXTRACT(YEAR FROM CURRENT_DATE()), 8, 31)]) last_august_31
In case if you need to use this within the query with date field - consider below example
#standardSQL
WITH `project.dataset.table` AS (
SELECT DATE '2019-01-01'dt UNION ALL
SELECT '2019-12-31' UNION ALL
SELECT CURRENT_DATE()
)
SELECT dt, IF(dt < last_august_31, DATE_SUB(last_august_31, INTERVAL 1 YEAR), last_august_31) AS last_august_31
FROM `project.dataset.table`,
UNNEST([DATE(EXTRACT(YEAR FROM dt), 8, 31)]) last_august_31
-- ORDER BY dt
with result
Row dt last_august_31
1 2019-01-01 2018-08-31
2 2019-12-31 2019-08-31
3 2020-02-25 2019-08-31
with dates as (
select cast('2019-01-01' as date) as my_date union all select '2019-12-31' union all select current_date()
)
select
my_date,
date(extract(year from my_date) - case when extract(month from my_date) < 9 then 1 else 0 end, 8, 31) as prev_aug_31,
date(extract(year from my_date) + case when extract(month from my_date) >= 9 then 1 else 0 end, 8, 31) as next_aug_31
from dates

Change start day of the week impala

I am trying to change the start day of the week, but not able to achieve
currently its Monday to sunday, it should be made sunday- saturday
I tried using this query on the below dataset invoice date and invoice_week
SELECT invoice_date,
weekofyear(invoice_date) as invoice_week,
datesub(invoice_date,1),
weekofyear(datesub(invoice_date,1)) as invoice_week1
from table
I think instead of subtract the date by 1, you should add the date by 1:
select
invoice_date,
weekofyear(invoice_date) as invoice_week,
adddate(invoice_date,1),
weekofyear(adddate(invoice_date,1)) as invoice_week1
from(
select cast('2018-01-07 16:00:00' as timestamp) invoice_date
)stg
Found a solution which works. Given a date, here's the Date for the start of the week and end of the week if you want a Sunday - Saturday week, not a Monday - Sunday week as is default.
SET var:date=2019-01-06;
select to_date(IF( dayofweek(cast('${var:date}' as timestamp)) = 1, cast('${var:date}' as timestamp), trunc(cast('${var:date}' as timestamp), 'd') - interval 1 day)) as startOfWeek, to_date(IF( dayofweek(cast('${var:date}' as timestamp)) = 1, (trunc(cast('${var:date}' as timestamp) + interval 1 day, 'd') + interval 5 day), (trunc(cast('${var:date}' as timestamp), 'd') + interval 5 day) )) as endOfWeek;
+-------------+------------+
| startofweek | endofweek |
+-------------+------------+
| 2019-01-06 | 2019-01-12 |
+-------------+------------+

Oracle date as fraction of month

I would like to get a table of months between two dates with a fraction of each month that the two dates cover.
For example with a start date of 15/01/2017 and end date of 01/03/2017 it would output:
01/2017 : 0.5483..
02/2017 : 1
03/2017: 0.0322..
where for January and March the calculations are 17/31 and 1/31 respectively. I currently have the query:
WITH dates_between as (SELECT ADD_MONTHS(TRUNC(TO_DATE(:givenStartDate,'dd/mm/yyyy'), 'MON'), ROWNUM - 1) date_out
FROM DUAL
CONNECT BY ADD_MONTHS(TRUNC(TO_DATE(:givenStartDate,'dd/mm/yyyy'), 'MON'), ROWNUM - 1)
<= TRUNC(TO_DATE(:givenEndDate,'dd/mm/yyyy'), 'MON')
)
select * from dates_between
This outputs each month between two dates and formats it to the start of the month. I just need another column to give me the fraction the start and end dates cover. I'm not sure of a way to do this without it getting messy.
The months_between() function "calculates the fractional portion of the result based on a 31-day month". That means that if your range starts or ends in a month that doesn't have 31 days, the fraction you get might not be quite what you expect:
select months_between(date '2017-04-02', date '2017-04-01') as calc from dual
CALC
----------
.0322580645
... which is 1/31, not 1/30. To get 0.0333... instead you'd need to calculate the number of days in each month, at least for the first and last month. This uses a recursive CTE (11gR2+) to get the months, using a couple of date ranges provided by another CTE as a demo to show the difference (you can use a hierarchical query too of course):
with ranges (id, start_date, end_date) as (
select 1, date '2017-01-15', date '2017-03-01' from dual
union all select 2, date '2017-01-31', date '2017-03-01' from dual
union all select 3, date '2017-02-28', date '2017-04-01' from dual
),
months (id, month_start, month_days, range_start, range_end) as (
select id,
trunc(start_date, 'MM'),
extract(day from last_day(start_date)),
start_date,
end_date
from ranges
union all
select id,
month_start + interval '1' month,
extract(day from last_day(month_start + interval '1' month)),
range_start,
range_end
from months
where month_start < range_end
)
select id,
to_char(month_start, 'YYYY-MM-DD') as month_start,
month_days,
case when month_start = trunc(range_start, 'MM')
then month_days - extract(day from range_start) + 1
when month_start = trunc(range_end, 'MM')
then extract(day from range_end)
else month_days end as range_days,
(case when month_start = trunc(range_start, 'MM')
then month_days - extract(day from range_start) + 1
when month_start = trunc(range_end, 'MM')
then extract(day from range_end)
else month_days end) / month_days as fraction
from months
order by id, month_start;
which gets:
ID MONTH_STAR MONTH_DAYS RANGE_DAYS FRACTION
------ ---------- ---------- ---------- --------
1 2017-01-01 31 17 0.5483
1 2017-02-01 28 28 1
1 2017-03-01 31 1 0.0322
2 2017-01-01 31 1 0.0322
2 2017-02-01 28 28 1
2 2017-03-01 31 1 0.0322
3 2017-02-01 28 1 0.0357
3 2017-03-01 31 31 1
3 2017-04-01 30 1 0.0333
The first CTE ranges is just the demo data. The second, recursive, CTE months generates the start and number of days in each month, while keeping track of the original range dates too. The final query just calculates the fractions based on the number of days in the month in the range against the number of days in that month overall.
The month_days and range_days are only shown in the output so you can see what the calculation is based on, you can obviously omit those from your actual result, and format the month start date however you want.
With your original single pair of bind variables the equivalent would be:
with months (month_start, month_days, range_start, range_end) as (
select trunc(to_date(:givenstartdate, 'DD/MM/YYYY'), 'MM'),
extract(day from last_day(to_date(:givenstartdate, 'DD/MM/YYYY'))),
to_date(:givenstartdate, 'DD/MM/YYYY'),
to_date(:givenenddate, 'DD/MM/YYYY')
from dual
union all
select month_start + interval '1' month,
extract(day from last_day(month_start + interval '1' month)),
range_start,
range_end
from months
where month_start < range_end
)
select to_char(month_start, 'MM/YYYY') as month,
(case when month_start = trunc(range_start, 'MM')
then month_days - extract(day from range_start) + 1
when month_start = trunc(range_end, 'MM')
then extract(day from range_end)
else month_days end) / month_days as fraction
from months
order by month_start;
MONTH FRACTION
------- --------
01/2017 0.5483
02/2017 1
03/2017 0.0322
Here's how I would do it (n.b. I have expanded your dates_between to work against multiple rows, purely for demonstration purposes. If you're only working with a single set of parameters, you wouldn't need to do that):
WITH params AS (SELECT 1 ID, '15/01/2017' givenstartdate, '01/03/2017' givenenddate FROM dual UNION ALL
SELECT 2 ID, '15/01/2017' givenstartdate, '23/01/2017' givenenddate FROM dual UNION ALL
SELECT 3 ID, '01/01/2017' givenstartdate, '07/04/2017' givenenddate FROM dual),
dates_between AS (SELECT ID,
to_date(givenstartdate, 'dd/mm/yyyy') givenstartdate,
to_date(givenenddate, 'dd/mm/yyyy') givenenddate,
add_months(trunc(to_date(givenstartdate, 'dd/mm/yyyy'), 'MON'), LEVEL - 1) start_of_month,
last_day(add_months(trunc(to_date(givenstartdate, 'dd/mm/yyyy'), 'MON'), LEVEL - 1)) end_of_month
FROM params
CONNECT BY add_months(trunc(to_date(givenstartdate, 'dd/mm/yyyy'), 'MON'), LEVEL - 1) <=
trunc(to_date(givenenddate, 'dd/mm/yyyy'), 'MON')
AND PRIOR ID = ID
AND PRIOR sys_guid() IS NOT NULL)
SELECT ID,
givenstartdate,
givenenddate,
start_of_month date_out,
end_of_month,
months_between(LEAST(givenenddate, end_of_month) + 1, GREATEST(start_of_month, givenstartdate))
FROM dates_between;
ID GIVENSTARTDATE GIVENENDDATE DATE_OUT END_OF_MONTH DIFF
1 15/01/2017 01/03/2017 01/01/2017 31/01/2017 0.54838709
1 15/01/2017 01/03/2017 01/02/2017 28/02/2017 1
1 15/01/2017 01/03/2017 01/03/2017 31/03/2017 0.03225806
2 15/01/2017 23/01/2017 01/01/2017 31/01/2017 0.29032258
3 01/01/2017 07/04/2017 01/01/2017 31/01/2017 1
3 01/01/2017 07/04/2017 01/02/2017 28/02/2017 1
3 01/01/2017 07/04/2017 01/03/2017 31/03/2017 1
3 01/01/2017 07/04/2017 01/04/2017 30/04/2017 0.22580645
N.B. You may need to add a case statement to decide whether you want to add 1 or not to the diff calculation, based on your requirements.
Try this
For first month, I have calculated remaining days / total days and for last month, I subtracted it by 1 to get days passed / total days.
DBFiddle Demo
WITH tbl AS
(SELECT date '2017-01-15' AS givenStartDate
,date '2017-03-01' AS givenEndDate
FROM dual
)
SELECT ADD_MONTHS(TRUNC(givenStartDate, 'MON'), ROWNUM - 1) AS date_out ,
CASE
WHEN
rownum - 1 = 0
THEN months_between(last_day(givenStartDate), givenStartDate)
WHEN ADD_MONTHS(TRUNC(givenStartDate, 'MON'), ROWNUM - 1) = TRUNC(givenEndDate, 'MON')
THEN 1 - (months_between(last_day(givenEndDate), givenEndDate))
ELSE 1
END AS perc
FROM tbl
CONNECT BY ADD_MONTHS(TRUNC(givenStartDate, 'MON'), ROWNUM - 1)
<= TRUNC(givenEndDate, 'MON');
Output
+-----------+-------------------------------------------+
| DATE_OUT | PERC |
+-----------+-------------------------------------------+
| 01-JAN-17 | .5161290322580645161290322580645161290323 |
| 01-FEB-17 | 1 |
| 01-MAR-17 | .0322580645161290322580645161290322580645 |
+-----------+-------------------------------------------+