Skip to Main Content

Breadcrumb

Question and Answer

Chris Saxon

Thanks for the question, Carl.

Asked: December 15, 2000 - 9:32 am UTC

Last updated: August 01, 2011 - 12:01 pm UTC

Version: 8i

Viewed 100K+ times! This question is

You Asked

I need to know how to add 1 year to a start date in this format
for example:
if the start date is December,15 2000 I would like to generate the expiration date to be Deember 14 2001
thanks

and Tom said...

Add 12 months and subtract 1 day:

SQL> select add_months( to_date('15-dec-2000'), 12 )-1
from dual;

ADD_MONTH
---------
14-DEC-01



Rating

  (26 ratings)

Is this answer out of date? If it is, please let us know via a Comment

Comments

Very Handy

Jim, October 16, 2002 - 1:57 am UTC

First impressions to get the next year might be
to add 365 days... with leap years that sure to bite you
at some stage, and who wants to get into a what a leap year
debate.

Add_months is cool

Thanks Tom

another one on add_months

Vijay, November 17, 2005 - 4:03 am UTC

Dear Tom,
good day to you, hope you are fine and doing good, I am facing one small issue with add_months below is the scenario.

below query returns 27/02/2008 which is fine
select add_months(to_date('27/02/2005','dd/mm/yyyy'),36) from dual;

but if i change the query to
select add_months(to_date('28/02/2005','dd/mm/yyyy'),36) from dual
it returns 29/02/2008, I want to know why 28/02/2008 is missed,
basically I am working on a script which selects max date from a column of table and for each day of next 3 years inserts the records in the table if there's a leap year between the day is missed, how can this be rectified.

Kind Regards,
Vijay

Tom Kyte
November 17, 2005 - 8:13 am UTC

add_months, as documented and by definition, returns the last_day of the resulting month if you input the last_day of a month to begin with.


add_months( any_last_day_of_a_month, some_number_of_months) returns the last_day of the resulting month.


I don't know what you mean in the last part of your paragraph, why doesn't this work?

here's the example

Vijay, November 17, 2005 - 8:27 am UTC

Hi Tom,
here's the example

INSERT INTO CV
SELECT TRANSCO_REGION, ADD_MONTHS(CALENDAR_DAT,12), VALUE FROM CV
WHERE calendar_dat <= (SELECT MAX(calendar_dat) FROM CV)
AND calendar_dat > (SELECT ADD_MONTHS(MAX(calendar_dat),-12) FROM CV)
UNION ALL
SELECT TRANSCO_REGION, ADD_MONTHS(CALENDAR_DAT,24), VALUE FROM CV
WHERE calendar_dat <= (SELECT MAX(calendar_dat) FROM CV)
AND calendar_dat > (SELECT ADD_MONTHS(MAX(calendar_dat),-12) FROM CV)
UNION ALL
SELECT TRANSCO_REGION, ADD_MONTHS(CALENDAR_DAT,36), VALUE FROM CV
WHERE calendar_dat <= (SELECT MAX(calendar_dat) FROM CV)
AND calendar_dat > (SELECT ADD_MONTHS(MAX(calendar_dat),-12) FROM CV)
UNION ALL
SELECT TRANSCO_REGION, ADD_MONTHS(CALENDAR_DAT,48), VALUE FROM CV
WHERE calendar_dat <= (SELECT MAX(calendar_dat) FROM CV)
AND calendar_dat > (SELECT ADD_MONTHS(MAX(calendar_dat),-12) FROM CV)
UNION ALL
SELECT TRANSCO_REGION, ADD_MONTHS(CALENDAR_DAT,60), VALUE FROM CV
WHERE calendar_dat <= (SELECT MAX(calendar_dat) FROM CV)
AND calendar_dat > (SELECT ADD_MONTHS(MAX(calendar_dat),-12) FROM CV)
UNION ALL
SELECT TRANSCO_REGION, ADD_MONTHS(CALENDAR_DAT,72), VALUE FROM CV
WHERE calendar_dat <= (SELECT MAX(calendar_dat) FROM CV)
AND calendar_dat > (SELECT ADD_MONTHS(MAX(calendar_dat),-12) FROM CV)
UNION ALL
SELECT TRANSCO_REGION, ADD_MONTHS(CALENDAR_DAT,84), VALUE FROM CV
WHERE calendar_dat <= (SELECT MAX(calendar_dat) FROM CV)
AND calendar_dat > (SELECT ADD_MONTHS(MAX(calendar_dat),-12) FROM CV)

the above sql inserts into CV table records for next 7 years starting from max(calendar_dat) already in the table, what the problem I am facing is 2008 is leap year it misses the date 28/02/2008 similary for 2012.

