## Wednesday, February 28, 2024

### Calculating the difference between two dates in SQL with the result in days

I have written before about how to calculate the difference between two dates using SQL. The returned result is given in years, months, and days. But there are some occasions I need the result in days. For example I need a list of customers who have not paid in over 90 days.

Let me jump into my first example. Here I used a DDS file to contain a "date" field, like many ERP databases the date is held in a numeric field:

 ```01 A R TESTFILER 02 A CUSTNBR 10A 03 A ENTERDTE 8P 0 ```

To calculate how many days difference there is from the date I wrote this post, I first need to convert the field ENTERDTE from a packed numeric to a "true" date, and then I can perform the calculation using it. I am showing a very simple method of doing this. You might find this will not work in your situation. If any of the numeric fields in the file contain a numeric value that is not a valid date, zero for example, then this example would fail with an error.

 ```01 SELECT CUSTNBR,ENTERDTE, 02 DATE(TIMESTAMP_FORMAT(CHAR(ENTERDTE),'YYYYMMDD')) as "Date", 03 TO_CHAR(CURRENT_DATE - DATE(TIMESTAMP_FORMAT(CHAR(ENTERDTE), 'YYYYMMDD')),'9G99G99') 04 AS "Diff 1", 05 DAYS(CURRENT_DATE) - DAYS(DATE(TIMESTAMP_FORMAT(CHAR(ENTERDTE), 'YYYYMMDD'))) 06 AS "Diff 2" 07 FROM TESTFILE ```

Line 1: The first two columns in the results will be the two columns from the file.

Line 2: Here I am converting the value in ENTERDTE to be a "true" date. First I need to convert it to a timestamp, using the TIMESTAMP_FORMAT function. And then use the DATE function to extract the date part from the generated timestamp.

Line 3: Basically I am taking the date calculated on line 2 and subtracting the current date from it. I am using the TO_CHAR function to place commas within the returned number.

Line 4: I am spoiling the surprise of what the results of this statement explaining what this line does. To calculate the difference between the current date and ENTERDTE I need to convert both to the number of days, using the DAYS function. Then I can subtract the two to get the number of days difference.

When I execute this statement the results are:

 ```CUSTNBR ENTERDTE Date Diff 1 Diff 2 ------- -------- ---------- ------ ------ A925 20230613 2023-06-13 7,08 214 A113 20230523 2023-05-23 7,29 235 ```

The column "Date" shows that the numeric versions of the "date" was converted to a "true" date.

In the column "Diff 1" I inserted the comma to separate the number of months from the number of days to makes number easier to understand.

The "Diff 2" column contains the difference in days only.

For my next example I decided to use a SQL DDL Table. The Table contains the same information as the DDS file, the only difference is that the "date" is now a timestamp:

 ```01 CREATE TABLE MYLIB.TESTTABLE 02 (CUSTOMER_CODE CHAR(10) NOT NULL WITH DEFAULT, 03 ENTER_TIMESTAMP TIMESTAMP) ; ```

I am going to use the following statement to show the data contained within:

 ```01 SELECT CUSTOMER_CODE,ENTER_TIMESTAMP, 02 DAYS(ENTER_TIMESTAMP) AS "Days", 03 DAYS(CURRENT_DATE) - DAYS(ENTER_TIMESTAMP) AS "Diff" 04 FROM TESTTABLE ```

Line 1: The two columns in the Table.

Line 2: Here I display the number of days for date in the timestamp column. The number of days is a count of days since December 31, 1 BCE.

Line 3: Calculate the difference from today to the date contained within the timestamp.

The results are:

 ```CUSTNBR ENTER_TIMESTAMP Days Diff ------- -------------------------- ------ ---- A751 2023-11-21 09:13:07.000000 738845 53 A108 2024-01-12 11:08:58.000000 738897 1 ```

Having shown just SQL statements, let me show how this can be used in a RPG program. In the below example I am checking if any of the rows above are older than 30 days:

 ```01 **free 02 dcl-ds Data qualified dim(*auto : 9999) ; 03 Customer char(10) ; 04 Entered timestamp ; 05 DaysLate int(5) ; 06 end-ds ; 07 dcl-s Rows int(5) inz(%elem(Data : *max)) ; 08 exec sql DECLARE C0 CURSOR FOR 09 SELECT CUSTOMER_CODE,ENTER_TIMESTAMP, 10 DAYS(CURRENT_DATE) - DAYS(ENTER_TIMESTAMP) 11 FROM TESTTABLE 12 WHERE (DAYS(CURRENT_DATE) - DAYS(ENTER_TIMESTAMP)) 13 > 30 14 FOR READ ONLY ; 15 exec sql OPEN C0 ; 16 exec sql FETCH C0 FOR :Rows ROWS INTO :Data ; 17 exec sql GET DIAGNOSTICS :Rows = ROW_COUNT ; 18 exec sql CLOSE C0 ; 19 dsply ('Rows fetched = ' + %char(Rows)) ; 20 *inlr = *on ; ```

