How to increment weeks by adding number - sql

I have a table that contains week number in string and number. I want to sum number with week and get the next week.
for example
tableA
week num
2022-1 1
2022-3 3
output
week num new_week
2022-1 1 2022-2
2022-3 3 2022-6
2022-52 2 2023-2
As a result, I converted the week into the date, added the week to the date, and finally converted the date back to the week. However, when I try to work date to week, I have issues. The SQL below is what I'm using
CONCAT(YEAR(DATEADD('week', num, date)), WEEK(DATEADD('week', num, date)))
I am not using the calendar year. Due to the fact that my week begins on the first Friday of every year, the calculation is incorrect. Would it be possible to avoid the need to convert week into date and date into week?

I wrote a small JS UDF to do your "week" math. It seems if December 31 is Thursday, then that year has 53 weeks. Good thing is, you don't need to convert your "year-week" to dates.
create or replace function addweeks( spcweek VARCHAR, num VARCHAR ) returns VARCHAR
LANGUAGE JAVASCRIPT
AS
$$
year = parseInt(SPCWEEK.substring( 0, 4 ));
week = parseInt(SPCWEEK.substring( 5 ));
week = week + parseInt(NUM);
weekinyear = (new Date(year, 11, 31).getDay() == 4 ? 53 : 52);
while (week > weekinyear ) {
week = week - weekinyear;
weekinyear = (new Date(year, 11, 31).getDay() == 4 ? 53 : 52);
year ++;
}
return year + "-" + week;
$$
;
select myweek, num, addweeks( myweek, num) new_week
from mydata;
+---------+-----+----------+
| MYWEEK | NUM | NEW_WEEK |
+---------+-----+----------+
| 2022-1 | 1 | 2022-2 |
| 2022-3 | 3 | 2022-6 |
| 2022-52 | 2 | 2023-2 |
| 2020-52 | 2 | 2021-1 |
+---------+-----+----------+
I think you can correct my logic if there is an error in calculating the total weeks of the year.

With a bit of string fiddling you could do the calulation like this.
SELECT week, num, CONCAT( SUBSTRING(week FROM 1 for 5), num + SUBSTRING(week FROM INSTR(week, '-')+1))
FROM table;

Related

Get a date from iso week and year in SQL

From iso week and year, I would like to get a date.
The date should be first day of the week.
First day of the week is Monday.
For example iso week 10 and iso year should convert to 2019-03-04.
I am using Snowflake
The date expression to do this is a little complex, but not impossible:
SELECT
DATEADD( /* Calculate start of ISOWeek as offset from Jan 1st */
DAY,
WEEK * 7 - CASE WHEN DAYOFWEEKISO(DATE_FROM_PARTS(YEAR, 1, 1)) < 5 THEN 7 ELSE 0 END
+ 1 - DAYOFWEEKISO(DATE_FROM_PARTS(YEAR, 1, 1)),
DATE_FROM_PARTS(YEAR, 1, 1)
)
FROM (VALUES (2000, 1), (2000, 2), (2001, 1), (2002, 1), (2003, 1)) v(YEAR, WEEK);
Unfortunately, Snowflake doesn't support this functionality natively.
While it's possible to compute manually the date from ISO week and year, it's very complex. So like others suggested, generating a Date Dimension table for this is much easier.
Example of a query that can generate it for the lookups (note - this is not a full Date Dimension table - that is typically one row per day, this is one row per week).
create or replace table iso_week_lookup as
select
date_part(yearofweek_iso, d) year_iso,
date_part(week_iso, d) week_iso,
min(d) first_day
from (
select dateadd(day, row_number() over (order by 1) - 1, '2000-01-03'::date) AS d
from table(generator(rowCount=>10000))
)
group by 1, 2 order by 1,2;
select * from iso_week_lookup limit 2;
----------+----------+------------+
YEAR_ISO | WEEK_ISO | FIRST_DAY |
----------+----------+------------+
2000 | 1 | 2000-01-03 |
2000 | 2 | 2000-01-10 |
----------+----------+------------+
select min(first_day), max(first_day) from iso_week_lookup;
----------------+----------------+
MIN(FIRST_DAY) | MAX(FIRST_DAY) |
----------------+----------------+
2000-01-03 | 2027-05-17 |
----------------+----------------+
select * from iso_week_lookup where year_iso = 2019 and week_iso = 10;
----------+----------+------------+
YEAR_ISO | WEEK_ISO | FIRST_DAY |
----------+----------+------------+
2019 | 10 | 2019-03-04 |
----------+----------+------------+
Note, you can play with the constants in create table to create a table of the range you want. Just remember to use Monday as the starting day, otherwise you'll get a wrong value for the first week in the table :)
If you do not have Date Dimension table and/or utilities, as mentioned in the comments, you should parsing it from a textual form. But it would be DBMS implementation dependent:
In MySQL: STR_TO_DATE(CONCAT(year, ' ', week), '%x %v')
In PostgreSQL: TO_DATE(year || ' ' || week, 'IYYY IW')
(also Oracle DB would be something similar)