I hope this example makes things a little clear for you.

Kind Regards,
Vijay



Tom Kyte
November 17, 2005 - 8:43 am UTC

but it is putting in feb 29th. What is special about the 28th?

You have something on the last day of a MONTH, add months says "add a month to the last day of a month, you get the last day of the resulting month"



An alternative method....

John Scott, November 17, 2005 - 10:25 am UTC

I've found that this works on 9.2.0.1 and later -

select sysdate + interval '1' year from dual

the interval can be a variety of types, e.g.

select sysdate + interval '3' day from dual


Tom Kyte
November 18, 2005 - 7:28 am UTC

must be careful with that one.

ops$tkyte@ORA10GR2> select to_date('31-jan-2005') from dual;

TO_DATE('
---------
31-JAN-05

ops$tkyte@ORA10GR2> select to_date('31-jan-2005') + interval '1' month from dual;
select to_date('31-jan-2005') + interval '1' month from dual
                              *
ERROR at line 1:
ORA-01839: date not valid for month specified


ops$tkyte@ORA10GR2> select to_date('31-jan-2005') + interval '2' month from dual;

TO_DATE('
---------
31-MAR-05



I'm not fond of interval math too much - not with adding months anyway.  It takes the rather simplistic view of "it'll be the same day in the resulting month" - meaning adding months to anything on days 29, 30, 31 is potentially an "error" 

add_months is right approach

Oraboy, November 17, 2005 - 5:56 pm UTC

John 
 if you have Tom's latest book, there is in-depth details  by Tom , on when to use interval and when not to.

"add_months" is calendar sensitive, "interval" is not.

consider this-
SQL>alter session set nls_date_format='DD.MM.YYYY';
SQL>select to_date('10-01-2000', 'DD-MM-YYYY')+ interval '1' year from dual;

TO_DATE('1
----------
10.01.2001 -- this is fine

SQL>select to_date('29-02-2000', 'DD-MM-YYYY')+ interval '1' year from dual;
                                             *
ERROR at line 1:
ORA-01839: date not valid for month specified

SQL>select add_months(to_date('29-02-2000', 'DD-MM-YYYY'),12) from dual;
ADD_MONTHS
----------
28.02.2001

the error is because interval expects feb 29th in 2001..

(so atleast with the current versions releases, add_months is more reliable)

 

Tom Kyte
November 18, 2005 - 10:19 am UTC

this is the way ANSI specified intervals to work, unless the standard changes, it'll be that way.

good learning

Muhammad Ibrahim, November 17, 2005 - 8:59 pm UTC

first of all this interval concept first time i am seeing it. thanks to the poster.

fine if it doesnt work for the below

select to_date('29-02-2000', 'DD-MM-YYYY')+ interval '1' year from dual;

then i have two questions:

1) what is the difference between adding interval and add_months?
2) what is the use oracle has provided this interval concept???

Regards,
Ibrahim.

Tom Kyte
November 18, 2005 - 10:30 am UTC

interval is ANSI, it is part of the sql standard.

add_months is Oracle, we implemented the feature years and years ago, very much before the interval type.




followup the my query of missing 28th.

Vijay, November 17, 2005 - 11:38 pm UTC

Hi Tom,
good day to you, and thanks for taking the efforts and time to look into my problem, as you have mentioned
add months says "add a month to the last day of a month, you get the last day of the resulting month"
so my query example inserts 29th of Feb but I miss 28th of Feb how do I get that, here's some more details on it

select add_months(to_date('27/02/2007','dd/mm/yyyy'),12) from dual

output is 27/02/2008

select add_months(to_date('28/02/2007','dd/mm/yyyy'),12) from dual

output is 29/02/2008

how do I get 28/02/2008

thanking you in anticipation
Vijay



Tom Kyte
November 18, 2005 - 10:33 am UTC

you don't, not with add months.

you'd have to "do it yourself" or use an interval (but beware of the cautions above about it failing on days that are 29, 30, 31 easily.

seems like you are answering the questions and followups I am waiting for your help on this

vijay, November 18, 2005 - 7:44 am UTC


Tom Kyte
November 18, 2005 - 3:14 pm UTC

gee, sorry? I mean, come on. I do them in the order I see them.



Amusing ...

Greg, November 18, 2005 - 8:20 am UTC

What I find really brain-wracking is this:

select add_months(add_months(to_date('30-jul-2005'), -1), 1) nd from dual;

ND
---------
31-JUL-05

1 row selected.

Forget about leap years and all for 1 second ...

date - 1 month + 1 month ... is not equal to date ??

You'd almost think that:

d + x - x = d

But that doesn't seem to hold here .. irritating date math ... Ouch .. my head ... :(


Tom Kyte
November 18, 2005 - 3:17 pm UTC

A month is ambigious.

It is not like "a week"

Tell me, *what is a month*


A year even is ambigous - years do not have the same number of days.. If someone tells you "I am exactly 12 years and 2 months old" today - tell me how many days old they are?

The missing 28th

Ralf, November 18, 2005 - 8:53 am UTC

Vijay,

what about the following query?

select case extract(day from to_date('28/02/2007','dd/mm/yyyy'))
when 1 then add_months(to_date('28/02/2007','dd/mm/yyyy'),12)
else add_months(to_date('28/02/2007','dd/mm/yyyy')-1,12)+1
end
from dual;

CASEEXTRA
---------
01-MAR-13

It's a little bit more complicated but always returns 28/02 when you add a multiple of 12 to 28/02 of any year.

However, you get a perhaps unexpected result if you feed in a date like 29/02/2008!

Kind regards, Ralf


Tom Kyte
November 18, 2005 - 3:18 pm UTC

well, if he always wants the same day - interval 1 year, just beware of the CAVEAT with days 29, 30, 31.

To the poster - wondering how to get 28th feb

Oraboy, November 18, 2005 - 9:36 am UTC

SQL>select add_months(to_date('28/02/2007','dd/mm/yyyy'),12) from dual;

ADD_MONTH
---------
29-FEB-08

SQL>-- I dont care of leap year..Just add 365 days and show me the result

SQL>select to_date('28/02/2007','dd/mm/yyyy')+365 from dual;

TO_DATE('
---------
28-FEB-08 

Tom Kyte
November 18, 2005 - 3:20 pm UTC

years are not 365 days.

ops$tkyte@ORA9IR2> select to_date('28/02/2007','dd/mm/yyyy')+365*rownum from all_users;

TO_DATE('
---------
28-FEB-08
27-FEB-09
27-FEB-10
27-FEB-11
27-FEB-12
26-FEB-13
26-FEB-14
26-FEB-15
26-FEB-16
25-FEB-17
25-FEB-18
25-FEB-19
25-FEB-20
24-FEB-21
24-FEB-22
24-FEB-23
24-FEB-24
23-FEB-25
23-FEB-26
23-FEB-27
23-FEB-28
22-FEB-29
22-FEB-30
22-FEB-31
22-FEB-32
21-FEB-33
21-FEB-34
21-FEB-35
21-FEB-36
20-FEB-37
20-FEB-38
20-FEB-39
20-FEB-40
19-FEB-41
19-FEB-42

35 rows selected.
 

I should have made it clear

Oraboy, November 18, 2005 - 4:19 pm UTC

Tom
  I  know adding 365 days is not right and I intended to explain to original poster (who wanted to jump from 28th Feb 2007 to 28th Feb 2008 ) was not calendar-friendly , rather plain arithmetic. (like adding n days)

I should have put it this way (explaining the poster he/she needs to instruct Oracle the logic they want to follow)


SQL>alter session set nls_date_format='dd.mm.yyyy';

Session altered.

SQL>ed
Wrote file afiedt.buf
  1* select last_day(add_months(to_date('28/02/2007','dd/mm/yyyy'), (12*rownum-1)))+28 from dba_users where rownum<10
SQL>/

LAST_DAY(A
----------
28.02.2008
28.02.2009
28.02.2010
28.02.2011
28.02.2012
28.02.2013
28.02.2014
28.02.2015
28.02.2016


or some other better way to implement the logic in SQL

 

Tom Kyte
November 19, 2005 - 9:45 am UTC

select to_date( '28-feb-' || to_char(2007+rownum,'fm9999'), dd-mon-yyyy
from whatever....

To VIjay

Raj, November 18, 2005 - 9:12 pm UTC

How about query
select decode(to_char(to_date('28-Feb-2005','dd-Mon-yyyy'),'dd'),to_char(add_months(to_date('28-Feb-2005','dd-Mon-yyyy'),36),'dd'),add_months(to_date('28-Feb-2005','dd-Mon-yyyy'),36),add_months(to_date('28-Feb-2005','dd-Mon-yyyy'),36)-1) from dual
/

duh..My bad

Oraboy, November 21, 2005 - 10:14 am UTC

"
select to_date( '28-feb-' || to_char(2007+rownum,'fm9999'), dd-mon-yyyy from whatever....
"
- Duh..!! thats right

I dont know why I was trying to find a hard solution for a simple problem ( Guess I was thinking too much and my mind was preoccupied with date functions and overlooked the basic question.) :-)

PS: May be when you get a chance you should try to put this in your blog (if this sounds interesting).. how do you manage to keep your feet on ground (I mean basics).. I mean you read a lot, answer lots of questions and I am sure you would have more than one solution, for a given problem.






something more on this.

vijay, November 29, 2005 - 6:43 am UTC

Hi Tom,
as you have said "well, if he always wants the same day - interval 1 year", I want to use the same day interval and I can't use the interval as it's 9i feature,my database is 8i, how can I populate a date column of table for all the days of a year using query starting from today's date

Kind Regards,
Vijay

Tom Kyte
November 29, 2005 - 10:28 am UTC

select trunc(sysdate) + rownum-1
from all_objects
where rownum <= 365 (or 366, whichever you want)

And yet another

Borkur, November 30, 2005 - 9:47 am UTC

here is a way, but the question remains what to do with the leap days that don't map in to the next year. Here I put 29.02 to null. If this would be run for a long (long) series we could map each leap day to the next leap day in about 4 years time.
SQL> create table toto  as
  2  select to_date('311299','DDMMRR')+rownum as datum from dba_source where rownum<3000;

Table created.
SQL> select datum,add_months(trunc(datum,'MONTH'),12)+
  2  case
  3  when to_char(datum,'DDMM')='2902' then null
  4  else to_number(to_char(datum,'DD')-1) end datum_yar
  5  from toto
  6* order by 1

DATUM     DATUM_YAR
--------- ---------
25-FEB-00 25-FEB-01
26-FEB-00 26-FEB-01
27-FEB-00 27-FEB-01
28-FEB-00 28-FEB-01
29-FEB-00
01-MAR-00 01-MAR-01
02-MAR-00 02-MAR-01
03-MAR-00 03-MAR-01
04-MAR-00 04-MAR-01
05-MAR-00 05-MAR-01
06-MAR-00 06-MAR-01

...

DATUM     DATUM_YAR
--------- ---------
18-FEB-03 18-FEB-04
19-FEB-03 19-FEB-04
20-FEB-03 20-FEB-04
21-FEB-03 21-FEB-04
22-FEB-03 22-FEB-04
23-FEB-03 23-FEB-04
24-FEB-03 24-FEB-04
25-FEB-02 25-FEB-03
26-FEB-02 26-FEB-03
27-FEB-02 27-FEB-03
28-FEB-02 28-FEB-03
01-MAR-02 01-MAR-03
02-MAR-02 02-MAR-03
 

how to add days to date

Ahmad Farghaly, June 16, 2007 - 3:54 am UTC

Hi Tom, how to add days to a specified day
like add_daye ('1 1 2007', 8) = '9 1 2007'

is it implemented in Oracle,

Thanks

28/12/2007

A reader, September 18, 2010 - 6:10 am UTC

select add_months(to_date('28/feb/2007'),12)-
decode(mod(to_char(add_months(to_date('28/feb/2007'),12),'yyyy'),4),0,1,0) from dual

Need Different Add_Months Function!

Tom Chien, December 10, 2010 - 1:43 pm UTC

First, let me clear the confusion most people seem to have about Add_Months Function. The Add_Months definition includes "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." Most people miss out on the importance of the first clause and simply presume that it only applies to cases where the input "day of month" is greater than the "last day of the resulting month". However, there is no such restriction in the definition. Therefore, it could also apply to cases where the input "day of month" is the last day of the input month and is less than or equal to the result "day of month" which is the case in question here.

What you need is an expression or better yet a "Function" (imagine that) that simulates a modified Add_Months Function, one where the above definition is changed to simply "If the resulting month has fewer days than the day component of date, then the result is the last day of the resulting month." Then, subtract one day from whatever the result is. By the way, the part before subtracting one day would also be the same definition as a VB/VBA DateAdd("m", NumMonthsInc, SourceDate) call.

To do that, you would need an expression like:

case
when
to_char(SourceDate, 'DD') > to_char(last_day(add_months(SourceDate, NumMonths)), 'DD')
then
add_months(SourceDate, NumMonths) - 1
else
to_date (
to_char(add_months(SourceDate, NumMonths), 'MM) + '/'
to_char(SourceDate, 'DD') + '/'
to_char(add_months(SourceDate, NumMonths), 'YYYY)
'MM/DD/YYYY')
)
- 1
end

If you wrote it into a Function, you could eliminate all those duplicate "add_months(SourceDate, NumMonths)" calls with a local variable not to mention embedding that monster of a Case Expression every time you needed it.

A alternative ADD_MONTHS function

Ron, January 03, 2011 - 6:36 am UTC

Perhaps I missed something in my hurry, but a function based on the next coding wordks for me:

declare
v_date date;
v_bool boolean := FALSE;
begin
v_date := to_date('&datum','DDMMYYYY');
if v_date = last_day(v_date) then
v_date := v_date - 1;
v_bool := TRUE;
end if;
v_date := add_months(v_date, &aantal);
if v_bool then
if v_date = last_day(v_date) then
null;
else
v_date := v_date + 1;
end if;
end if;
dbms_output.put_line(to_char(v_date, 'DDMMYYYY'));
end;


Close but Not Answer Original Q and Too Complex

Tom Chien, February 19, 2011 - 5:56 pm UTC

@Ron: Your function does solves the "bug" with the "design" of AddMonths not making the Result Day of Month = Source Day of Month when Source Date = Last Day of Source Date's Month and Result Date's Month has more Days than Source Date's Month. However:

a) It doesn't solve the Original Poster's Q, which is to make the Result Day of Month 1 Day less than the Source Date's Day of Month (even when not in end-of-month scenarios). To do that, you just need to subtract 1 from the result at the end of your Function (or one that calls your Function to do what the Poster needs).

b) Your Function's logic is unnecessarily complex. It uses 2 Local Var's, 3 If's, 1 Else and 5 Assign's. If you converted my Case Expression to a Function, it would just need 1 Local Var, 1 If, 1 Else and 3 Assign's. Also and therefore, it reads more clearly, if you were to translate it to a natural language (i.e if you needed to explain it to someone or someone were trying to understand what it's doing by reading it).

BTW, I say it's a "bug" with the "design" of the AddMonths Function, not just the Function because it's doing what its docs claim it was designed to do. However, I still think it's poor (or at least "un-useful") design because I suspect the most likely use is probably to calculate Expiration Date's of Insurance Policies and that's not how they (and I suspect also other contractual agreements with "months"-based terms) are normally calculated. A 12-month Policy Effective 2/28/2003 (@ 12:01 am) would Expire on 2/28/2004 (@ 12:01 am), not the 2/29/2004 AddMonths (2/28/2003, 12) would return. 2/28/2004 is also what would be returned by Excel's EDate, VB/VBA's DateAdd and .NET's DateTime.AddMonths. Of course, those are all in software produced by a little company in Washingtion. What do they know? ;)

Simplified

Ron, February 24, 2011 - 3:30 am UTC

@TOM CHIEN: You are right Tom. In my search for my own problem I failed to see the original quest.

Don't want to be the wise guy, but if I simplify my test script it would look like this:

begin
if Sourcedate = last_day(Sourcedate) then
Sourcedate := Sourcedate - 1;
Sourcedate := add_months(Sourcedate, NumMonths);
Sourcedate := Sourcedate + 1;
else
Sourcedate := add_months(Sourcedate, NumMonths);
end if;

-- Sourcedate :- Sourcedate - 1 ( the original quest )
end

This is clear and readable, is it not?

I cannot help wondering that I mis something. Something that you (clearly) have taken care of in your solution. If so, please help me out because I've deployed this solution.


Correction Simplified

Ron, February 24, 2011 - 5:39 am UTC

The simplified coding works fine with positive addition. Working with negative values can cause unexpected results.

Coding should be:

begin
if Sourcedate = last_day(Sourcedate) then
Sourcedate := Sourcedate - 1;
Sourcedate := add_months(Sourcedate, NumMonths);
if Sourcedate ^= last_day(Sourcedate) then
Sourcedate := Sourcedate + 1;
end if;
else
Sourcedate := add_months(Sourcedate, NumMonths);
end if;

-- Sourcedate :- Sourcedate - 1 ( the original quest )
end


converting in year.

kuldeep singh, August 01, 2011 - 7:52 am UTC

hi tom
my question is that i change the particular date in year.
example that..
select to_char( add_months(to_date('29-02-2000', 'DD-MM-YYYY'),12),'year') from dual;
result is
TO_CHAR(ADD_MONT
----------------
two thousand one
but my requirement is 2001.
Tom Kyte
August 01, 2011 - 12:01 pm UTC

use YYYY as the format, not 'year'
  1  select to_char( add_months(to_date('29-02-2000', 'DD-MM-YYYY'),12),'yyyy')
  2* from dual
ops$tkyte%ORA11GR2> /

TO_C
----
2001

thanks for your quick reply

kuldeep singh, August 02, 2011 - 1:26 am UTC


A reader, November 09, 2020 - 10:09 pm UTC