[Python-Dev] dateutil

Gustavo Niemeyer niemeyer at conectiva.com
Sun Mar 14 19:13:46 EST 2004


Tim, first, thank you very much for your careful review of the
relativedelta documentation. It looks like most of these issues
are result of the documentation being written by someone which
already knew how things work out, and was deeply involved in the
development. I'll try to answer your doubts here, and later I'll
review the documentation again trying to explain these issues in
a better, non-ambiguous way.

> The docs aren't clear enough to predict what the code may or
> may not do.  I can't make time for an exhaustive review, so
> I'll just stop after pointing out ambiguities in the
> relativedelta docs:
> 
> And the other way is to use any of the following keyword arguments:
> 
>     year, month, day, hour, minute, seconds, microseconds
>     Absolute information.
> 
> Why are only the last two plural?

They are not plural. This is a bug in the documentation.

> What does absolute *mean*?

Absolute mean *set* date/time to the given parameter. Relative mean
*add* the given parameter to the date/time.

>>> d
datetime.datetime(2004, 4, 4, 0, 0)
>>> d+relativedelta(month=1)
datetime.datetime(2004, 1, 4, 0, 0)
>>> d+relativedelta(months=1)
datetime.datetime(2004, 5, 4, 0, 0)

> What type(s) are these things?

They're integers.

> years, months, weeks, days, hours, minutes, seconds, microseconds
> Relative information, may be negative.
> 
> How can "seconds" and "microseconds" be both absolute and
> relative?

They're not. It's a documentation bug.

> What type(s)?

They're integers.

> What ranges?  For example, does days=+1000 make sense?

Yes.. -1000 as well. "relative" information will be "added" to
the given date, so any integer makes sense. For example:

>>> d+relativedelta(days=1000)
datetime.datetime(2006, 12, 30, 0, 0)

Absolute information may receive any integer as well, but while
being applied on a date, the values will be rounded to the
minimum acceptable value. For example:

>>> d+relativedelta(day=1000)
datetime.datetime(2004, 4, 30, 0, 0)

