views:

2164

answers:

3

I have some code that uses the Oracle function add_months to increment a Date by X number of months.

I now need to re-implement the same logic in a C / C++ function. For reasons I don't want/need to go into I can't simply issue a query to oracle to get the new date.

Does anyone know of a simple and reliable way of adding X number of months to a time_t? Some examples of the types of calculations are shown below.

30/01/2009 + 1 month = 28/02/2009
31/01/2009 + 1 month = 28/02/2009
27/02/2009 + 1 month = 27/03/2009
28/02/2009 + 1 month = 31/03/2009
31/01/2009 + 50 months = 31/03/2013

thanks,
Glen

+5  A: 

Convert time_t to struct tm, add X to month, add months > 12 to years, convert back. tm.tm_mon is an int, adding 32000+ months shouldn't be a problem.

[edit] You might find that matching Oracle is tricky once you get to the harder cases, like adding 12 months to 29/02/2008. Both 01/03/2009 and 28/02/2008 are reasonable.

MSalters
This doesn't work quite like the oracle function. Using the above 30-01-2009 becomes 02-03-2009
Glen
+3  A: 

You can use Boost.GregorianDate for this.

More specifically, determine the month by adding the correct date_duration, and then use end_of_month_day() from the date algorithms

Pieter
Because "There is a simplicity only to be found on the other side of complexity." time_t simply does not have enough information for the type of context he is asking for in his examples. Using boost in a function to do the conversion, then convert back to time_t seems reasonable to me.
Aaron
It seems reasonable to me as well. Unfortunately we don't use Boost and won't be in the near future. :-(
Glen
+2  A: 

Method AddMonths_OracleStyle does what you need.

Perhaps you would want to replace IsLeapYear and GetDaysInMonth to some librarian methods.

#include <ctime>
#include <assert.h>

bool IsLeapYear(int year) 
{
 if (year % 4 != 0) return false;
 if (year % 400 == 0) return true;
 if (year % 100 == 0) return false;
 return true;
}

int daysInMonths[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};

int GetDaysInMonth(int year, int month)
{
 assert(month >= 0);
 assert(month < 12);

 int days = daysInMonths[month];

 if (month == 1 && IsLeapYear(year)) // February of a leap year
  days += 1;

 return days;
}

tm AddMonths_OracleStyle(const tm &d, int months)
{
 bool isLastDayInMonth = d.tm_mday == GetDaysInMonth(d.tm_year, d.tm_mon);

 int year = d.tm_year + months / 12;
 int month = d.tm_mon + months % 12;

 if (month > 11)
 {
  year += 1;
  month -= 12;
 }

 int day;

 if (isLastDayInMonth)
  day = GetDaysInMonth(year, month); // Last day of month maps to last day of result month
 else
  day = std::min(d.tm_mday, GetDaysInMonth(year, month));

 tm result = tm();

 result.tm_year = year;
 result.tm_mon = month;
 result.tm_mday = day;

 result.tm_hour = d.tm_hour;
 result.tm_min = d.tm_min;
 result.tm_sec = d.tm_sec;

 return result;
}

time_t AddMonths_OracleStyle(const time_t &date, int months)
{
 tm d = tm();

 localtime_s(&d, &date);

 tm result = AddMonths_OracleStyle(d, months);

 return mktime(&result);
}
Konstantin Spirin
januari 2012 has 32 days in your code...
Pieter
I think you want to change GetDaysInMonth to check if IsLeapYear(year) AND if month is Feb
hamishmcn
Also not resetting isdst to -1 in the tm structure makes the answer wrong should the additional time added cross dst boundaries. Highly recommend stealing code that already works/handles this properly.
Einstein
hamishmcn, thanks. That's how Microsoft developed software for Zune :)
Konstantin Spirin
This works with a few modifications. 1, use gmtime instead of localtime. 2, add 1900 to year when checking is it a leap year. 3, need to force mktime to use GMT as it's timezone. That should take care of crossing DST boundaries
Glen
Einstein: Got any suggestions as to locations I could 'steal' some working code?
Glen
Glen, thanks for review. Now I realize that standard C++ is too difficult to me these days. I'd propose something of higher level of abstraction than time_t. Regarding place to steal the code - Boost looks the most evident candidate.
Konstantin Spirin