7

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

Deduplicator
  • 44,692
  • 7
  • 66
  • 118
Glen
  • 21,816
  • 3
  • 61
  • 76
  • 1
    As Mehrdad Afshari points out below, 28/02/2009 + 1 month = 31/03/2009 is not possible with a simple type. How do you know that 28/02/2009 is the "last day of the month" and not the "28th day of the month". It's simply not encoded in the type. You need a better representation. – Aaron Jan 08 '09 at 12:22
  • I think that 28/02/2009 + 1 month = 31/03/2009 is wrong. It should be 28/02/2009 + 1 month = 28/03/2009. From how .Net behaves, the only examples that don't result in the same day of the month are where the resulting month has less days than the original month. ie: 31/01/2009 + 1 month = 28/02/2009 – Kibbee Jan 08 '09 at 14:16
  • Now that I reread your examples it seems like what you are really looking for is how to look for the last day of the month, at some point in month in the future, which is a slightly different question. – Kibbee Jan 08 '09 at 14:18
  • Subtle: 30/1/2009 + 1 month + 1 month = 28/3/2009 ! – MSalters Jan 09 '09 at 13:04
  • @MSalters Even more subtle: `30/1/2009 +1 month - 1 month = 28/1/2009`. This seems like a bad way to define months. – nwp Jul 20 '15 at 13:29
  • @Aaron, boost date add_month just performs in that way. I think it's strange. – Zhang May 14 '19 at 04:29

4 Answers4

6

Really new answer to a really old question!

Using this free and open source library, and a C++14 compiler (such as clang) I can now write this:

#include "date.h"

constexpr
date::year_month_day
add(date::year_month_day ymd, date::months m) noexcept
{
    using namespace date;
    auto was_last = ymd == ymd.year()/ymd.month()/last;
    ymd = ymd + m;
    if (!ymd.ok() || was_last)
        ymd = ymd.year()/ymd.month()/last;
    return ymd;
}

int
main()
{
    using namespace date;
    static_assert(add(30_d/01/2009, months{ 1}) == 28_d/02/2009, "");
    static_assert(add(31_d/01/2009, months{ 1}) == 28_d/02/2009, "");
    static_assert(add(27_d/02/2009, months{ 1}) == 27_d/03/2009, "");
    static_assert(add(28_d/02/2009, months{ 1}) == 31_d/03/2009, "");
    static_assert(add(31_d/01/2009, months{50}) == 31_d/03/2013, "");
}

And it compiles.

Note the remarkable similarity between the actual code, and the OP's pseudo-code:

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

Also note that compile-time information in leads to compile-time information out.

C++20 <chrono> update:

Your C++ vendors are starting to ship C++20 <chrono> which can do this without a 3rd party library with nearly identical syntax.

Also note the somewhat unusual rules the OP requires for adding months, which is easily implementable in <chrono>:

If the resultant month overflows the day field or if the input month is the last day of the month, then snap the result to the end of the month.

#include <chrono>

constexpr
std::chrono::year_month_day
add(std::chrono::year_month_day ymd, std::chrono::months m) noexcept
{
    using namespace std::chrono;
    auto was_last = ymd == ymd.year()/ymd.month()/last;
    ymd = ymd + m;
    if (!ymd.ok() || was_last)
        ymd = ymd.year()/ymd.month()/last;
    return ymd;
}

int
main()
{
    using namespace std::chrono;
    static_assert(add(30d/01/2009, months{ 1}) == 28d/02/2009);
    static_assert(add(31d/01/2009, months{ 1}) == 28d/02/2009);
    static_assert(add(27d/02/2009, months{ 1}) == 27d/03/2009);
    static_assert(add(28d/02/2009, months{ 1}) == 31d/03/2009);
    static_assert(add(31d/01/2009, months{50}) == 31d/03/2013);
}

Demo.

Howard Hinnant
  • 206,506
  • 52
  • 449
  • 577
  • You should disclaim in the answer that it's your library. :) – Lightness Races in Orbit Jul 20 '15 at 03:43
  • @LightnessRacesinOrbit: If you click the link and look at the top line, is it not clear? I'm unsure how to follow your advice and not look awkward. But I'm open to suggestions. – Howard Hinnant Jul 20 '15 at 04:16
  • Can't help you there I'm afraid :) I have no problem with this answer but I know some of the meta police are obsessed with blatant disclosure etc – Lightness Races in Orbit Jul 20 '15 at 09:33
  • @HowardHinnant Using a less authoritative phrase can make it look a little less "imposed". However I don't see it that much problematic since it's something free and open source. – edmz Jul 20 '15 at 13:07
  • Does your library also provide user-defined literals like `wk`, `mo` and `yr`? – TemplateRex Jul 20 '15 at 19:52
  • 1
    @TemplateRex: `year`, yes, `2015_y`. `month`, yes, but not using user-defined literals. `jan`, 'feb`, etc. `weeks`, no. I couldn't settle on a good way to spell it. So `weeks{2}`. But there is `sun`, `mon`, `tue`, etc. for the weekdays. – Howard Hinnant Jul 20 '15 at 23:28
6

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
  • 17,435
  • 8
  • 50
  • 89
  • 2
    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 Jan 08 '09 at 12:31
  • It seems reasonable to me as well. Unfortunately we don't use Boost and won't be in the near future. :-( – Glen Jan 08 '09 at 13:03
5

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
  • 173,980
  • 10
  • 155
  • 350
  • This doesn't work quite like the oracle function. Using the above 30-01-2009 becomes 02-03-2009 – Glen Jan 08 '09 at 13:02
3

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
  • 20,609
  • 15
  • 72
  • 90
  • I think you want to change GetDaysInMonth to check if IsLeapYear(year) AND if month is Feb – hamishmcn Jan 08 '09 at 13:07
  • 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 Jan 08 '09 at 13:33
  • hamishmcn, thanks. That's how Microsoft developed software for Zune :) – Konstantin Spirin Jan 08 '09 at 14:11
  • 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 Jan 08 '09 at 14:22
  • Einstein: Got any suggestions as to locations I could 'steal' some working code? – Glen Jan 08 '09 at 14:23
  • 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 Jan 08 '09 at 14:37