easy way to add 1 month to a time_t in C/C++
Asked Answered
L

4

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

Langbehn answered 8/1, 2009 at 11:29 Comment(6)
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.Dunning
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/2009Ingaborg
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.Ingaborg
Subtle: 30/1/2009 + 1 month + 1 month = 28/3/2009 !Horseflesh
@Horseflesh Even more subtle: 30/1/2009 +1 month - 1 month = 28/1/2009. This seems like a bad way to define months.Allograph
@Aaron, boost date add_month just performs in that way. I think it's strange.Onassis
P
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);
}
Priapitis answered 8/1, 2009 at 12:51 Comment(6)
I think you want to change GetDaysInMonth to check if IsLeapYear(year) AND if month is FebChandlery
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.Checky
hamishmcn, thanks. That's how Microsoft developed software for Zune :)Priapitis
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 boundariesLangbehn
Einstein: Got any suggestions as to locations I could 'steal' some working code?Langbehn
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.Priapitis
H
6

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.

Horseflesh answered 8/1, 2009 at 11:39 Comment(1)
This doesn't work quite like the oracle function. Using the above 30-01-2009 becomes 02-03-2009Langbehn
C
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

Columniation answered 8/1, 2009 at 11:48 Comment(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.Dunning
It seems reasonable to me as well. Unfortunately we don't use Boost and won't be in the near future. :-(Langbehn
D
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.

Dubbin answered 20/7, 2015 at 3:32 Comment(6)
You should disclaim in the answer that it's your library. :)Nil
@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.Dubbin
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 etcNil
@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.Monson
Does your library also provide user-defined literals like wk, mo and yr?Prudential
@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.Dubbin
P
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);
}
Priapitis answered 8/1, 2009 at 12:51 Comment(6)
I think you want to change GetDaysInMonth to check if IsLeapYear(year) AND if month is FebChandlery
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.Checky
hamishmcn, thanks. That's how Microsoft developed software for Zune :)Priapitis
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 boundariesLangbehn
Einstein: Got any suggestions as to locations I could 'steal' some working code?Langbehn
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.Priapitis

© 2022 - 2024 — McMap. All rights reserved.