Get Recent Quarters Without Dates

I'm tasked with pulling the data for the four recent quarters. If I was dealing with dates this would be easy, but I'm not sure how to do so when I have a quarters table that looks like this:
| quarter | year |
+---------+------+
| 1 | 2016 |
| 2 | 2016 |
| 3 | 2016 |
...
I know that I can get the current quarter by doing something like this:
SELECT *
FROM quarters
WHERE quarter = (EXTRACT(QUARTER FROM CURRENT_DATE))
AND year = (EXTRACT(YEAR FROM CURRENT_DATE));
However, I'm not sure the best way to get the four most recent quarters. I thought about getting this quarter from last year, and selecting everything since then, but I don't know how to do that with tuples like this. My expected results would be:
| quarter | year |
+---------+------+
| 1 | 2017 |
| 2 | 2017 |
| 3 | 2017 |
| 4 | 2017 |
Keep in mind they won't always be the same year - in Q12018 this will change.
I've built a SQLFiddle that can be used to tinker with this - http://sqlfiddle.com/#!17/0561a/1
Here is one method:
select quarter, year
from quarters
order by year desc, quarter desc
fetch first 4 rows only;
This assumes that the quarters table only has quarters with data in it (as your sample data suggests). If the table has future quarters as well, then you need to compare the values to the current date:
select quarter, year
from quarters
where year < extract(year from current_date) or
(year = extract(year from current_date) and
quarter <= extract(quarter from current_date)
)
order by year desc, quarter desc
fetch first 4 rows only;
For the case that there can be gaps, like 2/2017 missing, and one would then want to return only three quarters instead of four, one can turn years and quarters into consecutive numbers by multiplying the year by four and adding the quarters.
select *
from quarters
where year * 4 + quarter
between extract(year from current_date) * 4 + extract(quarter from current_date) - 3
and extract(year from current_date) * 4 + extract(quarter from current_date)
order by year desc, quarter desc;

hive date dim - customize week number

My requirement is to populate week number against calendar date.The catch is week number will start from October 1 and end at December 7.
So week commencing October 1 will be treated as week 1 , 7th October as week 2 and so on last week number will populate against December 7. Rest will have week number column as NULL. How to do it in hive ?
with t as (select date '2014-10-23' as dt)
select case
when dt between cast(concat(date_format(dt,'yyyy'),'-10-01') as date)
and cast(concat(date_format(dt,'yyyy'),'-12-07') as date)
then datediff (dt,cast(concat(date_format(dt,'yyyy'),'-10-01') as date)) div 7 + 1
end as week_number
from t
+-------------+
| week_number |
+-------------+
| 4 |
+-------------+

MariaDB start of week date & week number 1 to 52

