Subtract n months from datetime

38 views
Skip to first unread message

Paulo da Silva

unread,
Jun 21, 2022, 12:30:13 AMJun 21
to
Hi!

I implemented a part of a script to subtract n months from datetime.
Basically I subtracted n%12 from year and n//12 from the month adding 12
months when it goes<=0. Then used try when converting to datetime again.
So, if the day is for example 31 for a 30 days month it raises a
ValuError exception. Then I subtract 1 to day and repeat.

The code seems too naive and very very complicated!
What is the best way to achieve this? Any existent module?

At the very end, what I want is to subtract nx where x can be y, m, w, d
for respectively years, months, weeks or days.

I feel I am missing something here ...

Thanks.
Paulo

Paul Bryan

unread,
Jun 21, 2022, 12:45:13 AMJun 21
to
Here's how my code does it:


import calendar

def add_months(value: date, n: int):
  """Return a date value with n months added (or subtracted if
negative)."""
  year = value.year + (value.month - 1 + n) // 12
  month = (value.month - 1 + n) % 12 + 1
  day = min(value.day, calendar.monthrange(year, month)[1])
  return date(year, month, day)

Paul

Paul Rubin

unread,
Jun 21, 2022, 12:46:20 AMJun 21
to
Paulo da Silva <p_d_a_s_i...@nonetnoaddress.pt> writes:
> I implemented a part of a script to subtract n months from datetime.

Since months don't always have the same lengths, maybe you want to
subtract some number of days, like 90 days instead of 3 months? Do that
by making a timedelta and subtracting it from the datetime. Otherwise,
as you say, how are you supposed to subtract 1 month from July 31,
since there is no June 31?

Richard Damon

unread,
Jun 21, 2022, 8:00:27 AMJun 21
to
The biggest issue with "subtracting months" is getting the right
definition of what you mean by that, especially in the corner cases,
once that is established, programming it is fairly easy.

The problem is that a month isn't a fixed unit of time, but is a period
anywhere from 28 to 31 days. (you get the same problem for years, but
the difference is more special case, the presence or absent of Feb 29th.)

The normal definition of this operation has the strange property that if
you subtract a month, then add a month, you sometimes don't get back to
the same day as you started with. Also subtracting one month, and then
subtracting another month might get you a different day than subtracting
2 months at once (Think of Mar 31st).

In short, this sort of date operation IS hard, and application specific,
so while there may be pre-built modules that have this operation, you
need to see if it uses a compatible definition of what you want.

One alternative, which breaks other expectations, is to think of a month
as 30 or 30.5 (so 2 months are 61 days) days, and add that. It says that
often a month later than a given day isn't the same day of the month,
but does make some operations less surprising. (This is hard to do to a
date expressed as year-month-day, but trivial in some other formats like
a timestamp.)

--
Richard Damon

Dan Stromberg

unread,
Jun 21, 2022, 10:06:48 AMJun 21
to
On Mon, Jun 20, 2022 at 9:45 PM Paul Bryan <pbr...@anode.ca> wrote:

> Here's how my code does it:
>
>
> import calendar
>
> def add_months(value: date, n: int):
> """Return a date value with n months added (or subtracted if
> negative)."""
> year = value.year + (value.month - 1 + n) // 12
> month = (value.month - 1 + n) % 12 + 1
> day = min(value.day, calendar.monthrange(year, month)[1])
> return date(year, month, day)
>

This looks interesting.

You also could add or subtract the average number of seconds in a month:
2629743.75

This has the strange property that the time of day, or even calendar day,
could change. However, it is round-trippable.

Paulo da Silva

unread,
Jun 21, 2022, 12:02:49 PMJun 21
to
Às 05:44 de 21/06/22, Paul Bryan escreveu:
> Here's how my code does it:
>
>
> import calendar
>
> def add_months(value: date, n: int):
>   """Return a date value with n months added (or subtracted if
> negative)."""
>   year = value.year + (value.month - 1 + n) // 12
>   month = (value.month - 1 + n) % 12 + 1
>   day = min(value.day, calendar.monthrange(year, month)[1])
>   return date(year, month, day)
>
> Paul
I have a datetime, not a date.
Anyway, the use of calendar.monthrange simplifies the task a lot.