>     weekday
>     One of the weekday instances (MO, TU, etc).  These instances may
>     receive a parameter n, specifying the nth weekday, which could be
>     positive or negative (like MO(+2) or MO(-3).

(awful explanation indeed)

> Does MO(+42) make sense?  MO(-42)?

Yes, both make sense.

>>> date(2004,1,1)+relativedelta(weekday=MO(+200))
datetime.date(2007, 10, 29)
>>> date(2004,1,1)+relativedelta(weekday=MO(-200))
datetime.date(2000, 3, 6)

> At this point it's not clear what MO(+2) or MO(-3) may mean
> either.

They mean "the monday after the next/current monday" and "two mondays
before the last/current monday" respectively.

> What about MO(0)?  Or is there "a hole" at 0 in this scheme?
> Is MO(+2) different than MO(2)?

MO(0) shouldn't be used as it makes no sense, but is the same
as MO(+1) which is the same as MO(1). The '+' in the
documentation is just to give the idea of 'next'.

>     Not specifying it is the same as specifying +1.
> 
> Not exactly clear what "it" means.  Assuming it means the argument n.
> 
>     You can also use an integer, where 0=MO.
> 
> Meaning that MO(0) is the same as MO(MO)?  Or that you can say weekday=0?
> If the latter, what's the full set of allowed integers?

Meaning weekday=0. This is for compatibility with the notation
already in use in the tuple notation, in the "calendar" module,
and in the weekday() method of date/datetime objects.

The range is:

>>> calendar.MONDAY, calendar.SUNDAY
(0, 6)

Notice that you're unable to specify MO(+2) using this notation.

>     Notice that, for example, if the calculated date is already Monday,
>     using MO or MO(+1) (which is the same thing in this context), won't
>     change the day.
> 
> So we know that if the calculated date is a Monday, then if
> 
>     weekday=MO
> or
>     weekday=MO(+1)
> 
> were specified then they won't change the day.  It hasn't
> explained what any other value means.

Basically, it should be explained in the documentation that
"today" counts.

>     leapdays
>     Will add given days to the date found, but only if the computed
>     year is a leap year and the computed date is post 28 of february.
> 
> Couldn't follow this one at all.  Is this a Boolean argument?  An integer?
> The explanation below makes it sound like a bool, but the "add given days"
> above makes it sound like an int.  If integer, what range?  What use is
> this?

The expected type is an integer. This is mainly used to implement
nlyearday.

>     yearday, nlyearday
>     Set the yearday or the non-leap year day (jump leap days). These are
>     converted to day/month/leapdays information.
> 
> "jump leap days" doesn't mean anything to me, and neither does "yearday".

yearday means the number of days since the year started. For
example:

>>> time.localtime().tm_yday
74
>>> date(2004,1,1)+relativedelta(yearday=74)
datetime.date(2004, 3, 14)
>>> date.today()
datetime.date(2004, 3, 14)

"non-leap yearday" means the number of days since the year
started, ignoring the leap-day. For example:

>>> date(2004,1,1)+relativedelta(nlyearday=74)
datetime.date(2004, 3, 15)

> Are "yearday" and "nlyearday" mutually exclusive, or can you
> specifiy both?

Yes, they're mutually exclusive, since nlyearday and yearday
are both converted to day/month/leapdays:

>>> relativedelta(nlyearday=74)
relativedelta(month=3, day=15)
>>> relativedelta(yearday=74)
relativedelta(leapdays=-1, month=3, day=15)

> What types, and ranges, make sense for them?  What do they
> mean?

1-366 make sense for them.

>     If you're curious about exactly how the relative delta will act on
>     operations, here is a description of its behavior.
> 
> What are the supported operations?  The docs mention
> 
>     relativedelta(datetime1, datetime2)
> 
> and
> 
>     datetime1 = datetime2 + relativedelta(datetime1, datetime2)
> 
> The explanation below doesn't seem to make any sense for the first of those,
> so it must be explaining the second.

I don't understand your sentence above. The first of those is
just an instantiation of a relativedelta, while the second one is
an instantiation and an operation being made at the same time.

> Are these the *only* supported operations?  For example,
> datetime.timedelta also supports multiplying timedelta by an
> integer.

Yes, there are other operations supported as well, and should
be documented.

>     1. Calculate the absolute year, using the year argument, or the
>        original datetime year, if the argument is not present.
> 
> Are there range restrictions on these?  If so, what happens if they go out
> of bounds?

datetime will tell what'll happen, since it has specific range limitations:

>>> datetime.now()+relativedelta(year=10000)
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
  File "/home/niemeyer/src/dateutil/dateutil/relativedelta.py", line 261, in __radd__
    day = min(calendar.monthrange(year, month)[1],
  File "/usr/lib/python2.3/calendar.py", line 101, in monthrange
    day1 = weekday(year, month, 1)
  File "/usr/lib/python2.3/calendar.py", line 94, in weekday
    return datetime.date(year, month, day).weekday()
ValueError: year is out of range

>     2. Add the relative years argument to the absolute year.
> 
> I *assume* that the relative years added here is 0 if no years= was
> specified.  Same questions about range restrictions.

Correct about relative years. Same answers about ranges.

>     3. Do steps 1 and 2 for month/months.
> 
> Same questions, but more acutely, as it's obviously possible for even simple
> arguments like months=-2 to lead to a calculated month outside range(1, 13).
> What happens then?

About month, it only makes sense in the 1-12 range.

About months, it will do the "correct thing":

>>> datetime(2004,1,1)-relativedelta(months=1)
datetime.datetime(2003, 12, 1, 0, 0)
>>> datetime(2004,1,1)+relativedelta(months=-1)
datetime.datetime(2003, 12, 1, 0, 0)

>     4. Calculate the absolute day, using the day argument, or the
>        original datetime day, if the argument is not present.  Then,
>        subtract from the day until it fits in the year and month found
>        after their operations.
> 
>     5. Add the relative days argument to the absolute day.
> 
> What if the resulting day doesn't "fit in the year and month found" so far?

In this step it will just add the number of days, as timedelta would do.

>       Notice that the weeks argument is multiplied by 7 and added
>       to days.
> 
>    6. If leapdays is present, the computed year is a leap year, and
>       the computed month is after february, remove one day from the
>       found date.
> 
> So leapdays=True and leapdays=False and leapdays=45 and leapdays=None all
> mean the same thing?

Oops. Bug in the documentation. leapdays is a "relative" integer.

>    7. Do steps 1 and 2 for hour/hours, minute/minutes, second/seconds,
>       microsecond/microseconds.
> 
> All the same questions about types, ranges, and what happens when results
> are out of range.  If I understood what the rest of it intended, I suspect
> I'd also wonder what happens if leapdays=True, the computed date after #6 is
> February 1, and hours=24*60 was passed.  *Presumably* 60 days get added to
> the date then, moving the result well beyond February, but then the presence
> of leapdays *doesn't* "remove one day"?

Correct (assuming leapdays=-1).

>    8. If the weekday argument is present, calculate the nth occurrence of
>       the given weekday.
> 
> Where +1 means "don't move if you're already there, but move ahead to the
> closest succeeding if you're not"?  -1 means "move to closest preceding, and
> regardless of whether you're already there"?  0 is an error, or means the
> same as +1, or ...?

-1 and +1 have the same behavior. Go to the last/next given weekday,
with the current day counting. 0 is not supposed to be used in this
context, but means the same as +1.

> That was the end of the explanation, and "yearday" and "nlyearday" weren't
> mentioned again.

Notice in the explanation of yearday and nlyearday that they're
converted to month/day/leapdays.

> None of this is meant to imply that the functionality isn't useful -- it's
> that the functionality isn't specified clearly enough to know what it is.

Completely agreed. As I said, this is clearly my fault, as I've
written the documentation while developing the module, and thus
was aware about many details that a person which has never seen
it, nor studied other documents, wouldn't aware.

> Is it true that adding relativedelta(months=+1) 12 times isn't necessarily
> the same as adding relativedelta(years=+1) once?  (I picture starting with
> 31 January, landing on Feb 28 or 29 after one addition of months=+1, and
> then sticking on 28 or 29 thereafter.)

They land on the same date. While the documentation looks somewhat
confusing, the implementation is not. For example:

>>> date(2000,2,29)+relativedelta(months=+12)
datetime.date(2001, 2, 28)
>>> date(2000,2,29)+relativedelta(years=+1)
datetime.date(2001, 2, 28)
>>> date(2000,2,29)+relativedelta(years=-1)
datetime.date(1999, 2, 28)
>>> date(2000,2,29)+relativedelta(months=-12)
datetime.date(1999, 2, 28)

>>> rd = relativedelta(months=+1)
>>> d = date(2000,2,29)
>>> for i in range(12):
...   d += rd
...
>>> d
datetime.date(2001, 2, 28)

>>> rd = relativedelta(months=+1)
>>> d = date(2000,2,29)
>>> for i in range(12):
...   d -= rd
...
>>> d
datetime.date(1999, 2, 28)

Again, thank you very much!

-- 
Gustavo Niemeyer
http://niemeyer.net



More information about the Python-Dev mailing list