├── .gitignore ├── setup.py ├── UNLICENSE ├── README.md └── daterange └── __init__.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .DS_Store 3 | MANIFEST 4 | dist/ 5 | build/ 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from distutils.core import setup 4 | 5 | 6 | setup( 7 | name='daterange', 8 | version='0.2', 9 | description='Like xrange(), but for datetime objects.', 10 | author='Zachary Voase', 11 | author_email='zacharyvoase@me.com', 12 | url='http://github.com/zacharyvoase/daterange', 13 | packages=['daterange'], 14 | ) 15 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `daterange` 2 | 3 | Like `xrange()`, but for `datetime` objects. 4 | 5 | ## Example Usage 6 | 7 | >>> import datetime 8 | >>> start = datetime.date(2009, 6, 21) 9 | 10 | >>> g1 = daterange(start) 11 | >>> g1.next() 12 | datetime.date(2009, 6, 21) 13 | >>> g1.next() 14 | datetime.date(2009, 6, 22) 15 | >>> g1.next() 16 | datetime.date(2009, 6, 23) 17 | >>> g1.next() 18 | datetime.date(2009, 6, 24) 19 | >>> g1.next() 20 | datetime.date(2009, 6, 25) 21 | >>> g1.next() 22 | datetime.date(2009, 6, 26) 23 | 24 | >>> g2 = daterange(start, to=datetime.date(2009, 6, 25)) 25 | >>> g2.next() 26 | datetime.date(2009, 6, 21) 27 | >>> g2.next() 28 | datetime.date(2009, 6, 22) 29 | >>> g2.next() 30 | datetime.date(2009, 6, 23) 31 | >>> g2.next() 32 | datetime.date(2009, 6, 24) 33 | >>> g2.next() 34 | datetime.date(2009, 6, 25) 35 | >>> g2.next() 36 | Traceback (most recent call last): 37 | ... 38 | StopIteration 39 | 40 | >>> g3 = daterange(start, step='2 days') 41 | >>> g3.next() 42 | datetime.date(2009, 6, 21) 43 | >>> g3.next() 44 | datetime.date(2009, 6, 23) 45 | >>> g3.next() 46 | datetime.date(2009, 6, 25) 47 | >>> g3.next() 48 | datetime.date(2009, 6, 27) 49 | 50 | >>> g4 = daterange(start, to=datetime.date(2009, 6, 25), step='2 days') 51 | >>> g4.next() 52 | datetime.date(2009, 6, 21) 53 | >>> g4.next() 54 | datetime.date(2009, 6, 23) 55 | >>> g4.next() 56 | datetime.date(2009, 6, 25) 57 | >>> g4.next() 58 | Traceback (most recent call last): 59 | ... 60 | StopIteration 61 | 62 | 63 | ## (Un)license 64 | 65 | This is free and unencumbered software released into the public domain. 66 | 67 | Anyone is free to copy, modify, publish, use, compile, sell, or distribute this 68 | software, either in source code form or as a compiled binary, for any purpose, 69 | commercial or non-commercial, and by any means. 70 | 71 | In jurisdictions that recognize copyright laws, the author or authors of this 72 | software dedicate any and all copyright interest in the software to the public 73 | domain. We make this dedication for the benefit of the public at large and to 74 | the detriment of our heirs and successors. We intend this dedication to be an 75 | overt act of relinquishment in perpetuity of all present and future rights to 76 | this software under copyright law. 77 | 78 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 79 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 80 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE 81 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 82 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 83 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 84 | 85 | For more information, please refer to 86 | -------------------------------------------------------------------------------- /daterange/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | Example Usage 5 | ============= 6 | 7 | >>> import datetime 8 | >>> start = datetime.date(2009, 6, 21) 9 | 10 | >>> g1 = daterange(start) 11 | >>> g1.next() 12 | datetime.date(2009, 6, 21) 13 | >>> g1.next() 14 | datetime.date(2009, 6, 22) 15 | >>> g1.next() 16 | datetime.date(2009, 6, 23) 17 | >>> g1.next() 18 | datetime.date(2009, 6, 24) 19 | >>> g1.next() 20 | datetime.date(2009, 6, 25) 21 | >>> g1.next() 22 | datetime.date(2009, 6, 26) 23 | 24 | >>> g2 = daterange(start, to=datetime.date(2009, 6, 25)) 25 | >>> g2.next() 26 | datetime.date(2009, 6, 21) 27 | >>> g2.next() 28 | datetime.date(2009, 6, 22) 29 | >>> g2.next() 30 | datetime.date(2009, 6, 23) 31 | >>> g2.next() 32 | datetime.date(2009, 6, 24) 33 | >>> g2.next() 34 | datetime.date(2009, 6, 25) 35 | >>> g2.next() 36 | Traceback (most recent call last): 37 | ... 38 | StopIteration 39 | 40 | >>> g3 = daterange(start, step='2 days') 41 | >>> g3.next() 42 | datetime.date(2009, 6, 21) 43 | >>> g3.next() 44 | datetime.date(2009, 6, 23) 45 | >>> g3.next() 46 | datetime.date(2009, 6, 25) 47 | >>> g3.next() 48 | datetime.date(2009, 6, 27) 49 | 50 | >>> g4 = daterange(start, to=datetime.date(2009, 6, 25), step='2 days') 51 | >>> g4.next() 52 | datetime.date(2009, 6, 21) 53 | >>> g4.next() 54 | datetime.date(2009, 6, 23) 55 | >>> g4.next() 56 | datetime.date(2009, 6, 25) 57 | >>> g4.next() 58 | Traceback (most recent call last): 59 | ... 60 | StopIteration 61 | 62 | """ 63 | 64 | import datetime 65 | import re 66 | 67 | 68 | def daterange(date, to=None, step=datetime.timedelta(days=1)): 69 | 70 | """ 71 | Similar to the built-in ``xrange()``, only for datetime objects. 72 | 73 | If called with just a ``datetime`` object, it will keep yielding values 74 | forever, starting with that date/time and counting in steps of 1 day. 75 | 76 | If the ``to_date`` keyword is provided, it will count up to and including 77 | that date/time (again, in steps of 1 day by default). 78 | 79 | If the ``step`` keyword is provided, this will be used as the step size 80 | instead of the default of 1 day. It should be either an instance of 81 | ``datetime.timedelta``, an integer, a string representing an integer, or 82 | a string representing a ``delta()`` value (consult the documentation for 83 | ``delta()`` for more information). If it is an integer (or string thereof) 84 | then it will be interpreted as a number of days. If it is not a simple 85 | integer string, then it will be passed to ``delta()`` to get an instance 86 | of ``datetime.timedelta()``. 87 | 88 | Note that, due to the similar interfaces of both objects, this function 89 | will accept both ``datetime.datetime`` and ``datetime.date`` objects. If 90 | a date is given, then the values yielded will be dates themselves. A 91 | caveat is in order here: if you provide a date, the step should have at 92 | least a ‘days’ component; otherwise the same date will be yielded forever. 93 | """ 94 | 95 | if to is None: 96 | condition = lambda d: True 97 | else: 98 | condition = lambda d: (d <= to) 99 | 100 | if isinstance(step, (int, long)): 101 | # By default, integers are interpreted in days. For more granular 102 | # steps, use a `datetime.timedelta()` instance. 103 | step = datetime.timedelta(days=step) 104 | elif isinstance(step, basestring): 105 | # If the string 106 | if re.match(r'^(\d+)$', str(step)): 107 | step = datetime.timedelta(days=int(step)) 108 | else: 109 | try: 110 | step = delta(step) 111 | except ValueError: 112 | pass 113 | 114 | if not isinstance(step, datetime.timedelta): 115 | raise TypeError('Invalid step value: %r' % (step,)) 116 | 117 | # The main generation loop. 118 | while condition(date): 119 | yield date 120 | date += step 121 | 122 | 123 | class delta(object): 124 | 125 | """ 126 | Build instances of ``datetime.timedelta`` using short, friendly strings. 127 | 128 | ``delta()`` allows you to build instances of ``datetime.timedelta`` in 129 | fewer characters and with more readability by using short strings instead 130 | of a long sequence of keyword arguments. 131 | 132 | A typical (but very precise) spec string looks like this: 133 | 134 | '1 day, 4 hours, 5 minutes, 3 seconds, 120 microseconds' 135 | 136 | ``datetime.timedelta`` doesn’t allow deltas containing months or years, 137 | because of the differences between different months, leap years, etc., so 138 | this function doesn’t support them either. 139 | 140 | The parser is very simple; it takes a series of comma-separated values, 141 | each of which represents a number of units of time (such as one day, 142 | four hours, five minutes, et cetera). These ‘specifiers’ consist of a 143 | number and a unit of time, optionally separated by whitespace. The units 144 | of time accepted are (case-insensitive): 145 | 146 | * Days ('d', 'day', 'days') 147 | * Hours ('h', 'hr', 'hrs', 'hour', 'hours') 148 | * Minutes ('m', 'min', 'mins', 'minute', 'minutes') 149 | * Seconds ('s', 'sec', 'secs', 'second', 'seconds') 150 | * Microseconds ('ms', 'microsec', 'microsecs' 'microsecond', 151 | 'microseconds') 152 | 153 | If an illegal specifier is present, the parser will raise a ValueError. 154 | 155 | This utility is provided as a class, but acts as a function (using the 156 | ``__new__`` method). This is so that the names and aliases for units are 157 | stored on the class object itself: as ``UNIT_NAMES``, which is a mapping 158 | of names to aliases, and ``UNIT_ALIASES``, the converse. 159 | """ 160 | 161 | UNIT_NAMES = { 162 | ## unit_name: unit_aliases 163 | 'days': 'd day'.split(), 164 | 'hours': 'h hr hrs hour'.split(), 165 | 'minutes': 'm min mins minute'.split(), 166 | 'seconds': 's sec secs second'.split(), 167 | 'microseconds': 'ms microsec microsecs microsecond'.split(), 168 | } 169 | 170 | # Turn `UNIT_NAMES` inside-out, so that unit aliases point to canonical 171 | # unit names. 172 | UNIT_ALIASES = {} 173 | 174 | for cname, aliases in UNIT_NAMES.items(): 175 | for alias in aliases: 176 | UNIT_ALIASES[alias] = cname 177 | # Make the canonical unit name point to itself. 178 | UNIT_ALIASES[cname] = cname 179 | 180 | def __new__(cls, string): 181 | specifiers = (specifier.strip() for specifier in string.split(',')) 182 | kwargs = {} 183 | 184 | for specifier in specifiers: 185 | match = re.match(r'^(\d+)\s*(\w+)$', specifier) 186 | if not match: 187 | raise ValueError('Invalid delta specifier: %r' % (specifier,)) 188 | 189 | number, unit_alias = match.groups() 190 | number, unit_alias = int(number), unit_alias.lower() 191 | 192 | unit_cname = cls.UNIT_ALIASES.get(unit_alias) 193 | if not unit_cname: 194 | raise ValueError('Invalid unit: %r' % (unit_alias,)) 195 | kwargs[unit_cname] = kwargs.get(unit_cname, 0) + number 196 | 197 | return datetime.timedelta(**kwargs) 198 | 199 | 200 | if __name__ == '__main__': 201 | import doctest 202 | doctest.testmod() 203 | --------------------------------------------------------------------------------