Assuming dtnow has the current datetime and dtn the number of months to
be subtracted, here is my solution (the code was not cleaned yet - just
a test):
dtnow_t=list(dtnow.timetuple()[:6]+(dtnow.microsecond,))
y=dtnow_t[0] # y,m,d,*_=dtnow_t seems slower
m=dtnow_t[1]
d=dtnow_t[2]
dy,dm=divmod(dtn,12)
y-=dy
m-=dm
if m<1:
m+=12
y-=1
daysinmonth=calendar.monthrange(y,m)[1]
d=min(d,daysinmonth)
dtnow_t[0]=y
dtnow_t[1]=m
dtnow_t[2]=d
bt=datetime.datetime(*dtnow_t)

Any comments are welcome.

Thank you.
Paulo

Paul Rubin

unread,
Jun 21, 2022, 4:50:09 PMJun 21
to
Paulo da Silva <p_d_a_s_i...@nonetnoaddress.pt> writes:
> Any comments are welcome.

Richard Damon makes the astute comment that adding a month and then
subtracting it again could fail to put you back where you started. That
doesn't seem good.

Question: why is it that you want to do this thing, of adding and
subtracting months? Do you have an application that requires it? Does
the application precisely specify what adding and subtracting months is
supposed to mean? If it is for some business purpose, like figuring out
expiration dates of something, you really have to get this clarified by
the project owner, who might not have thought things through enough.
You can't just rely on your own judgment to figure out what makes the
most sense. Someone else's presumptions might be different and they can
get bitten by the disconnect.

You're much better off adding and subtracting days rather than months.
Those at least are usually all the same length, and nobody is likely to
care about leap seconds either way.

Cameron Simpson

unread,
Jun 21, 2022, 6:20:19 PMJun 21
to
On 21Jun2022 17:02, Paulo da Silva <p_d_a_s_i...@nonetnoaddress.pt> wrote:
>I have a datetime, not a date.

Then you need a date. I would break the datetime into a date and a time,
then do the months stuff to the date, then compose a new datetime from
the result.

>Anyway, the use of calendar.monthrange simplifies the task a lot.

Hmm, yes it would.

The important thing to remember about any solutions mentioned is that
dates and datetimes have different semantics. Specificly, you can't add
fixed elapsed times such as seconds to do "calendar like" arithmetic,
which works in days etc because months have varying numbers of days, and
days have varying numbers of seconds (not merely the odd leap second but
also the horrors of timezones and summer/winter time shifts).

So working with the calendar component (days upwards) is a meaningful
thing. But working in, say, seconds with the _bjective_ of doing days or
months is nearly pointless.

Cheers,
Cameron Simpson <c...@cskk.id.au>

Paulo da Silva

unread,
Jun 22, 2022, 12:59:24 PMJun 22
to
Às 05:29 de 21/06/22, Paulo da Silva escreveu:

As a general response to some comments ...

Suppose we need to delete records from a database older than ...
Today, it's usual to specify days. For example you have to keep some gov
papers for 90 days. This seems to come from computers era. In our minds,
however, we immediately think 90 days=3 months.
For example, one may want to delete some files older than 9 months. It's
far more intuitive than 270 days.
When we talk about years it is still going. For example I need to keep
my receipts for 5 years because IRS audits.
Accepting this, it's intuitive, for example, that 3 months before July,
31 is April, 30.
The same happens for the years. 5 years before February, 29 is February, 28.

Again, this is my opinion and that's the way I like it :-)
Regards
Paulo

MRAB

unread,
Jun 22, 2022, 1:49:23 PMJun 22
to
What makes sense depends on where you're looking from.

It's 28 February, you need to keep it for 5 years, therefore you could
reason that you can dispose of it on 28 February, 5 years hence.

However, that happens to be a leap year.

Should you still have it on 29 February?

Marco Sulla

unread,
Jun 22, 2022, 2:48:36 PMJun 22
to
The package arrow has a simple shift method for months, weeks etc

https://arrow.readthedocs.io/en/latest/#replace-shift

Barry Scott

unread,
Jun 22, 2022, 3:35:48 PMJun 22
to
The advantage of 30 days, 90 days etc is that a contract or law does not need to tell you
how to deal with the problems of calendar months.

As you say in peoples thoughts that 1 month or 3 months etc. But an accounts department
will know how to to the number of days till they have to pay up.

Barry


>
> Again, this is my opinion and that's the way I like it :-)
> Regards
> Paulo
> --
> https://mail.python.org/mailman/listinfo/python-list

MRAB

