allow daterange to run backwards as well as forwards. greatly enhance daterange's docstring. make stop a required argument (though None will still work). user operator module instead of lambdas for clarity.

This commit is contained in:
Mahmoud Hashemi 2016-05-21 00:26:06 -07:00
parent a0c2c992d9
commit 294344b84f
2 changed files with 54 additions and 27 deletions

View File

@ -23,6 +23,7 @@ degree of accuracy in corner cases, check out `pytz`_.
import re
import time
import bisect
import operator
from datetime import tzinfo, timedelta, date, datetime
@ -257,11 +258,13 @@ def strpdate(string, format):
Args:
string (str): The date string to be parsed.
format (str): The `strptime()`-style date format string.
format (str): The `strptime`_-style date format string.
Returns:
datetime.date
>>> strpdate('20160214', '%Y%m%d')
.. _`strptime`: https://docs.python.org/2/library/datetime.html#strftime-strptime-behavior
>>> strpdate('2016-02-14', '%Y-%m-%d')
datetime.date(2016, 2, 14)
>>> strpdate('26/12 (2015)', '%d/%m (%Y)')
datetime.date(2015, 12, 26)
@ -274,13 +277,30 @@ def strpdate(string, format):
return whence.date()
def daterange(start, stop=None, step=1, inclusive=False):
"""Generator that yields a range of `date` objects.
def daterange(start, stop, step=1, inclusive=False):
"""In the spirit of :func:`range` and :class:`xrange`, the `daterange`
generator that yields a sequence of :class:`~datetime.date`
objects, starting at *start*, incrementing by *step*, until *stop*
is reached.
If `stop` is present, the final date produced will be the day before `stop`
(for consistency with the range() builtin). When `inclusive` is True, the
final date will be `stop`. By default, `step` is one day. Pass an `int`
(for days) or a :class:`timedelta` object to change this.
When *inclusive* is True, the final date may be *stop*, **if**
*step* falls evenly on it. By default, *step* is one day. See
details below for many more details.
Args:
start (datetime.date): The starting date The first value in
the sequence.
stop (datetime.date): The stopping date. By default not
included in return. Can be `None` to yield an infinite
sequence.
step (int): The value to increment *start* by to reach
*stop*. Can be an :class:`int` number of days, a
:class:`datetime.timedelta`, or a :class:`tuple` of integers,
`(year, month, day)`. Positive and negative *step* values
are supported.
inclusive (bool): Whether or not the *stop* date can be
returned. *stop* is only returned when a *step* falls evenly
on it.
>>> christmas = date(year=2015, month=12, day=25)
>>> boxing_day = date(year=2015, month=12, day=26)
@ -297,19 +317,16 @@ def daterange(start, stop=None, step=1, inclusive=False):
>>> for day in daterange(christmas, boxing_day):
... print(repr(day))
datetime.date(2015, 12, 25)
>>> for day in daterange(christmas, boxing_day, inclusive=True):
>>> for day in daterange(date(2017, 5, 1), date(2017, 8, 1),
... step=(0, 1, 0), inclusive=True):
... print(repr(day))
datetime.date(2015, 12, 25)
datetime.date(2015, 12, 26)
>>> for day in daterange(christmas, new_year, step=2):
... print(repr(day))
datetime.date(2015, 12, 25)
datetime.date(2015, 12, 27)
datetime.date(2015, 12, 29)
datetime.date(2015, 12, 31)
datetime.date(2017, 5, 1)
datetime.date(2017, 6, 1)
datetime.date(2017, 7, 1)
datetime.date(2017, 8, 1)
Care should be exercised when stop=None, as this will yield an infinite
sequence of dates:
*Be careful when using stop=None, as this will yield an infinite
sequence of dates.*
"""
if not isinstance(start, date):
raise TypeError("start expected datetime.date instance")
@ -328,19 +345,21 @@ def daterange(start, stop=None, step=1, inclusive=False):
else:
raise ValueError('step expected int, timedelta, or tuple'
' (year, month, day), not: %r' % step)
if stop is None:
finished = lambda t: False
elif inclusive:
finished = lambda t: t > stop
elif start < stop:
finished = operator.gt if inclusive else operator.ge
else:
finished = lambda t: t >= stop
finished = operator.lt if inclusive else operator.le
now = start
while not finished(now):
while not finished(now, stop):
yield now
if y_step or m_step:
m_y_step, cur_month = divmod(now.month + 1, 12)
m_y_step, cur_month = divmod(now.month + m_step, 12)
now = now.replace(year=now.year + y_step + m_y_step,
month=now.month + m_step)
month=cur_month or 12)
now = now + d_step
return

View File

@ -37,5 +37,13 @@ def test_daterange_years():
assert len(list(new_years_remaining)) == 22
y2025 = date(2025, 1, 1)
bakers_new_years_til_2025 = daterange(new_year, y2025, step=(1, 1, 0))
assert len(list(bakers_new_years_til_2025)) == 8
bakers_years_til_2025 = list(daterange(new_year, y2025, step=(1, 1, 0)))
assert len(bakers_years_til_2025) == 8
assert bakers_years_til_2025[-1] == date(2024, 8, 1)
assert bakers_years_til_2025[-1] == date(2024, 8, 1)
years_from_2025 = list(daterange(y2025, new_year, step=(-1, 0, 0),
inclusive=True))
assert years_from_2025[0] == date(2025, 1, 1)
assert years_from_2025[-1] == date(2017, 1, 1)