DB2 - use field as a labeled duration for time calculation - db2

Given a table that looks like this:
id task scheduled_date reminder
-- ----------------------- ---------------- --------
1 mail january newsletter 2022-01-01 15 days
I had planned on executing a query to mimic date addition as in
SELECT TASK, SCHEDULED_DATE + 15 DAYS FROM ...
==> 2022-01-16
Unfortunately, using the REMINDER field gives an error:
SELECT TASK, (SCHEDULED_DATE + REMINDER) FROM ...
==>[Code: -182, SQL State: 42816] [SQL0182] A date, time, or timestamp expression not valid.
Is there any way to accomplish using the reminder field as a labeled duration? (I'm using IBMi DB2)

You'll need to convert the string "15 days" into an actual duration.
A date durration is a decimal(8,0) number representing YYYYMMDD
So 15 days would be 00000015
1 year, 00010000
1 year 1 month, one day '00010101`
create table testdur (
datedur decimal(8,0)
);
insert into testdur
values (15), (10000), (10101), (90), (300);
select current_date as curDate
, dateDur
,current_date + dateDur
from testdur;
Results

There is an attempt to implement the interval function available in Db2 for LUW. It supports string expression as a parameter, not just string constant as the built-in one.
The result of this function can participate in whatever allowed date arithmetic.
This works on Db2 for LUW v11.1+ and Db2 for IBM i v7.5+ at least.
create or replace function interval_d (p_interval varchar (100))
returns dec (8)
contains sql
deterministic
no external action
begin atomic
declare v_sign dec (1) default 0;
declare v_pattern varchar (100) default '([+-]? *[0-9]+) *(\w+)';
declare v_y int default 0;
declare v_m int default 0;
declare v_d int default 0;
declare v_occ int default 1;
declare v_num int;
declare v_kind varchar (10);
l1: while 1=1 DO
set v_kind =
lower
(
regexp_substr
(
p_interval
, v_pattern
, 1, v_occ, '', 2
)
);
if v_kind is null then leave l1; end if;
set v_num =
int
(
replace
(
regexp_substr
(
p_interval
, v_pattern
, 1, v_occ, '', 1
)
, ' ', ''
)
);
if sign (v_num) * v_sign < 0 then
signal sqlstate '75001' set message_text = 'Sign of all operands must be the same';
end if;
if v_sign = 0 then set v_sign = sign (v_num); end if;
if v_kind in ('d', 'day', 'days')
then set v_d = v_d + v_num;
elseif v_kind in ('mon', 'mons', 'month', 'months')
then set v_m = v_m + v_num;
elseif v_kind in ('y', 'year', 'years')
then set v_y = v_y + v_num;
else
signal sqlstate '75000' set message_text = 'wrong duration';
end if;
set v_occ = v_occ + 1;
end while l1;
if abs (v_d) > 99 then
set v_m = v_m + v_d / 30, v_d = mod (v_d, 30);
end if;
if abs (v_m) > 99 then
set v_y = v_y + v_m / 12, v_m = mod (v_m, 12);
end if;
return v_y * 10000 + v_m * 100 + v_d;
end
select interval_d (i) as d
from
(
values
('4 years 2 months 3 days')
, ('3 day 4 year 2 month')
, ('-4y -2mon -3d')
) t (i)
D
40203
40203
-40203
fiddle

Related

How to check the query has values and get the difference of the start date and end date in intervals in postgres SQL?

These are the summary of issue:
There is an issue of not going inside the if clause and data not inserted.
Need to compute the target_progress_date as the difference between the end_date and start_date and all the interval months to be computed and stored in the variable so as to insert into the project_target_progress table.
Viz: Start date :1-1-2021, End date : 1-12-2021 Months: 1-1-2021
1-2-2021 1-3-2021 .... 12-12-2021
// To check if any values are there
$query_check = pg_query(DBCON,"select id , (COALESCE(revised_cost,administrative_sanction_project_cost,award_tender_project_cost) as cost, COALESCE(award_tender_contract_date,administrative_sanction_date,actual_start_date) as start_date, COALESCE(revised_completion_date,scheduled_completion_date)::date) as end_date from sp_index_v4 where id = $proj_id ");
// If it has no null values then execute the logic - Autoset of target progress before editing it.
if ($query_check != null) {
console.log ('Hi'+$query_check);
$query_cost = pg_query(DBCON,"select id ,(COALESCE(revised_cost,administrative_sanction_project_cost,award_tender_project_cost)/ datediff('month',COALESCE(award_tender_contract_date,administrative_sanction_date,actual_start_date)::date, COALESCE(revised_completion_date,scheduled_completion_date)::date)) as cost_of_one_month, TRUNC(((COALESCE(revised_cost,administrative_sanction_project_cost,award_tender_project_cost)/ datediff('month',COALESCE(award_tender_contract_date,administrative_sanction_date,actual_start_date)::date, COALESCE(revised_completion_date,scheduled_completion_date)::date))/ (COALESCE(revised_cost,administrative_sanction_project_cost,award_tender_project_cost)))*100,2) as cost_percent from sp_index_v4 id = $proj_id");
// Insert for new project entry update for already having project
$ins_query = "insert into project_target_progress(project_id,cum_financial_progress_in_cr,cum_financial_progress_in_percent,cum_physical_progress_in_percent,target_progress_date)values('$id',cost_of_one_month,cost_percent,cost_percent,start_date)";
$insert_data = pg_query(DBCON, $ins_query);
}
else {
echo "Value cannot be inserted";
console.log("inside the else block");
}
I wrote my own datediff function with different opportunities:
create or replace function datediff
(
units varchar(30),
start_t timestamp,
end_t timestamp
)
returns int as $$
declare
diff_interval interval;
diff int = 0;
years_diff int = 0;
begin
if units in ('yy', 'yyyy', 'year', 'mm', 'm', 'month') then
years_diff = date_part('year', end_t) - date_part('year', start_t);
if units in ('yy', 'yyyy', 'year') then
return years_diff;
else
return years_diff * 12 + (date_part('month', end_t) - date_part('month', start_t));
end if;
end if;
diff_interval = end_t - start_t;
diff = diff + date_part('day', diff_interval);
if units in ('wk', 'ww', 'week') then
diff = diff/7;
return diff;
end if;
if units in ('dd', 'd', 'day') then
return diff;
end if;
diff = diff * 24 + date_part('hour', diff_interval);
if units in ('hh', 'hour') then
return diff;
end if;
diff = diff * 60 + date_part('minute', diff_interval);
if units in ('mi', 'n', 'minute') then
return diff;
end if;
diff = diff * 60 + date_part('second', diff_interval);
return diff;
end;
$$ language plpgsql;
Now, we can write a sample query using our function:
select
mm.start_date,
mm.end_date,
test.datediff('year', mm.start_date, mm.end_date) as diff_year,
test.datediff('month', mm.start_date, mm.end_date) as diff_month,
test.datediff('day', mm.start_date, mm.end_date) as diff_day
from (
select '2021-11-25 02:20:54.200'::timestamp as start_date, now()::timestamp as end_date
) mm
Result:
start_date
end_date
diff_year
diff_month
diff_day
2021-11-25 02:20:54.200
2022-02-24 06:10:52.258
1
3
91

How do I use a calculated value within a DATEFROMPARTS in SQL

I need a calculated month value within DATEFROMPARTS function. The month has to be seven month prior to CURRENT_TIMESTAMP month.
This is what I tried:
DATEFROMPARTS(Year(CURRENT_TIMESTAMP), Month(CURRENT_TIMESTAMP)-7, 1) as SevenMoAgo;
I will eventually use this in the following expression where '12-01-2018' is:
where RECORDED_SERVICE_STARTTIME > ='12-01-2018'
I later used
declare #CurMo AS INT;
declare #MonPri7 AS INT;
set #CurMo = Month(CURRENT_TIMESTAMP);
set #MonPri7 = (#CurMo -7);
Datefromparts(Year(CURRENT_TIMESTAMP), #MonPri7, 1) as SevenMoAgo;
This also did not work.
I get the following error message:
"Cannot construct data type date, some of the arguments have values which are not valid."
For the second code I get:
Msg 102, Level 15, State 1, Line 8
Incorrect syntax near 'Datefromparts'.
Try this...
SELECT DATEADD(MONTH, DATEDIFF(MONTH, 0, CURRENT_TIMESTAMP) - 7, 0)
Let me explain. First off, we need to understand that SQL Server interprets 0 as 1900-01-01 as shown by the following DATEPART functions.
SELECT DATEPART(YEAR, 0) AS Year
, DATEPART(MONTH, 0) AS Month
, DATEPART(DAY, 0) AS Day;
Which returns...
Year Month Day
----------- ----------- -----------
1900 1 1
Therefore, my SQL could be rewritten as...
SELECT DATEADD(MONTH, DATEDIFF(MONTH, '1900-01-01', CURRENT_TIMESTAMP) - 7, '1900-01-01')
Now perhaps it is a little easier to see what is going on here. The DATEDIFF function returns the number number of months between 1900-01-01 and today (CURRENT_TIMESTAMP) which is 1434.
SELECT DATEADD(MONTH, 1434 - 7, '1900-01-01')
Then we subtract 7 from 1434 which is 1427 and add that many months back to 1900-01-01.
SELECT DATEADD(MONTH, 1427, '1900-01-01')
Which yields 2018-12-01.
The reason is #MonPri7 is equal to ZERO when you say (#CurMo -7)
There are many different ways to calculate it, but if you want to fix your logic, you should use this:
declare #CurMo AS INT;
declare #MonPri7 AS INT;
set #CurMo = Month(CURRENT_TIMESTAMP);
set #MonPri7 = (#CurMo -7);
declare #Y int = Year(CURRENT_TIMESTAMP) -- <-- This is new variable
-- if 7 months ago is ZERO then you should go back to prev year December
if #MonPri7 = 0
begin
set #MonPri7 = 12
set #Y = #Y - 1
end
Edit:
declare #SevenMonthsAgo datetime;
select #SevenMonthsAgo = Datefromparts(#Y, #MonPri7, 1);
SELECT yourfields
FROM yourtable
where RECORDED_SERVICE_STARTTIME > = '01-01-2019' and
RECORDED_SERVICE_STARTTIME > = #SevenMonthsAgo

Find total number in a specific period of time SQL

I am trying to find the total number of members in a given period. Say I have the following data:
member_id start_date end_date
1 9/1/2013 12/31/2013
2 10/1/2013 11/12/2013
3 12/1/2013 12/31/2013
4 5/1/2012 8/5/2013
5 9/1/2013 12/31/2013
6 7/1/2013 12/31/2013
7 6/6/2012 12/5/2013
8 10/1/2013 12/31/2013
9 7/8/2013 12/31/2013
10 1/1/2012 11/5/2013
In SQL I need to create a report that will list out the number of members in each month of the year. In this case something like the following:
Date Members Per Month
Jan-12 1
Feb-12 1
Mar-12 1
Apr-12 1
May-12 2
Jun-12 3
Jul-12 3
Aug-12 3
Sep-12 3
Oct-12 3
Nov-12 3
Dec-12 3
Jan-13 3
Feb-13 3
Mar-13 3
Apr-13 3
May-13 3
Jun-13 3
Jul-13 5
Aug-13 4
Sep-13 6
Oct-13 8
Nov-13 6
Dec-13 6
So there is only 1 member from Jan-12 (member id 10) until May-12 when member id 4 joins making the count 2 and so on.
The date range can be all over so I can't specify the specific dates but it is by month, meaning that even if someone ends 12-1 it is considered active for the month for Dec.
I was able to create the following stored procedure that was able to accomplish what I needed:
USE [ValueBasedSandbox]
GO
/****** Object: StoredProcedure [dbo].[sp_member_count_per_month] Script Date: 01/08/2015 12:02:37 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Create date: 2015-08-01
-- Description: Find the counts per a given date passed in
-- =============================================
CREATE PROCEDURE [dbo].[sp_member_count_per_month]
-- Add the parameters for the stored procedure here
#YEAR int
, #ENDYEAR int
AS
DECLARE #FIRSTDAYMONTH DATETIME
DECLARE #LASTDAYMONTH DATETIME
DECLARE #MONTH INT = 1;
--Drop the temporary holding table if exists
IF OBJECT_ID('tempdb.dbo.##TEMPCOUNTERTABLE', 'U') IS NOT NULL
DROP TABLE dbo.##TEMPCOUNTERTABLE
CREATE TABLE dbo.##TEMPCOUNTERTABLE (
counter INT
, start_date DATETIME2
, end_date DATETIME2
)
--Perform this loop for each year desired
WHILE #YEAR <= #ENDYEAR
BEGIN
--Perform for each month of the year
WHILE (#MONTH <= 12)
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
SET #FIRSTDAYMONTH = DATEADD(MONTH, #MONTH - 1, DATEADD(YEAR, #YEAR-1900, 0))
SET #LASTDAYMONTH = DATEADD(MONTH, #MONTH, DATEADD(YEAR, #YEAR-1900, 0)-1)
INSERT INTO dbo.##TEMPCOUNTERTABLE(counter, start_date, end_date)
SELECT COUNT(*) AS counter
, #FIRSTDAYMONTH AS start_date
, #LASTDAYMONTH AS end_date
FROM dbo.member_table
WHERE start_date <= #LASTDAYMONTH
AND end_date >= #FIRSTDAYMONTH
--Increment through all the months of the year
SET #MONTH = #MONTH + 1
END -- End Monthly Loop
--Reset Month counter
SET #MONTH = 1
--Increment the desired years
SET #YEAR = #YEAR + 1
END -- End Yearly Loop
--Display the results
SELECT *
FROM dbo.##TEMPCOUNTERTABLE
-- Drop the temp table
IF OBJECT_ID('tempdb.dbo.##TEMPCOUNTERTABLE', 'U') IS NOT NULL
DROP TABLE dbo.##TEMPCOUNTERTABLE
GO
This should do the trick
with datesCte(monthStart,monthEnd) as
(
select cast('20120101' as date) as monthStart, cast('20120131' as date) as monthEnd
union all
select DATEADD(MONTH, 1, d.monthStart), dateadd(day, -1, dateadd(month, 1, d.monthStart))
from datesCte as d
where d.monthStart < '20140101'
)
select *
from datesCte as d
cross apply
(
select count(*) as cnt
from dbo.MemberDates as m
where m.startDate <= d.monthEnd and m.endDate > d.monthStart
) as x
order by d.monthStart

Whats an alternative to using a cursor as per this example in t-sql

I’m new to SQL (specifically t-sql and microsoft SQL Server 2008 R2) and had a problem my boss advised me to fix using a cursor. The problem being taking records (equating to shifts entered into a roster) that are over an hour (but divisible by an hour) and effectively splitting them into multiple shift records of an hour each for a report.
Below you can see the section of the query re the cursor logic that I used. My understanding is that cursors are very inefficient and frowned upon – but neither my boss nor myself could identify an alternative solution to this problem.
Can anyone demonstrate a way we could do this without cursors?
Open Curs;
FETCH NEXT FROM Curs INTO #ClientID, #RDNSID, #SvceType, #SDate, #ClientNm, #CHours, #StaffNm, #Package
WHILE (##Fetch_Status = 0)
BEGIN
SET #Hour = 60
SET #Num = #Chours
IF (#Num % 60 = 0)
BEGIN
WHILE (#Num >= 60)
BEGIN
INSERT INTO #ASRTable VALUES (#ClientID, #RDNSID, #SvceType, #SDate, #ClientNm, #Hour, #StaffNm, #Package)
SET #Num = #Num - 60
SET #SDate = DATEADD(HH, 1, #SDate)
END
END
ELSE
BEGIN
SET #Hour = 'INVALID SHIFT'
INSERT INTO #ASRTable VALUES (#ClientID, #RDNSID, #SvceType, #SDate, #ClientNm, #Hour, #StaffNm, #Package)
END
FETCH NEXT FROM Curs INTO #ClientID, #RDNSID, #SvceType, #SDate, #ClientNm, #CHours, #StaffNm, #Package
END
SELECT * FROM #ASRTable
DROP TABLE #ASRTable
CLOSE Curs
DEALLOCATE Curs
Well, you haven't given us sample data or expected results, but I think this follows the same logic:
declare #t table (
SDate datetime not null,
Chours int not null --Curiously, will store a number of minutes?
)
insert into #t (SDate,Chours) values ('2012-12-19T10:30:00',120),('2012-12-18T09:00:00',60),('2012-12-17T08:00:00',90)
;with shifts as (
select SDate,Chours,'60' as Hour from #t where Chours % 60 = 0
union all
select DATEADD(hour,1,SDate),CHours - 60,'60' from shifts where Chours > 0
)
select SDate,Hour from shifts
union all
select SDate,'Invalid Shift' from #t where CHours % 60 <> 0
Result:
SDate Hour
----------------------- -------------
2012-12-19 10:30:00.000 60
2012-12-18 09:00:00.000 60
2012-12-18 10:00:00.000 60
2012-12-19 11:30:00.000 60
2012-12-19 12:30:00.000 60
2012-12-17 08:00:00.000 Invalid Shift
Of course, I don't have all of your other columns, since I have no idea what they're meant to be.

Check for leap year

How do I check if a year is a leap year?
I have this code:
declare #year int
set #year = 1968
SELECT CASE WHEN #YEAR = <LEAPYEAR> THEN 'LEAP YEAR' ELSE 'NORMAL YEAR' END
Expected result:
LEAP YEAR
Check for 29th Feb:
CASE WHEN ISDATE(CAST(#YEAR AS char(4)) + '0229') = 1 THEN 'LEAP YEAR' ELSE 'NORMAL YEAR' END
or use the following rule
CASE WHEN (#YEAR % 4 = 0 AND #YEAR % 100 <> 0) OR #YEAR % 400 = 0 THEN 'LEAP YEAR'...
MOST EFFICIENT LEAP YEAR TEST:
CASE WHEN #YEAR & 3 = 0 AND (#YEAR % 25 <> 0 OR #YEAR & 15 = 0) THEN ...
Adapted from: http://stackoverflow.com/a/11595914/3466415
Leap year calculation:
(#year % 4 = 0) and (#year % 100 != 0) or (#year % 400 = 0)
When this is true, then it is a leap year. Or to put it in case statement
select case when
(
(#year % 4 = 0) and (#year % 100 != 0) or
(#year % 400 = 0)
) then 'LEAP' else 'USUAL' end
;
This could also help
DECLARE #year INT = 2012
SELECT IIF(DAY(EOMONTH(DATEFROMPARTS(#year,2,1))) = 29,1,0)
Result: 1 --(1 if Leap Year, 0 if not)
SELECT IIF(DAY(EOMONTH(DATEFROMPARTS(#year,2,1))) = 29,'Leap year','Not Leap year')
Result: Leap year
Not sure how efficient this is compared to the other solutions. But is another option.
DECLARE #year int = 2016
SELECT CASE
WHEN DATEPART(dayofyear, DATEFROMPARTS(#year, 12, 31)) = 366
THEN 'LEAP'
ELSE 'NOT LEAP'
END
3 line... but could be also 2...
DECLARE #Y as int = 2021;
DECLARE #Dt as char(10) = CAST(#Y as CHAR(4)) + '-02-29'
SELECT IIF(isDATE(#Dt) = 1, 1,0)
or
DECLARE #Dt as char(10) = '2020-02-29'
SELECT IIF(isDATE(#Dt) = 1, 1,0)
Alen
I Have a better solution
CREATE FUNCTION dbo.IsLeapYear(#year INT)
RETURNS BIT AS
BEGIN
DECLARE #d DATETIME,
#ans BIT
SET #d = CONVERT(DATETIME,'31/01/'+CONVERT(VARCHAR(4),#year),103)
IF DATEPART(DAY,DATEADD(MONTH,1,#d))=29 SET #ans=1 ELSE SET #ans=0
RETURN #ans
END
GO
feel free to use
There are different way you can find.
DEMO
DECLARE #year INT = 2024;
-- Date Cast
SELECT #year AS [Year],
CASE WHEN ISDATE(CAST(#year AS CHAR(4)) + '0229') = 1
THEN 'Leap Year'
ELSE 'Not a Leap Year' END
-- Year divisible by 4 but not by 100 OR year divisible by 400
SELECT #year AS [Year],
CASE WHEN (#year % 4 = 0 AND #year % 100 <> 0) OR (#year % 400 = 0)
THEN 'Leap Year'
ELSE 'Not a Leap Year' END
-- Find Month
SELECT #year As [Year],
CASE WHEN MONTH(DATEADD(D, 1, DATEFROMPARTS(#Year, 2, 28))) <> 3
THEN 'Leap Year'
ELSE 'Not a Leap Year' END
-- A Leap Year has 366 days (the extra day is the 29th of February).
SELECT #year As [Year],
CASE WHEN DATEPART(dy,DATEFROMPARTS(#year,12,31)) = 366
THEN 'Leap Year'
ELSE 'Not a Leap Year' END