-
-
Save jacobian/165321 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- coding: utf-8 -*- | |
""" | |
Example Usage | |
============= | |
>>> import datetime | |
>>> start = datetime.date(2009, 6, 21) | |
>>> g1 = daterange(start) | |
>>> g1.next() | |
datetime.date(2009, 6, 21) | |
>>> g1.next() | |
datetime.date(2009, 6, 22) | |
>>> g1.next() | |
datetime.date(2009, 6, 23) | |
>>> g1.next() | |
datetime.date(2009, 6, 24) | |
>>> g1.next() | |
datetime.date(2009, 6, 25) | |
>>> g1.next() | |
datetime.date(2009, 6, 26) | |
>>> g2 = daterange(start, to=datetime.date(2009, 6, 25)) | |
>>> g2.next() | |
datetime.date(2009, 6, 21) | |
>>> g2.next() | |
datetime.date(2009, 6, 22) | |
>>> g2.next() | |
datetime.date(2009, 6, 23) | |
>>> g2.next() | |
datetime.date(2009, 6, 24) | |
>>> g2.next() | |
datetime.date(2009, 6, 25) | |
>>> g2.next() | |
Traceback (most recent call last): | |
... | |
StopIteration | |
>>> g3 = daterange(start, step='2 days') | |
>>> g3.next() | |
datetime.date(2009, 6, 21) | |
>>> g3.next() | |
datetime.date(2009, 6, 23) | |
>>> g3.next() | |
datetime.date(2009, 6, 25) | |
>>> g3.next() | |
datetime.date(2009, 6, 27) | |
>>> g4 = daterange(start, to=datetime.date(2009, 6, 25), step='2 days') | |
>>> g4.next() | |
datetime.date(2009, 6, 21) | |
>>> g4.next() | |
datetime.date(2009, 6, 23) | |
>>> g4.next() | |
datetime.date(2009, 6, 25) | |
>>> g4.next() | |
Traceback (most recent call last): | |
... | |
StopIteration | |
""" | |
import datetime | |
import re | |
def daterange(date, to=None, step=datetime.timedelta(days=1)): | |
""" | |
Similar to the built-in ``xrange()``, only for datetime objects. | |
If called with just a ``datetime`` object, it will keep yielding values | |
forever, starting with that date/time and counting in steps of 1 day. | |
If the ``to_date`` keyword is provided, it will count up to and including | |
that date/time (again, in steps of 1 day by default). | |
If the ``step`` keyword is provided, this will be used as the step size | |
instead of the default of 1 day. It should be either an instance of | |
``datetime.timedelta``, an integer, a string representing an integer, or | |
a string representing a ``delta()`` value (consult the documentation for | |
``delta()`` for more information). If it is an integer (or string thereof) | |
then it will be interpreted as a number of days. If it is not a simple | |
integer string, then it will be passed to ``delta()`` to get an instance | |
of ``datetime.timedelta()``. | |
Note that, due to the similar interfaces of both objects, this function | |
will accept both ``datetime.datetime`` and ``datetime.date`` objects. If | |
a date is given, then the values yielded will be dates themselves. A | |
caveat is in order here: if you provide a date, the step should have at | |
least a ‘days’ component; otherwise the same date will be yielded forever. | |
""" | |
if to is None: | |
condition = lambda d: True | |
else: | |
condition = lambda d: (d <= to) | |
if isinstance(step, (int, long)): | |
# By default, integers are interpreted in days. For more granular | |
# steps, use a `datetime.timedelta()` instance. | |
step = datetime.timedelta(days=step) | |
elif isinstance(step, basestring): | |
# If the string | |
if re.match(r'^(\d+)$', str(step)): | |
step = datetime.timedelta(days=int(step)) | |
else: | |
try: | |
step = delta(step) | |
except ValueError: | |
pass | |
if not isinstance(step, datetime.timedelta): | |
raise TypeError('Invalid step value: %r' % (step,)) | |
# The main generation loop. | |
while condition(date): | |
yield date | |
date += step | |
class delta(object): | |
""" | |
Build instances of ``datetime.timedelta`` using short, friendly strings. | |
``delta()`` allows you to build instances of ``datetime.timedelta`` in | |
fewer characters and with more readability by using short strings instead | |
of a long sequence of keyword arguments. | |
A typical (but very precise) spec string looks like this: | |
'1 day, 4 hours, 5 minutes, 3 seconds, 120 microseconds' | |
``datetime.timedelta`` doesn’t allow deltas containing months or years, | |
because of the differences between different months, leap years, etc., so | |
this function doesn’t support them either. | |
The parser is very simple; it takes a series of comma-separated values, | |
each of which represents a number of units of time (such as one day, | |
four hours, five minutes, et cetera). These ‘specifiers’ consist of a | |
number and a unit of time, optionally separated by whitespace. The units | |
of time accepted are (case-insensitive): | |
* Days ('d', 'day', 'days') | |
* Hours ('h', 'hr', 'hrs', 'hour', 'hours') | |
* Minutes ('m', 'min', 'mins', 'minute', 'minutes') | |
* Seconds ('s', 'sec', 'secs', 'second', 'seconds') | |
* Microseconds ('ms', 'microsec', 'microsecs' 'microsecond', | |
'microseconds') | |
If an illegal specifier is present, the parser will raise a ValueError. | |
This utility is provided as a class, but acts as a function (using the | |
``__new__`` method). This is so that the names and aliases for units are | |
stored on the class object itself: as ``UNIT_NAMES``, which is a mapping | |
of names to aliases, and ``UNIT_ALIASES``, the converse. | |
""" | |
UNIT_NAMES = { | |
## unit_name: unit_aliases | |
'days': 'd day'.split(), | |
'hours': 'h hr hrs hour'.split(), | |
'minutes': 'm min mins minute'.split(), | |
'seconds': 's sec secs second'.split(), | |
'microseconds': 'ms microsec microsecs microsecond'.split(), | |
} | |
# Turn `UNIT_NAMES` inside-out, so that unit aliases point to canonical | |
# unit names. | |
UNIT_ALIASES = {} | |
for cname, aliases in UNIT_NAMES.items(): | |
for alias in aliases: | |
UNIT_ALIASES[alias] = cname | |
# Make the canonical unit name point to itself. | |
UNIT_ALIASES[cname] = cname | |
def __new__(cls, string): | |
specifiers = (specifier.strip() for specifier in string.split(',')) | |
kwargs = {} | |
for specifier in specifiers: | |
match = re.match(r'^(\d+)\s*(\w+)$', specifier) | |
if not match: | |
raise ValueError('Invalid delta specifier: %r' % (specifier,)) | |
number, unit_alias = match.groups() | |
number, unit_alias = int(number), unit_alias.lower() | |
unit_cname = cls.UNIT_ALIASES.get(unit_alias) | |
if not unit_cname: | |
raise ValueError('Invalid unit: %r' % (unit_alias,)) | |
kwargs[unit_cname] = kwargs.get(unit_cname, 0) + number | |
return datetime.timedelta(**kwargs) | |
if __name__ == '__main__': | |
import doctest | |
doctest.testmod() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment