In our project we are maintaining a customers weekly spend. For that every week start we are resetting a customer limit.
Whenever customer doing a transaction we are updating his weekly spend by using the below query.
UPDATE SUMMARY
SET WEEKLIMIT = (
SELECT NVL (SUM (AMT / 100), 0)
FROM TRANSACTION
WHERE MOBILENO = :mobileNumber
AND TRUNC(TXNDT) BETWEEN (TRUNC (SYSTIMESTAMP, 'IW') - 1 ) AND TRUNC (SYSTIMESTAMP)
) WHERE MOBILENO = :mobileNumber
But the problem is customer who are doing transaction in sunday 00:00:01 to 01:00:00 hours, the above query updating the previous week limits
instead of current week.
SUMMARY
MOBILENUMBER AMT TXNDATE
0000000000 10000 26-12-2019 09:05:34
0000000000 10000 28-12-2019 11:05:34
0000000000 10000 29-12-2019 00:01:35
When I run this query in sunday first hour it returning
200
But from next hour
it returning only
100
Why the starting hour of week gives the previous week data.
Here is my NLS parameters
NLS_LANGUAGE ENGLISH
NLS_TERRITORY INDIA
NLS_CURRENCY Rs
NLS_ISO_CURRENCY INDIA
NLS_NUMERIC_CHARACTERS .,
NLS_CALENDAR GREGORIAN
NLS_DATE_FORMAT DD-MM-RR
NLS_DATE_LANGUAGE ENGLISH
NLS_SORT BINARY
NLS_TIME_FORMAT HH12:MI:SSXFF AM
NLS_TIMESTAMP_FORMAT DD-MM-RR HH12:MI:SSXFF AM
NLS_TIME_TZ_FORMAT HH12:MI:SSXFF AM TZR
NLS_TIMESTAMP_TZ_FORMAT DD-MM-RR HH12:MI:SSXFF AM TZR
NLS_DUAL_CURRENCY Rs
NLS_COMP BINARY
NLS_LENGTH_SEMANTICS BYTE
NLS_NCHAR_CONV_EXCP FALSE
Any help will be greatly appreciated!!!!
I believe your issue lies in your date manipulation. You are doing:
TRUNC(TXNDT) BETWEEN (TRUNC (SYSTIMESTAMP, 'IW') - 1 ) AND TRUNC (SYSTIMESTAMP)
and expecting that to make the start of the week a Sunday.
However, you haven't done this correctly - your logic says "take the Monday of the current week and subtract a day from it". If your day is a Sunday, that means it first gets the previous Monday and subtracts a day to make it the previous Sunday.
What you must first do is turn the Sunday into a Monday (by adding a day), truncate it to the first day of the week (Monday), and then subtract a day.
Which means your predicate should now look like:
TRUNC(TXNDT) BETWEEN (TRUNC (SYSDATE + 1, 'IW') - 1 ) AND TRUNC (SYSDATE)
Here's a test case demonstrating the difference:
WITH dts AS (SELECT TRUNC(SYSDATE, 'mm') - 1 + LEVEL dt
FROM dual
CONNECT BY LEVEL <= 13)
SELECT dt,
to_char(dt, 'Dy') day_of_week,
TRUNC(dt, 'iw') monday_start_week,
TRUNC(dt, 'iw') - 1 your_sunday_start_week,
TRUNC(dt + 1, 'iw') - 1 actual_sunday_start_week
FROM dts;
DT DAY_OF_WEEK MONDAY_START_WEEK YOUR_SUNDAY_START_WEEK ACTUAL_SUNDAY_START_WEEK
----------- ----------- ----------------- ---------------------- ------------------------
01/01/2020 Wed 30/12/2019 29/12/2019 29/12/2019
02/01/2020 Thu 30/12/2019 29/12/2019 29/12/2019
03/01/2020 Fri 30/12/2019 29/12/2019 29/12/2019
04/01/2020 Sat 30/12/2019 29/12/2019 29/12/2019
05/01/2020 Sun 30/12/2019 29/12/2019 05/01/2020 <-----
06/01/2020 Mon 06/01/2020 05/01/2020 05/01/2020
07/01/2020 Tue 06/01/2020 05/01/2020 05/01/2020
08/01/2020 Wed 06/01/2020 05/01/2020 05/01/2020
09/01/2020 Thu 06/01/2020 05/01/2020 05/01/2020
10/01/2020 Fri 06/01/2020 05/01/2020 05/01/2020
11/01/2020 Sat 06/01/2020 05/01/2020 05/01/2020
12/01/2020 Sun 06/01/2020 05/01/2020 12/01/2020 <-----
13/01/2020 Mon 13/01/2020 12/01/2020 12/01/2020
You can see that for the two Sunday rows, your version returns the previous Sunday, whereas the amended version returns the current Sunday.
Related
I need to get last 6 weeks data from some table, right now the logic that I use is this
WEEK([date column]) BETWEEN WEEK(NOW()) - 6 AND WEEK(NOW())
It run as I want, but January is near and I realize that this query will not working as it is. I try to run my query on 15th January 2022, I only get data from 1st January to 15th January when I use my logic.
TGL MINGGU_KE
2022-01-01 | 1
2022-01-02 | 2
2022-01-03 | 2
2022-01-04 | 2
2022-01-05 | 2
2022-01-06 | 2
2022-01-07 | 2
2022-01-08 | 2
2022-01-09 | 3
2022-01-10 | 3
2022-01-11 | 3
2022-01-12 | 3
2022-01-13 | 3
2022-01-14 | 3
2022-01-15 | 3
Can I get the last 6 weeks data including last year?
This is my dbfiddle: https://dbfiddle.uk/o9BeAFJF
You can round the dates to the first day of the week using ROUND, TRUNC or THIS_WEEK
WITH
SEARCH_WEEK (TGL) AS (
VALUES date '2020-12-01'
UNION ALL
SELECT tgl + 1 DAY FROM SEARCH_WEEK WHERE tgl < CURRENT date
),
BASE_DATE (base_date) AS (
VALUES date '2022-01-15'
),
OPTIONS (OPTION, OPTION_BASE_DATE) AS (
SELECT OPTION, option_base_date FROM base_date CROSS JOIN LATERAL (
VALUES
('ROUND D', ROUND(base_date, 'D')),
('ROUND IW', ROUND(base_date, 'IW')),
('ROUND W', ROUND(base_date, 'W')),
('ROUND WW', ROUND(base_date, 'WW')),
('TRUNC D', TRUNC(base_date, 'D')),
('TRUNC IW', TRUNC(base_date, 'IW')),
('TRUNC W', TRUNC(base_date, 'W')),
('TRUNC WW', TRUNC(base_date, 'WW')),
('THIS_WEEK', THIS_WEEK(base_date)),
('THIS_WEEK + 1 DAY', THIS_WEEK(base_date) + 1 DAY)
) a (OPTION, OPTION_BASE_DATE)
)
SELECT
OPTION,
MIN(TGL) BEGIN,
max(tgl) END,
dayname(MIN(TGL)) day_BEGIN,
dayname(max(tgl)) day_end,
days_between(max(tgl), min(tgl)) + 1 duration_in_days
FROM
SEARCH_WEEK
CROSS JOIN options
WHERE
TGL BETWEEN option_base_date - 35 DAYS AND option_base_date + 6 DAYS
GROUP BY OPTION
OPTION
BEGIN
END
DAY_BEGIN
DAY_END
DURATION_IN_DAYS
ROUND D
2021-12-12
2022-01-22
Sunday
Saturday
42
ROUND IW
2021-12-13
2022-01-23
Monday
Sunday
42
ROUND W
2021-12-11
2022-01-21
Saturday
Friday
42
ROUND WW
2021-12-11
2022-01-21
Saturday
Friday
42
THIS_WEEK
2021-12-05
2022-01-15
Sunday
Saturday
42
THIS_WEEK + 1 DAY
2021-12-06
2022-01-16
Monday
Sunday
42
TRUNC D
2021-12-05
2022-01-15
Sunday
Saturday
42
TRUNC IW
2021-12-06
2022-01-16
Monday
Sunday
42
TRUNC W
2021-12-11
2022-01-21
Saturday
Friday
42
TRUNC WW
2021-12-11
2022-01-21
Saturday
Friday
42
fiddle
you can use dateadd to get first day of week six weeks ago like this:
Select * from tableName
where [dateColumn] between dateadd(WEEK,-6,getdate()) and getdate()
You can use DATEADD to get last 6 weeks of data as follows:
Select * from [TableName] where [DateColumn] between
DATEADD(WEEK,-6,GETDATE()) and GETDATE();
I want to calculate the last day of the month, but if this date is a sunday then the below query should not give any output.
SELECT * FROM DUAL WHERE LAST_DAY(SYSDATE) = SYSDATE
I am using it in a trigger hence I only want the above query it give an output when the last day of the month is not a sunday. Which function to use to get the day of the month or to tweak this query ?
Like this:
select 1
from dual
where to_char(last_day(sysdate),'D') != 1;
Depending on your NLS-settings you may have to change the constant or use an NLS-parameter in to_char().
There are 2 common ways to check the day of week:
Most obvious and simple thing is to specify you nls_date_language and use weekday names:
SQL> select to_char(sysdate,'day','nls_date_language=English') from dual;
TO_CHAR(SYSDATE,'DAY','NLS_DATE_LANG
------------------------------------
friday
1 row selected.
SQL> select to_char(sysdate,'day','nls_date_language=French') from dual;
TO_CHAR(SYSDATE,'DAY','NLS_DATE_
--------------------------------
vendredi
1 row selected.
So you can use
SQL> select 'ok' x from dual where to_char(sysdate,'fmday','nls_date_language=English')='friday';
X
--
ok
1 row selected.
NB. Do not forget to use 'fm' to trim white spaces, ie 'friday' and not 'friday ':
SQL> select '['||to_char(sysdate,'day','nls_date_language=English')||']' from dual;
'['||TO_CHAR(SYSDATE,'DAY','NLS_DATE_L
--------------------------------------
[friday ]
1 row selected.
SQL> select '['||to_char(sysdate,'fmday','nls_date_language=English')||']' from dual;
'['||TO_CHAR(SYSDATE,'FMDAY','NLS_DATE
--------------------------------------
[friday]
1 row selected.
Another way is to use numeric day format, but in this case your day number depends on nls_terrory, for example:
DBFiddle: https://dbfiddle.uk/o4i3rlBp
SQL> alter session set nls_territory=America;
Session altered.
with first_10_of_2020 as (
select date'2020-01-01'+level-1 dt from dual connect by level<=10
)
select
to_char(dt,'yyyy-mm-dd') dt
,to_char(dt,'day','nls_date_language=''English''') day_en
,to_char(dt, 'd', 'nls_date_language=''NUMERIC DATE LANGUAGE''') d_1
from first_10_of_2020;
DT DAY_EN D
---------- ------------------------------------ -
2020-01-01 wednesday 4
2020-01-02 thursday 5
2020-01-03 friday 6
2020-01-04 saturday 7
2020-01-05 sunday 1
2020-01-06 monday 2
2020-01-07 tuesday 3
2020-01-08 wednesday 4
2020-01-09 thursday 5
2020-01-10 friday 6
10 rows selected.
SQL> alter session set nls_territory=France;
Session altered.
with first_10_of_2020 as (
select date'2020-01-01'+level-1 dt from dual connect by level<=10
)
select
to_char(dt,'yyyy-mm-dd') dt
,to_char(dt,'day','nls_date_language=''English''') day_en
,to_char(dt, 'd', 'nls_date_language=''NUMERIC DATE LANGUAGE''') d_1
from first_10_of_2020;
DT DAY_EN D
---------- ------------------------------------ -
2020-01-01 wednesday 3
2020-01-02 thursday 4
2020-01-03 friday 5
2020-01-04 saturday 6
2020-01-05 sunday 7
2020-01-06 monday 1
2020-01-07 tuesday 2
2020-01-08 wednesday 3
2020-01-09 thursday 4
2020-01-10 friday 5
10 rows selected.
As you can see in case of NLS_TERRITORY=America, Friday is 6, while in case of France it's 5. So to make it more reliable you can use ISO weeks, ie you can use > 'IW' format model:
IW: Calendar week of year (1-52 or 1-53), as defined by the ISO 8601 standard.
A calendar week starts on Monday.
The first calendar week of the year includes January 4.
The first calendar week of the year may include December 29, 30 and 31.
The last calendar week of the year may include January 1, 2, and 3.
So you can use trunc(dt)-trunc(dt,'iw')+1 and it doesn't depend on your nls_language/nls_territory: Monday is always 1 and Sunday is always 7:
DBFiddle: https://dbfiddle.uk/zVS2vcjK
SQL> alter session set nls_territory=America;
Session altered.
with first_10_of_2020 as (
select date'2020-01-01'+level-1 dt from dual connect by level<=10
)
select
to_char(dt,'yyyy-mm-dd') dt
,to_char(dt,'day','nls_date_language=''English''') day_en
,trunc(dt)-trunc(dt,'iw')+1 as d_1
from first_10_of_2020;
DT DAY_EN D_1
---------- ------------------------------------ -----
2020-01-01 wednesday 3
2020-01-02 thursday 4
2020-01-03 friday 5
2020-01-04 saturday 6
2020-01-05 sunday 7
2020-01-06 monday 1
2020-01-07 tuesday 2
2020-01-08 wednesday 3
2020-01-09 thursday 4
2020-01-10 friday 5
10 rows selected.
SQL> alter session set nls_territory=France;
Session altered.
with first_10_of_2020 as (
select date'2020-01-01'+level-1 dt from dual connect by level<=10
)
select
to_char(dt,'yyyy-mm-dd') dt
,to_char(dt,'day','nls_date_language=''English''') day_en
,trunc(dt)-trunc(dt,'iw')+1 as d_1
from first_10_of_2020;
DT DAY_EN D_1
---------- ------------------------------------ -----
2020-01-01 wednesday 3
2020-01-02 thursday 4
2020-01-03 friday 5
2020-01-04 saturday 6
2020-01-05 sunday 7
2020-01-06 monday 1
2020-01-07 tuesday 2
2020-01-08 wednesday 3
2020-01-09 thursday 4
2020-01-10 friday 5
10 rows selected.
I have a simple query as:
select to_date('2020-02-29', 'yyyy-mm-dd') - interval '1' year from dual
I think the result should be 2019-02-28, but oracle throws error as:
Error report -
ORA-01839: date not valid for month specified
That is the documented behaviour; it even gives this as an example:
When interval calculations return a datetime value, the result must be
an actual datetime value or the database returns an error. For
example, the next two statements return errors:
SELECT TO_DATE('31-AUG-2004','DD-MON-YYYY') + TO_YMINTERVAL('0-1')
FROM DUAL;
SELECT TO_DATE('29-FEB-2004','DD-MON-YYYY') + TO_YMINTERVAL('1-0')
FROM DUAL;
The first fails because adding one month to a 31-day month would
result in September 31, which is not a valid date. The second fails
because adding one year to a date that exists only every four years is
not valid. However, the next statement succeeds, because adding four
years to a February 29 date is valid:
SELECT TO_DATE('29-FEB-2004', 'DD-MON-YYYY') + TO_YMINTERVAL('4-0')
FROM DUAL;
TO_DATE('
---------
29-FEB-08
The alternative is to use add_months(..., -12) (docs), which won't error:
select add_months(date '2020-02-29', -12) from dual;
ADD_MONTHS
----------
2019-02-28
But note how that deals with different number of days in the month; not really an issue when you're going back exactly a year, but still something to be aware of:
If date is the last day of the month or if the resulting month has fewer days than the day component of date, then the result is the last day of the resulting month.
So some of these might not do what you expected:
with rcte (dt) as (
select last_day(date '2020-01-01')
from dual
union all
select last_day(trunc(dt, 'MM') + interval '1' month)
from rcte
where dt < date '2020-06-01'
)
select dt,
add_months(dt, -12) as minus12, add_months(dt, -3) as minus3, add_months(dt, -1) as minus1,
add_months(dt, 1) as plus1, add_months(dt, 3) as plus3, add_months(dt, 12) as plus12
from rcte
order by dt;
DT MINUS12 MINUS3 MINUS1 PLUS1 PLUS3 PLUS12
---------- ---------- ---------- ---------- ---------- ---------- ----------
2020-01-31 2019-01-31 2019-10-31 2019-12-31 2020-02-29 2020-04-30 2021-01-31
2020-02-29 2019-02-28 2019-11-30 2020-01-31 2020-03-31 2020-05-31 2021-02-28
2020-03-31 2019-03-31 2019-12-31 2020-02-29 2020-04-30 2020-06-30 2021-03-31
2020-04-30 2019-04-30 2020-01-31 2020-03-31 2020-05-31 2020-07-31 2021-04-30
2020-05-31 2019-05-31 2020-02-29 2020-04-30 2020-06-30 2020-08-31 2021-05-31
2020-06-30 2019-06-30 2020-03-31 2020-05-31 2020-07-31 2020-09-30 2021-06-30
I am writing a procedure and using dynamic cursor and SQL query which I am passing as string in V_SQL variable. Query is as following:
In the where condition, I'm passing date but the condition is that if it's month end and it falls on Friday, Saturday or sunday then reset it to Thursday. for instance, 30th June will be sunday so value passed to day_of_month in SQL query should be 27 i.e. day no from Thursday date.
Could you please help me whether writing separate function will be good and what code should I put for better performance and desired result.
V_SQL := 'SELECT B.FIN_ELEM, A.ORG_UNIT_ID, A.GL_ACCOUNT_ID, B.CMN_COA_ID, B.PROD1, B.PROD2, B.PROD3, '||
'A.AS_OF_DATE, SUM(CURRENT_BAL) AS CB_SUM, SUM(AVG_BAL) AS AB_SUM, B.FLAG1 FROM DAILYGL A, AL_LOOKUP B '||
'WHERE A.GL_ACCOUNT_ID = B.GL_ACCT ***AND DAY_OF_MONTH = '|| TO_DO_FUNCTION(V_RUN_DATE)***
' AND ROWNUM <=15 GROUP BY B.FIN_ELEM, A.ORG_UNIT_ID, A.GL_ACCOUNT_ID,B.CMN_COA_ID, B.PROD1, B.PROD2, B.PROD3';
DAY_OF_MONTH = '|| TO_DO_FUNCTION(V_RUN_DATE)
Desired result will be passed Thursday day number if last business day is in fri, sat or sunday.
You don't really need a function, you can achieve this with a case expression, something like:
AND DAY_OF_MONTH = case
when last_day(v_run_date) - v_run_date <= 3
and to_char(v_run_date, 'Dy', 'NLS_DATE_LANGUAGE=ENGLISH') in ('Fri', 'Sat', 'Sun')
and to_char(last_day(v_run_date), 'Dy', 'NLS_DATE_LANGUAGE=ENGLISH') in ('Fri', 'Sat', 'Sun')
then next_day(trunc(v_run_date, 'IW'), 'THURSDAY')
else v_run_date
end
That is, if v_run_date is within three days of the last day of the month, and v_run_date is a Friday, Saturday or Sunday, and the last day of the month is a Friday, Saturday or Sunday, then use the date of that week's Thursday.
Demo with dates generated for all of this year:
with cte (v_run_date) as (
select date '2018-12-31' + level
from dual
connect by level <= 365
)
select v_run_date,
to_char(v_run_date, 'Dy', 'NLS_DATE_LANGUAGE=ENGLISH') as dy,
case
when last_day(v_run_date) - v_run_date <= 3
and to_char(v_run_date, 'Dy', 'NLS_DATE_LANGUAGE=ENGLISH') in ('Fri', 'Sat', 'Sun')
and to_char(last_day(v_run_date), 'Dy', 'NLS_DATE_LANGUAGE=ENGLISH') in ('Fri', 'Sat', 'Sun')
then next_day(trunc(v_run_date, 'IW'), 'THURSDAY')
else v_run_date
end as adjusted_date
from cte;
V_RUN_DATE DY ADJUSTED_D
---------- ------------ ----------
2019-01-01 Tue 2019-01-01
2019-01-02 Wed 2019-01-02
...
2019-01-30 Wed 2019-01-30
2019-01-31 Thu 2019-01-31
2019-02-01 Fri 2019-02-01
2019-02-02 Sat 2019-02-02
...
2019-03-26 Tue 2019-03-26
2019-03-27 Wed 2019-03-27
2019-03-28 Thu 2019-03-28
2019-03-29 Fri 2019-03-28
2019-03-30 Sat 2019-03-28
2019-03-31 Sun 2019-03-28
2019-04-01 Mon 2019-04-01
...
2019-04-30 Tue 2019-04-30
2019-05-01 Wed 2019-05-01
...
2019-05-28 Tue 2019-05-28
2019-05-29 Wed 2019-05-29
2019-05-30 Thu 2019-05-30
2019-05-31 Fri 2019-05-30
2019-06-01 Sat 2019-06-01
2019-06-02 Sun 2019-06-02
...
2019-06-26 Wed 2019-06-26
2019-06-27 Thu 2019-06-27
2019-06-28 Fri 2019-06-27
2019-06-29 Sat 2019-06-27
2019-06-30 Sun 2019-06-27
2019-07-01 Mon 2019-07-01
...
2019-07-31 Wed 2019-07-31
2019-08-01 Thu 2019-08-01
...
2019-08-28 Wed 2019-08-28
2019-08-29 Thu 2019-08-29
2019-08-30 Fri 2019-08-29
2019-08-31 Sat 2019-08-29
2019-09-01 Sun 2019-09-01
...
2019-11-27 Wed 2019-11-27
2019-11-28 Thu 2019-11-28
2019-11-29 Fri 2019-11-28
2019-11-30 Sat 2019-11-28
2019-12-01 Sun 2019-12-01
...
db<>fiddle
As mentioned in a comment, your code doesn't appear to need to be dynamic; you can use static SQL for what you've shown, e.g.:
SELECT *
-- into ...
FROM (
SELECT B.FIN_ELEM, A.ORG_UNIT_ID, A.GL_ACCOUNT_ID,
B.CMN_COA_ID, B.PROD1, B.PROD2, B.PROD3, A.AS_OF_DATE,
SUM(CURRENT_BAL) AS CB_SUM, SUM(AVG_BAL) AS AB_SUM, B.FLAG1
FROM DAILYGL A
JOIN AL_LOOKUP B ON B.GL_ACCT = A.GL_ACCOUNT_ID
WHERE DAY_OF_MONTH = case
when last_day(v_run_date) - v_run_date <= 3
and to_char(v_run_date, 'Dy', 'NLS_DATE_LANGUAGE=ENGLISH') in ('Fri', 'Sat', 'Sun')
and to_char(last_day(v_run_date), 'Dy', 'NLS_DATE_LANGUAGE=ENGLISH') in ('Fri', 'Sat', 'Sun')
then next_day(trunc(v_run_date, 'IW'), 'THURSDAY')
else v_run_date
end
GROUP BY B.FIN_ELEM, A.ORG_UNIT_ID, A.GL_ACCOUNT_ID,B.CMN_COA_ID, B.PROD1, B.PROD2, B.PROD3
-- order by something...
)
WHERE ROWNUM <= 15;
I've also switched to ANSI joins but more importantly moved the main query into an inline view, and then applied the rownum filter to that; which will make no difference unless/until you add an order-by clause inside that inline view. Without it this, and your original, will get an indeterminate (not quite random, but similar effect) set of 15 rows back. If you order the inner query then you'll always get the first 15 rows according to your ordering criteria. From 12c you can use a row limiting clause instead of the subquery/rownum filter, but this approach will work in earlier versions.
Obviously you still need to either select that into variables or use it as a cursor query - whatever you had planned to do dynamically.
I hope this will solve your problem
V_RUN_DATE - (case when V_RUN_DATE > LAST_DAY (V_RUN_DATE)- 3 then V_RUN_DATE else
(case TO_CHAR(date V_RUN_DATE, 'DY') when 'FRI' then 1 when 'SAT' then 2 when 'SUN' then 3 else 0 end) end)
My fiscal year starts on May 1 and ends Apr 30. I am trying to return the fiscal year start date using a CASE statement.
I.e.
CAST(
CASE
WHEN TO_NUMBER (TO_CHAR (GET_DATE, 'MM')) IN (11,
12,
5,
6,
7,
8,
9,
10)
THEN
'05/01/'
|| TO_NUMBER (TO_CHAR (TRUNC (get_date, 'year'), 'YYYY'))
WHEN TO_NUMBER (TO_CHAR (GET_DATE, 'MM')) IN (1,
2,
3,
4)
THEN
'05/01/'
|| TO_NUMBER (TO_CHAR (TRUNC (get_date, 'year'), 'YYYY') - 1)
END AS DATE)
I am getting "invalid month" when I use the cast but when I take it off, it defaults to number. Either way, i'm not getting the results I want.
Just subtract 4 (four) months off your date and pull the year out of it (use trunc to reduce it to the year) then add 4 (four) months back on
SELECT
ADD_MONTHS(
TRUNC(
ADD_MONTHS( <yourdate> ,-4),
'YEAR'),
4)
FROM DUAL;
As to why it works:
We have some example dates: 25-04-2009 13-07-2009
These are in the fiscal years beginning: 01-05-2008 01-05-2009
We subtract 4 months from the date: 25-12-2008 13-03-2009
We trunc down to the year start: 01-01-2008 01-01-2009
We add 4 months back on to get to May: 01-05-2009 01-05-2009
Why is it good/better than converting to string and back? Well, that's the reason right there. Dates are represented as a number, and this method keeps it as a number and works entirely off maths; adding, rounding and substracting. It's always better to avoid unnecessary data type conversions, because thy're slow, resource intensive and can introduce unexpected conversion errors
Using TRUNC with dates is one of the coolest things oracle does with dates that other databases just can't handle. Being able to take any date and TRUNC() it to the start of the year/month/day/hour/minute/weekday etc month is a great help in reports where events are logged to millisecond precision, but you want to summarise or work with them in terms of "number of things that happened this week/month" etc
For Oracle, this get the Start of Fiscal Year for any date.
Just replace "sysdate" function with a variable of type DATE or a column name of type DATE:
select /* for Oracle */
to_date(CASE
WHEN extract(month from sysdate)<5 then
extract(year from sysdate)-1
ELSE
extract(year from sysdate)
end||'-05-01',
'yyyy-mm-dd') as start_fiscal_year
from dual;
Testing on Oracle database using anonymous block:
Declare
/*=========================================================================================
-- objective: calculate Start fiscal date
-- Fisacal year starts on May 1 and ends April 30
--
-- https://stackoverflow.com/questions/52426117/casting-date-returns-invalid-month-error-in-plsql
--
-- Database: Oracle
--
-- 2018-09-20 alvalongo
==========================================================================================*/
dtStart_date date:=to_date('2018-01-10','yyyy-mm-dd');
dtAny_date date;
dtStart_fiscal_date date;
Begin
dbms_output.put_line('I |ANY_DATE |START_FISCAL_YEAR');
for nuI in 0..24 loop
dtAny_date:=add_months(dtStart_date,nuI);
--
select to_date(CASE
WHEN extract(month from dtAny_date)<5 then
extract(year from dtAny_date)-1
ELSE
extract(year from dtAny_date)
end||'-05-01','yyyy-mm-dd') as start_fiscal_year
into dtStart_fiscal_date
from dual;
if extract(month from dtAny_date)=5 then
dbms_output.put_line('--|----------|----------');
end if;
dbms_output.put_line(lpad(nuI,2)
||'|'||to_char(dtAny_date ,'yyyy-mm-dd')
||'|'||to_char(dtStart_fiscal_date,'yyyy-mm-dd')
);
End loop;
End;
/
Output using dbms_output buffer:
I |ANY_DATE |START_FISCAL_YEAR
0|2018-01-10|2017-05-01
1|2018-02-10|2017-05-01
2|2018-03-10|2017-05-01
3|2018-04-10|2017-05-01
--|----------|----------
4|2018-05-10|2018-05-01
5|2018-06-10|2018-05-01
6|2018-07-10|2018-05-01
7|2018-08-10|2018-05-01
8|2018-09-10|2018-05-01
9|2018-10-10|2018-05-01
10|2018-11-10|2018-05-01
11|2018-12-10|2018-05-01
12|2019-01-10|2018-05-01
13|2019-02-10|2018-05-01
14|2019-03-10|2018-05-01
15|2019-04-10|2018-05-01
--|----------|----------
16|2019-05-10|2019-05-01
17|2019-06-10|2019-05-01
18|2019-07-10|2019-05-01
19|2019-08-10|2019-05-01
20|2019-09-10|2019-05-01
21|2019-10-10|2019-05-01
22|2019-11-10|2019-05-01
23|2019-12-10|2019-05-01
24|2020-01-10|2019-05-01
Total execution time 517 ms
Expanding on #CaiusJard's answer, you can get the start of the fiscal year with:
add_months(trunc(add_months(get_date, -4), 'YYYY'), 4)
The add_months(get_date, -4) subtracts four months from the starting date, so a value in January to April will be adjusted to a date in September to December of the previous year. So for instance, 2018-03-11 will become 2017-11-11. But dates dates from May onwards will stay in the same year, so for instance 2018-07-04 becomes 2018-03-04.
Then the trunc(..., 'YYYY') truncates that adjusted value to the first day of its year. So 2018-03-11 becomes 2017-11-11 which becomes 2017-01-01; and 2018-07-04 becomes 2018-03-04 which becomes 2018-01-01.
Then the outer add_months(..., 4) adds four months back on to that adjusted value. So 2018-03-11 becomes 2017-11-11 which becomes 2017-01-01 which finally becomes 2017-05-01; and 2018-07-04 becomes 2018-03-04 which becomes 2018-01-01 which finally becomes 2018-05-01.
To get the last day of the fiscal year you can do the same thing but add an extra 12 months in the final calculation - which gives you the start of the next fiscal year - and then subtract one day:
add_months(trunc(add_months(get_date, -4), 'YYYY'), 16) - 1
Read more about the add_months() and trunc() functions, and about date arithmetic, in the documentation.
Demo with dummy dates in a CTE to show the steps in the adjustment:
with your_table (get_date) as (
select add_months(date '2018-01-15', level)
from dual
connect by level <= 30
)
select get_date,
add_months(get_date, -4) as adjusted_month,
trunc(add_months(get_date, -4), 'YYYY') as adjusted_year,
add_months(trunc(add_months(get_date, -4), 'YYYY'), 4) as start_date,
add_months(trunc(add_months(get_date, -4), 'YYYY'), 16) - 1 as end_date
from your_table
order by get_date;
GET_DATE ADJUSTED_M ADJUSTED_Y START_DATE END_DATE
---------- ---------- ---------- ---------- ----------
2018-02-15 2017-10-15 2017-01-01 2017-05-01 2018-04-30
2018-03-15 2017-11-15 2017-01-01 2017-05-01 2018-04-30
2018-04-15 2017-12-15 2017-01-01 2017-05-01 2018-04-30
2018-05-15 2018-01-15 2018-01-01 2018-05-01 2019-04-30
2018-06-15 2018-02-15 2018-01-01 2018-05-01 2019-04-30
2018-07-15 2018-03-15 2018-01-01 2018-05-01 2019-04-30
...
2019-01-15 2018-09-15 2018-01-01 2018-05-01 2019-04-30
2019-02-15 2018-10-15 2018-01-01 2018-05-01 2019-04-30
2019-03-15 2018-11-15 2018-01-01 2018-05-01 2019-04-30
...
2020-02-15 2019-10-15 2019-01-01 2019-05-01 2020-04-30
2020-03-15 2019-11-15 2019-01-01 2019-05-01 2020-04-30
2020-04-15 2019-12-15 2019-01-01 2019-05-01 2020-04-30
2020-05-15 2020-01-15 2020-01-01 2020-05-01 2021-04-30
2020-06-15 2020-02-15 2020-01-01 2020-05-01 2021-04-30
2020-07-15 2020-03-15 2020-01-01 2020-05-01 2021-04-30
db<>fiddle
As noted elsewhere in comments, your original code is erroring because cast(<string> as date) uses your session's NLS settings, and the string you are constructing does not match that setting. You could use to_date() instead of case so you can supply the expected format mask (see #alvalongo's answer!).
This should be work, if you want to maintain the CASE statement
select CASE WHEN
MONTH(CURRENT_DATE) < 5 THEN
YEAR(CURRENT_DATE)-1 ELSE YEAR(CURRENT_DATE)
end
I tried the following code and it worked for me. DATE is an inbuilt key word so we cannot use that as the column alias. I changed that to FS_START_DATE.
SELECT SYSDATE, TO_DATE(CASE
WHEN TO_NUMBER (TO_CHAR (SYSDATE, 'MM')) IN (11, 12, 5,6,7, 8,9, 10)
THEN '05/01/' || TO_NUMBER (TO_CHAR (TRUNC (SYSDATE, 'year'), 'YYYY'))
WHEN TO_NUMBER (TO_CHAR (SYSDATE, 'MM')) IN (1, 2, 3,4)
THEN '05/01/' || TO_NUMBER (TO_CHAR (TRUNC (SYSDATE, 'year'), 'YYYY') - 1)
END,'MM/DD/YYYY') AS FS_START_DATE FROM DUAL;
I am using TO_DATE function and this returns me 01-MAY-2018.