I've written a stored procedure to get the week from a date, it also returns the date at the start of the week as well as the week number and year.
I'm aware of the 'WEEK' function, however this doesn't give me the date at the start of the week and I'm not aware of a function that does this given the week and year.
Question is:
How can I get the 'date' at the start of the week given the week number? Where the start of the week is passed in as a day index, 0 = Sunday, 1 = Monday etc.
My current function doesn't always work and if the first day of the week is Monday, then Sunday falls into the next week, not the end of the same week as I would like it to be.
I was digging around this for a bit too. But I stumbled on some mysql code that also worked. It basically subtracts days based on the day of the week. i.e. If the date is a Wed (4), you know the date was 1-4=-3 days ago.
How about this:
# with Sunday being the start of the week:
select convert(date_add(now(), interval(1-dayofweek(now())) day), date) as WeekStartDate
select convert(date_add(now(), interval(7-dayofweek(now())) day), date) as WeekEndDate
# with Monday being the start of the week:
select convert(date_add(now(), interval(2-dayofweek(now())) day), date) as WeekStartDate
select convert(date_add(now(), interval(8-dayofweek(now())) day), date) as WeekEndDate
Credit:
How do I get the first day of the week of a date in mysql?
Use Sequence engine. You can adapt the following example as needed:
MariaDB [_]> SHOW ENGINES\G
.
.
.
*************************** 3. row ***************************
Engine: SEQUENCE
Support: YES
Comment: Generated tables filled with sequential values
Transactions: YES
XA: NO
Savepoints: YES
.
.
.
MariaDB [_]> SET #`year` := 2016,
-> #`mode` := 1,
-> #`week` := 23;
Query OK, 0 rows affected (0.00 sec)
MariaDB [_]> SELECT
-> `der`.`date`,
-> `der`.`week`,
-> `der`.`year`
-> FROM (
-> SELECT
-> `der`.`date`,
-> WEEK(`der`.`date`, #`mode`) `week`,
-> YEAR(`der`.`date`) `year`
-> FROM (
-> SELECT
-> DATE_ADD(CONCAT(#`year`, '-01-01'), INTERVAL `s`.`seq` DAY) `date`
-> FROM
-> seq_0_to_365 `s`
-> ) `der`
-> ) `der`
-> WHERE
-> `der`.`week` = #`week` AND
-> `der`.`year` = #`year`;
+------------+------+------+
| date | week | year |
+------------+------+------+
| 2016-06-06 | 23 | 2016 |
| 2016-06-07 | 23 | 2016 |
| 2016-06-08 | 23 | 2016 |
| 2016-06-09 | 23 | 2016 |
| 2016-06-10 | 23 | 2016 |
| 2016-06-11 | 23 | 2016 |
| 2016-06-12 | 23 | 2016 |
+------------+------+------+
7 rows in set (0.01 sec)
Solved, I re-wrote the stored procedure:
exitProc:BEGIN
#--
# Procedure:
# weekFromDate
#
# Parameters:
# vcCompKey, the key associated with the company
# dtDate, the date to translate
# dtOutSOW, returned start of week date
# siOutWeek, returned week number
# siOutYear, returned year
#--
DECLARE siDIY SMALLINT; #Day in year
DECLARE siFDOW SMALLINT; #First day of week
DECLARE siGoBack SMALLINT; #Flag used to check for last year
DECLARE siRmonth SMALLINT; #Reference Month
DECLARE siRyear SMALLINT; #Reference Year
DECLARE dtSOY DATE; #Date of start of year
DECLARE vcFMDOY VARCHAR(12);#First month and day of year
DECLARE vcFDOW VARCHAR(12);#First day of the week
DECLARE vcDYSOW VARCHAR(80);#Days of week
#Get the first day of the week for the specified company
SET vcFDOW = vcGetParamValue(vcCompKey, 'Var:First day of week');
IF (vcFDOW IS NULL) THEN
#No entry found, abort!
LEAVE exitProc;
END IF;
#Get the first month and day of the year for the specified company
SET vcFMDOY = vcGetParamValue(vcCompKey, 'Var:First day of year');
IF (vcFMDOY IS NULL) THEN
#No entry found, abort!
LEAVE exitProc;
END IF;
#Set-up days of week
SET vcDYSOW = 'Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday';
#Get the first day of the week index base 1
SET siFDOW = FIND_IN_SET(LOWER(vcFDOW), LOWER(vcDYSOW)) - 1;
#Get the reference month and year
SET siRmonth = MONTH(dtDate);
SET siRyear = YEAR(dtDate);
SET dtSOY = DATE(CONCAT(siRyear, '/', vcFMDOY));
#Calculate the start of week date
SET dtOutSOW = DATE_SUB(dtDate, INTERVAL (DAYOFWEEK(dtDate) - siFDOW) DAY) + 1;
#Calculate the day in year
SET siDIY = DATEDIFF(dtOutSOW, dtSOY);
#Do we need to go back to the end of the previous year?
SET siGoBack = YEAR(dtDate) - YEAR(dtOutSOW);
IF siGoBack < 0 Or siDIY < 0 Or dtDate < dtOutSOW THEN
#Yes
IF YEAR(dtOutSOW) = YEAR(dtDate) THEN
SET dtOutSOW = DATE_SUB(dtOutSOW, INTERVAL 7 DAY);
END IF;
SET dtSOY = DATE(CONCAT(YEAR(dtOutSOW), '/', vcFMDOY));
SET siDIY = DATEDIFF(dtOutSOW, dtSOY);
END IF;
#Calculate the week no. and year
SET siOutWeek = (siDIY / 7) + 1;
SET siOutYear = YEAR(dtOutSOW);
END
This routine does make use of other tables in my database and allows for companies to have different start of years.
As a test, I will find the start of the current week, first note:
mysql> SELECT NOW(), WEEK(NOW());
+---------------------+-------------+
| NOW() | WEEK(NOW()) |
+---------------------+-------------+
| 2016-06-18 12:10:58 | 24 |
+---------------------+-------------+
Then this is the meat of the function:
mysql> SELECT '2016-01-01'
+ INTERVAL 7*24
- DAYOFWEEK('2016-01-01')
+ 1 DAY;
+----------------------------------------------------------------+
| '2016-01-01' + INTERVAL 7*24 - DAYOFWEEK('2016-01-01') + 1 DAY |
+----------------------------------------------------------------+
| 2016-06-12 |
+----------------------------------------------------------------+
'2016-01-01' is the beginning of the year in question.
24 is the WEEK() number.
+ 1 DAY is to compensate for start of week.
Something else needs to be done for handling your option of what day the week starts week.
Some correction for user1014010's answer. When week starts with Monday you'll receive date of next week for Sundays. There's my correction:
SELECT DATE(DATE_ADD(NOW(), INTERVAL -((5 + DAYOFWEEK(NOW())) % 7) DAY)) AS WeekStartDate

How can I identify groups of consecutive dates in SQL?

Im trying to write a function which identifies groups of dates, and measures the size of the group.
I've been doing this procedurally in Python until now but I'd like to move it into SQL.
for example, the list
Bill 01/01/2011
Bill 02/01/2011
Bill 03/01/2011
Bill 05/01/2011
Bill 07/01/2011
should be output into a new table as:
Bill 01/01/2011 3
Bill 02/01/2011 3
Bill 03/01/2011 3
Bill 05/01/2011 1
Bill 07/01/2011 1
Ideally this should also be able to account for weekends and public holidays - the dates in my table will aways be Mon-Fri (I think I can solve this by making a new table of working days and numbering them in sequence). Someone at work suggested I try a CTE. Im pretty new to this, so I'd appreciate any guidance anyone could provide! Thanks.
You can do this with a clever application of window functions. Consider the following:
select name, date, row_number() over (partition by name order by date)
from t
This adds a row number, which in your example would simply be 1, 2, 3, 4, 5. Now, take the difference from the date, and you have a constant value for the group.
select name, date,
dateadd(d, - row_number() over (partition by name order by date), date) as val
from t
Finally, you want the number of groups in sequence. I would also add a group identifier (for instance, to distinguish between the last two).
select name, date,
count(*) over (partition by name, val) as NumInSeq,
dense_rank() over (partition by name order by val) as SeqID
from (select name, date,
dateadd(d, - row_number() over (partition by name order by date), date) as val
from t
) t
Somehow, I missed the part about weekdays and holidays. This solution does not solve that problem.
The following query account the weekends and holidays. The query has a provision to include the holidays on-the-fly, though for the purpose of making the query clearer, I just materialized the holidays to an actual table.
CREATE TABLE tx
(n varchar(4), d date);
INSERT INTO tx
(n, d)
VALUES
('Bill', '2006-12-29'), -- Friday
-- 2006-12-30 is Saturday
-- 2006-12-31 is Sunday
-- 2007-01-01 is New Year's Holiday
('Bill', '2007-01-02'), -- Tuesday
('Bill', '2007-01-03'), -- Wednesday
('Bill', '2007-01-04'), -- Thursday
('Bill', '2007-01-05'), -- Friday
-- 2007-01-06 is Saturday
-- 2007-01-07 is Sunday
('Bill', '2007-01-08'), -- Monday
('Bill', '2007-01-09'), -- Tuesday
('Bill', '2012-07-09'), -- Monday
('Bill', '2012-07-10'), -- Tuesday
('Bill', '2012-07-11'); -- Wednesday
create table holiday(d date);
insert into holiday(d) values
('2007-01-01');
/* query should return 7 consecutive good
attendance(from December 29 2006 to January 9 2007) */
/* and 3 consecutive attendance from July 7 2012 to July 11 2012. */
Query:
with first_date as
(
-- get the monday of the earliest date
select dateadd( ww, datediff(ww,0,min(d)), 0 ) as first_date
from tx
)
,shifted as
(
select
tx.n, tx.d,
diff = datediff(day, fd.first_date, tx.d)
- (datediff(day, fd.first_date, tx.d)/7 * 2)
from tx
cross join first_date fd
union
select
xxx.n, h.d,
diff = datediff(day, fd.first_date, h.d)
- (datediff(day, fd.first_date, h.d)/7 * 2)
from holiday h
cross join first_date fd
cross join (select distinct n from tx) as xxx
)
,grouped as
(
select *, grp = diff - row_number() over(partition by n order by d)
from shifted
)
select
d, n, dense_rank() over (partition by n order by grp) as nth_streak
,count(*) over (partition by n, grp) as streak
from grouped
where d not in (select d from holiday) -- remove the holidays
Output:
| D | N | NTH_STREAK | STREAK |
-------------------------------------------
| 2006-12-29 | Bill | 1 | 7 |
| 2007-01-02 | Bill | 1 | 7 |
| 2007-01-03 | Bill | 1 | 7 |
| 2007-01-04 | Bill | 1 | 7 |
| 2007-01-05 | Bill | 1 | 7 |
| 2007-01-08 | Bill | 1 | 7 |
| 2007-01-09 | Bill | 1 | 7 |
| 2012-07-09 | Bill | 2 | 3 |
| 2012-07-10 | Bill | 2 | 3 |
| 2012-07-11 | Bill | 2 | 3 |
Live test: http://www.sqlfiddle.com/#!3/815c5/1
The main logic of the query is to shift all the dates two days back. This is done by dividing the date to 7 and multiplying it by two, then subtracting it from the original number. For example, if a given date falls on 15th, this will be computed as 15/7 * 2 == 4; then subtract 4 from the original number, 15 - 4 == 11. 15 will become the 11th day. Likewise the 8th day becomes the 6th day; 8 - (8/7 * 2) == 6.
Weekends are not in attendance(e.g. 6,7,13,14)
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15
Applying the computation to all the weekday numbers will yield these values:
1 2 3 4 5
6 7 8 9 10
11
For holidays, you need to slot them on attendance, so to the consecutive-ness could be easily determined, then just remove them from the final query. The above attendance yields 11 consecutive good attendance.
Query logic's detailed explanation here: http://www.ienablemuch.com/2012/07/monitoring-perfect-attendance.html