Line 1: In 2024 everyone should be writing totally free RPG.

Lines 2 – 6: This is an auto-expanding data structure array. It can contain up to a maximum of 9,999 elements.

Lines 3 – 5: The data structure array contains the following subfields: customer code, timestamp, and the number of days difference between the current date and the timestamp.

Line 7: As I am going to perform a multi row fetch I need to give the number of rows to return from the FETCH statement. The code within the INZ keyword will be the maximum number of elements the array Data can have, which is 9,999.

Lines 8 – 14: The declaration for my cursor.

Lines 9 – 11: Is the same as my previous SQL Select statement.

Line 12 – 13: I have the Where clause only selecting rows where the difference between today and the date in the timestamp is greater than 30 days.

Line 15: Open the cursor.

Line 16: I fetch up to number of rows in the variable Rows from the cursor into the data structure array.

Line 17: I want to know how many rows were fetched. I use the GET DIAGNOSTICS to retrieve that using the ROW_COUNT keyword.

Line 18: Close the cursor.

Line 19: Use the Display operation code, DSPLY, to show how many rows were fetch.

Having compiled the program I also added a debug breakpoint at line 20. When I called the program the following was displayed:

 ```DSPLY Rows fetched = 1 ```

When the program encountered the debug breakpoint I could display the contents of the first, and only element, of the array:

 ```> EVAL data DATA.CUSTOMER(1) = 'A751 ' DATA.ENTERED(1) = '2023-11-21-09.13.07.000000' DATA.DAYSLATE(1) = 53 ```

If I had to report on all accounts that were overdue I would build a SQL View, calculating any additional columns of information that would make using the View simple. For example:

• Date extracted from the timestamp
• Days difference between today and the date extracted from the timestamp
• Is the account 30 days late?
• Is the account 60 days late?
• Is the account 90 days late?
• Is the account 120 days late?

With those in mind my View would look like:

 ```01 CREATE OR REPLACE VIEW MYLIB.TESTVIEW 02 (CUSTOMER_CODE,ENTER_TIMESTAMP,ENTER_DATE, 03 DAYS_DIFFERENCE,LATE_30_DAYS,LATE_60_DAYS,LATE_90_DAYS,LATE_120_DAYS) AS 04 (SELECT CUSTOMER_CODE,ENTER_TIMESTAMP,DATE(ENTER_TIMESTAMP), 05 DAYS(CURRENT_DATE) - DAYS(DATE(ENTER_TIMESTAMP)), 06 CASE WHEN (DAYS(CURRENT_DATE) - DAYS(DATE(ENTER_TIMESTAMP))) > 30 07 THEN BOOLEAN(TRUE) 08 END, 09 CASE WHEN (DAYS(CURRENT_DATE) - DAYS(DATE(ENTER_TIMESTAMP))) > 60 10 THEN BOOLEAN(TRUE) 11 END, 12 CASE WHEN (DAYS(CURRENT_DATE) - DAYS(DATE(ENTER_TIMESTAMP))) > 90 13 THEN BOOLEAN(TRUE) 14 END, 15 CASE WHEN (DAYS(CURRENT_DATE) - DAYS(DATE(ENTER_TIMESTAMP))) > 120 16 THEN BOOLEAN(TRUE) 17 END 18 FROM TESTTABLE) ```

Line 1: I always have my Views as CREATE OR REPLACE.

Lines 2 and 3: Names of the columns in the View.

Lines 4 – 18: The SQL statement that builds the View.

Line 4: The first two columns are the columns from the Table. The third is the date extracted from the timestamp.

Line 5: Calculate the difference, in days, between the today and the extracted date.

Lines 6 – 8: I am using a Case statement to check if the difference in days is greater than 30. As I am working in a partition that is IBM i 7.5 if the result of the difference is more than 30 I can return a Boolean value of true.

Lines 9 – 11: Same as above but for 60 days.

Lines 12 – 14: This time for 90 days.

Lines 15 – 17: And finally for 120 days.

If I was working on partition that is running a release that is less than IBM i 7.5 I would need to change lines 7, 10, 13, and 16 as earlier releases do not support the Boolean function:

 ```XX THEN '1' ```

