Skip to content

Instantly share code, notes, and snippets.

@jacobian
Forked from zacharyvoase/daterange.py
Created August 10, 2009 18:09
Show Gist options
  • Save jacobian/165321 to your computer and use it in GitHub Desktop.
Save jacobian/165321 to your computer and use it in GitHub Desktop.
# -*- 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