unread,
Jun 22, 2022, 4:55:31 PMJun 22
to
On 2022-06-22 20:25, Barry Scott wrote:
>
>
>> On 22 Jun 2022, at 17:59, Paulo da Silva <p_d_a_s_i...@nonetnoaddress.pt> wrote:
>>
> The advantage of 30 days, 90 days etc is that a contract or law does not need to tell you
> how to deal with the problems of calendar months.
>
> As you say in peoples thoughts that 1 month or 3 months etc. But an accounts department
> will know how to to the number of days till they have to pay up.
>
OT, but in the UK, when the Gregorian Calendar was adopted, there were
complaints.

It's often believed that they were just being superstitious about
"losing" 11 days, but the truth is that they were complaining that rent
was paid by the month, but wages by the number of days worked.

That month was a lot shorter, with far fewer working days, yet they were
still expected to pay the same rent!

Paulo da Silva

unread,
Jun 22, 2022, 8:56:35 PMJun 22
to
Às 20:25 de 22/06/22, Barry Scott escreveu:
>
>
>> On 22 Jun 2022, at 17:59, Paulo da Silva <p_d_a_s_i...@nonetnoaddress.pt> wrote:
>>
>> Às 05:29 de 21/06/22, Paulo da Silva escreveu:
>>
>> As a general response to some comments ...
>>
>> Suppose we need to delete records from a database older than ...
>> Today, it's usual to specify days. For example you have to keep some gov papers for 90 days. This seems to come from computers era. In our minds, however, we immediately think 90 days=3 months.
>> For example, one may want to delete some files older than 9 months. It's far more intuitive than 270 days.
>> When we talk about years it is still going. For example I need to keep my receipts for 5 years because IRS audits.
>> Accepting this, it's intuitive, for example, that 3 months before July, 31 is April, 30.
>> The same happens for the years. 5 years before February, 29 is February, 28.
>
> The advantage of 30 days, 90 days etc is that a contract or law does not need to tell you
> how to deal with the problems of calendar months.
>
> As you say in peoples thoughts that 1 month or 3 months etc. But an accounts department
> will know how to to the number of days till they have to pay up.
>
Yes. But my point is to justify why I want months. And it depends on the
application.
Let's suppose a program for Joe User to clean something - files, for
example. There are no rules except for the comfort of the user. He would
prefer to be able to say 9 months back instead of 270 days. And by 9
months, he expects to count down 9 months. Not 270 days.
That's what happens with the script I am writing.

Paulo

Paulo da Silva

unread,
Jun 22, 2022, 9:00:38 PMJun 22
to
Às 19:47 de 22/06/22, Marco Sulla escreveu:
> The package arrow has a simple shift method for months, weeks etc
>
> https://arrow.readthedocs.io/en/latest/#replace-shift

At first look it seems pretty good! I didn't know it.
Thank you Marco.

Paulo

Karsten Hilbert

unread,
Jun 23, 2022, 2:36:33 AMJun 23
to
> What makes sense depends on where you're looking from.
>
> It's 28 February, you need to keep it for 5 years, therefore you could
> reason that you can dispose of it on 28 February, 5 years hence.
>
> However, that happens to be a leap year.
>
> Should you still have it on 29 February?

Nope because that's *after* the 5 years (they end Feb 28).

If it originates on March 1st, however, you shouldn't dispose of it on Feb 29th just yet.

Karsten

Peter J. Holzer

unread,
Jun 27, 2022, 6:51:06 AMJun 27
to
On 2022-06-21 05:29:52 +0100, Paulo da Silva wrote:
> I implemented a part of a script to subtract n months from datetime.
> Basically I subtracted n%12 from year and n//12 from the month adding 12
> months when it goes<=0. Then used try when converting to datetime again. So,
> if the day is for example 31 for a 30 days month it raises a ValuError
> exception. Then I subtract 1 to day and repeat.

For a recent longish discussion of that matter see the threads starting
at https://mail.python.org/pipermail/python-list/2022-April/905985.html
and https://mail.python.org/pipermail/python-list/2022-April/906045.html
(the latter also contains some prototype code).

(I apologize for not pursuing that further at the time. I wanted to
bolster that case with some real world applications, but I was a bit
swamped with Real Work™ and didn't find anything suitable.)

hp

--
_ | Peter J. Holzer | Story must make more sense than reality.
|_|_) | |
| | | h...@hjp.at | -- Charles Stross, "Creative writing
__/ | http://www.hjp.at/ | challenge!"
signature.asc
Reply all
Reply to author
Forward
0 new messages