I create the View and then use the following statement to return the results from it:

 ```01 SELECT * FROM TESTVIEW ```

This returns:

 ```CUSTOMER ENTER _CODE ENTER_TIMESTAMP _DATE -------- -------------------------- ---------- A751 2023-11-21 09:13:07.000000 2023-11-21 A108 2024-01-12 11:08:58.000000 2024-01-12 A500 2023-12-21 05:28:22.000000 2023-12-21 A742 2023-11-21 15:15:04.000000 2023-11-21 A017 2023-10-22 15:10:17.000000 2023-10-22 A218 2023-09-22 18:07:04.000000 2023-09-22 DAYS LATE_ LATE_ LATE_ LATE_ DIFFERENCE 30_DAYS 60_DAYS 90_DAYS 120_DAYS ---------- ------- ------- -------- -------- 62 true true 10 32 true 62 true true 92 true true true 122 true true true true ```

In the late days columns if the entered timestamp fits that columns check I see the word "true". If it does not I see a null value.

If I wanted to get a list of all rows where they are 90 or more late I could just do:

 ```01 SELECT CUSTOMER_CODE,DAYS_DIFFERENCE 02 FROM TESTVIEW 03 WHERE LATE_90_DAYS = TRUE ```

As LATE_90_DAYS is a Boolean column I could replace line 3 just with:

 ```03 WHERE LATE_90_DAYS ```

Either way line 3 is written the same results are returned.

 ```CUSTOMER DAYS_ _CODE DIFFERENCE -------- ---------- A017 92 A218 122 ```

If I want to list all the rows where they are not 90 days late I could just use 'NOT TRUE' in my statement:

 ```01 SELECT CUSTOMER_CODE,DAYS_DIFFERENCE 02 FROM TESTVIEW 03 WHERE LATE_90_DAYS NOT TRUE ```

Line 3: This is where I would use the NOT TRUE.

The results are:

 ```CUSTOMER DAYS_ _CODE DIFFERENCE -------- ---------- A751 62 A108 10 A500 32 A742 62 ```

Rather than use the NOT TRUE I could check for null instead.

 ```03 WHERE LATE_90_DAYS IS NULL ```

I know I have answered more than the original premise. I hope you find the other examples useful as it gives you ideas of how you could use the difference between two dates.

This article was written for IBM i 7.5, and should work for some earlier releases too.

1. There is a timestamp diffrence (Timestampdiff) function, I am using here to calculate diffrence in minutes, but it can be used for Days

SQL to examine INQUIRY messages QSYSOPR messages and response times.
SELECT TO_CHAR(A.Msg_Time, 'DD/MM/YY HH24:MI') AS "Msg Time",
To_Char(B.MSG_TIME, 'DD/MM/YY HH24:MI') AS "Rpl Time",
A.FROM_USER,
TimestampDiff(4,CHAR(b.Msg_time - a.Msg_Time)) AS "Diff Min",
A.MSGID,
CAST(A.MSG_TEXT AS CHAR(60) CCSID 37) AS MSG_TEXT,
A.FROM_JOB,
CAST(B.MSG_TEXT AS CHAR(5) CCSID 37) AS RPL
FROM QSYS2.MESSAGE_QUEUE_INFO A
INNER JOIN QSYS2.MESSAGE_QUEUE_INFO B
ON A.MSG_KEY = B.ASSOC_KEY
WHERE A.MSG_TYPE = 'INQUIRY'
AND A.MSGQ_NAME = 'QSYSOPR'
AND A.MSGID NOT IN ('CPA3394', 'CPA404F', 'CPA405C', 'CPA403D')
AND B.MSGQ_NAME = 'QSYSOPR'
ORDER BY B.MSG_TIME DESC

1. I know of TIMESTAMPDIFF, see here.

The problem with TIMESTAMPDIFF is certain assumptions are made within it:
- A year is always 365 days, no leap years.
- All months are 30 days.
This can be a problem.

For example:
VALUES CURRENT_TIMESTAMP - 365 DAYS ;
= '2023-03-05 11:28:06.584604'

VALUES DAYS(CURRENT_DATE) - DAYS('2023-03-05') ;
= 365

VALUES TIMESTAMPDIFF(16,CHAR(CURRENT_TIMESTAMP - '2023-03-05 11:26:06.344631')) ;
= 360

2. TIMESTAMDIFF That's unfortunate would not think it would be rocket science for IBM to fix this (Or is it an SQL standard to be wrong?:)

3. As this is a SQL ISO standard I doubt it ever will be as all the companies who provide SQL will need to make the change.

To prevent "comment spam" all comments are moderated.
```