Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rrule bug when start date is month end and frequency is monthly #149

Closed
broncos168 opened this issue Nov 10, 2015 · 7 comments
Closed

rrule bug when start date is month end and frequency is monthly #149

broncos168 opened this issue Nov 10, 2015 · 7 comments

Comments

@broncos168
Copy link

Please bear me. I am new to Python and this is my first time post in github. Thanks.

Code snippet to demo the bug:

from datetime import date
from dateutil import rrule
from dateutil import relativedelta

start_date=date(2014, 12, 31)  # date(2014, 12, 29/30/31) 
end_date=date(2015, 12, 31)
freq = rrule.MONTHLY
interval = 1
dts = list(rrule.rrule(freq, interval=interval, dtstart=start_date, until=end_date))
for dt in dts:
    print(dt.date().isoformat())

In this edge case, outputs are not monthly

2014-12-31
2015-01-31
2015-03-31
2015-05-31
2015-07-31
2015-08-31
2015-10-31
2015-12-31
d1=date(2015, 2, 28)
d2=date(2015, 5, 31)
rel_delta1 = relativedelta.relativedelta(d2, d1)
rel_delta2 = relativedelta.relativedelta(d1, d2)
print(rel_delta1, rel_delta2)

Output:

relativedelta(months=+3, days=+3) relativedelta(months=-3)
rel_delta1 is not the negative of rel_delta2, vice verse
@pganssle pganssle added this to the 2.5.1 milestone Feb 18, 2016
@phep
Copy link

phep commented Feb 19, 2016

Hi,
While the behaviour is admittedly quite disconcerting, I'm not quite sure this is a valid bug since RFC5545 says:

Recurrence rules may generate recurrence instances with an invalid
date (e.g., February 30) or nonexistent local time (e.g., 1:30 AM
on a day where the local time is moved forward by an hour at 1:00
AM).  Such recurrence instances MUST be ignored and MUST NOT be
counted as part of the recurrence set.

The current behaviour respects the RFC.

Maybe there might be some parameter added to let the user decides which behaviour s/he's expecting (be strict and exclude, or floor/ceil the occurence).
I'm just discovering dateutil so this might have already been implemented somehow.

@pganssle
Copy link
Member

@phep Good call. I don't think it's necessary to add any sort of user option for this. If I recall correctly, if you want to get the last day of the month, you can use bymonth=-1. I'll double check that, though.

@phep
Copy link

phep commented Feb 20, 2016

Yes, it is true that one can use rrules to express offset from start or end of month, but this is not necessarily semantically equivalent to the given example.

Let's say you sell some online service to which customers may subscribe on a monthly basis and the terms are computed from day of month to day of month, i.e. if a customer subscribes the 15th of October, the next payment will occur on the 15th of November.

How to deal with the cases where customers first subscribe on the 29, 30 or 31? Presently we are forced to test this in the code to adjust the term date to the month duration, some way or another. Wouldn't it be nice if we'd just have to write something similar to rrule(MONTHLY, dtstart=today, adjust_end_of_month=True)?

@pganssle
Copy link
Member

@phep I'll have to think about this. I can't imagine that iCal doesn't have this problem and given that they expressly thought about it, I imagine they have a way to handle it.

I'm hesitant to start moving beyond the RFC. It may cause problems with the conversion between rrule and rrulestr.

In the relatively simple situation you describe, I'd probably just not use an RRULE at all:

from dateutil.relativedelta import relativedelta
from datetime import datetime
from itertools import count

dtstart = datetime(2015, 1, 31)
interval = 1
MONTH = relativedelta(months=1)

rr = (dtstart + x * MONTH for x in count(1, interval))

print([next(rr) for x in range(5)])

Output:

[datetime.datetime(2015, 2, 28, 0, 0),
 datetime.datetime(2015, 3, 31, 0, 0),
 datetime.datetime(2015, 4, 30, 0, 0),
 datetime.datetime(2015, 5, 31, 0, 0),
 datetime.datetime(2015, 6, 30, 0, 0)]

That said, obviously you lose the general functionality of an rrule by doing that, so it's worth thinking about expressing the same thing as an RRULE.

@phep
Copy link

phep commented Feb 20, 2016

@pganssle, going back to the problem, I think you nailed it:

I'm hesitant to start moving beyond the RFC

"Special cases aren't special enough to break the rules." the zen of Python says.

The RFC is not perfect but diverging from it would just bring less perfection, or, in this case, more uncertainty.

Yet the strangeness reported by @broncos168 might deserve some nota bene in the documentation.

@pganssle
Copy link
Member

Resolved by #213

@NcsMA
Copy link

NcsMA commented Jan 6, 2023

Hi,

I've been trying to execute what @pganssle said, it's works when we put as entry january 31, but if I change it to abril it's bring back the errors on the last day of the months:

image

the return is:

image

Can someone help to solve that in a better way?

I got how to solve buts it's looks like:

    data inicio = 01/01/2023
    #creates a list that starts in the begining of 2020/05
    lista_datas_inicio = ['01/05/2020']
    inicio_ciclo = '01/05/2020'
    inicio_ciclo = datetime.strptime(inicio_ciclo, '%d/%m/%Y').date()
    while inicio_ciclo < data_inicio:
        inicio_ciclo = (inicio_ciclo) + relativedelta(months=+1)
        entrada = inicio_ciclo.strftime('%d/%m/%Y')
        lista_datas_inicio.append(entrada)
     print(lista_datas_inicio)

    lista_datas_fim = []
    for item in lista_datas_inicio:
        item = datetime.strptime(item, '%d/%m/%Y').date()
        tamanho_mes = calendar.monthrange(item.year,item.month)
        ultimo_dia = tamanho_mes[1]
        data_fim = f"{ultimo_dia}/{str(item.month).zfill(2)}/{item.year}"
        data_fim = datetime.strptime(data_fim, '%d/%m/%Y').date()
        entrada = data_fim.strftime('%d/%m/%Y')
        lista_datas_fim.append(entrada)
     print(lista_datas_fim)

it's gets the list of first days of the month from the period and changes the day for which date using calendar.monthyrange()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants