├── .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.3', 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 | >>> next(g1) 12 | datetime.date(2009, 6, 21) 13 | >>> next(g1) 14 | datetime.date(2009, 6, 22) 15 | >>> next(g1) 16 | datetime.date(2009, 6, 23) 17 | >>> next(g1) 18 | datetime.date(2009, 6, 24) 19 | >>> next(g1) 20 | datetime.date(2009, 6, 25) 21 | >>> next(g1) 22 | datetime.date(2009, 6, 26) 23 | 24 | >>> g2 = daterange(start, to=datetime.date(2009, 6, 25)) 25 | >>> next(g2) 26 | datetime.date(2009, 6, 21) 27 | >>> next(g2) 28 | datetime.date(2009, 6, 22) 29 | >>> next(g2) 30 | datetime.date(2009, 6, 23) 31 | >>> next(g2) 32 | datetime.date(2009, 6, 24) 33 | >>> next(g2) 34 | datetime.date(2009, 6, 25) 35 | >>> next(g2) 36 | Traceback (most recent call last): 37 | ... 38 | StopIteration 39 | 40 | >>> g3 = daterange(start, step='2 days') 41 | >>> next(g3) 42 | datetime.date(2009, 6, 21) 43 | >>> next(g3) 44 | datetime.date(2009, 6, 23) 45 | >>> next(g3) 46 | datetime.date(2009, 6, 25) 47 | >>> next(g3) 48 | datetime.date(2009, 6, 27) 49 | 50 | >>> g4 = daterange(start, to=datetime.date(2009, 6, 25), step='2 days') 51 | >>> next(g4) 52 | datetime.date(2009, 6, 21) 53 | >>> next(g4) 54 | datetime.date(2009, 6, 23) 55 | >>> next(g4) 56 | datetime.date(2009, 6, 25) 57 | >>> next(g4) 58 | Traceback (most recent call last): 59 | ... 60 | StopIteration 61 | 62 | """ 63 | 64 | import datetime 65 | import re 66 | import sys 67 | 68 | # Support Python 3's simpler built-in types 69 | if sys.version > '3': 70 | long = int 71 | basestring = unicode = str 72 | 73 | 74 | def daterange(date, to=None, step=datetime.timedelta(days=1)): 75 | return DateRange(date, to, step) 76 | 77 | 78 | class DateRange: 79 | """ 80 | Similar to the built-in ``xrange()``, only for datetime objects. 81 | 82 | If called with just a ``datetime`` object, it will keep yielding values 83 | forever, starting with that date/time and counting in steps of 1 day. 84 | 85 | If the ``to_date`` keyword is provided, it will count up to and including 86 | that date/time (again, in steps of 1 day by default). 87 | 88 | If the ``step`` keyword is provided, this will be used as the step size 89 | instead of the default of 1 day. It should be either an instance of 90 | ``datetime.timedelta``, an integer, a string representing an integer, or 91 | a string representing a ``delta()`` value (consult the documentation for 92 | ``delta()`` for more information). If it is an integer (or string thereof) 93 | then it will be interpreted as a number of days. If it is not a simple 94 | integer string, then it will be passed to ``delta()`` to get an instance 95 | of ``datetime.timedelta()``. 96 | 97 | Note that, due to the similar interfaces of both objects, this function 98 | will accept both ``datetime.datetime`` and ``datetime.date`` objects. If 99 | a date is given, then the values yielded will be dates themselves. A 100 | caveat is in order here: if you provide a date, the step should have at 101 | least a ‘days’ component; otherwise the same date will be yielded forever. 102 | """ 103 | 104 | def __init__(self, date, to=None, step=datetime.timedelta(days=1)): 105 | self.date = date 106 | self.to = to 107 | 108 | if to is None: 109 | self.condition = lambda d: True 110 | else: 111 | self.condition = lambda d: (d <= to) 112 | 113 | if isinstance(step, (int, long)): 114 | # By default, integers are interpreted in days. For more granular 115 | # steps, use a `datetime.timedelta()` instance. 116 | step = datetime.timedelta(days=step) 117 | elif isinstance(step, basestring): 118 | # If the string 119 | if re.match(r'^(\d+)$', str(step)): 120 | step = datetime.timedelta(days=int(step)) 121 | else: 122 | try: 123 | step = delta(step) 124 | except ValueError: 125 | pass 126 | 127 | if not isinstance(step, datetime.timedelta): 128 | raise TypeError('Invalid step value: %r' % (step,)) 129 | 130 | self.step = step 131 | 132 | 133 | def __iter__(self): 134 | return self 135 | 136 | 137 | def next(self): 138 | if self.condition(self.date): 139 | current_date = self.date 140 | self.date += self.step 141 | return current_date 142 | else: 143 | raise StopIteration() 144 | 145 | 146 | # Python 3 compatibility 147 | def __next__(self): 148 | return self.next() 149 | 150 | 151 | def __contains__(self, item): 152 | """ 153 | Test membership of ``item`` in date-range, using date boundaries. 154 | 155 | >>> import datetime 156 | >>> now = datetime.datetime.now() 157 | >>> now_until_eternity = DateRange(now) 158 | 159 | >>> tomorrow = (now + datetime.timedelta(1)) 160 | >>> tomorrow in now_until_eternity 161 | True 162 | 163 | >>> a_week_ago = (now - datetime.timedelta(7)) 164 | >>> a_week_ago in now_until_eternity 165 | False 166 | 167 | >>> a_week_today = (now + datetime.timedelta(7)) 168 | >>> a_week_today in now_until_eternity 169 | True 170 | 171 | >>> now_until_next_week = DateRange(now, now + datetime.timedelta(7)) 172 | >>> a_week_ago in now_until_next_week 173 | False 174 | 175 | >>> tomorrow in now_until_next_week 176 | True 177 | 178 | >>> a_week_today in now_until_next_week 179 | False 180 | """ 181 | if item >= self.date: 182 | if self.to: 183 | if item < self.to: 184 | return True 185 | else: 186 | return True 187 | 188 | return False 189 | 190 | 191 | def __repr__(self): 192 | return 'DateRange({date}, to={to}, step={step})'.format(date=self.date, 193 | to=self.to, 194 | step=self.step) 195 | 196 | 197 | class delta(object): 198 | 199 | """ 200 | Build instances of ``datetime.timedelta`` using short, friendly strings. 201 | 202 | ``delta()`` allows you to build instances of ``datetime.timedelta`` in 203 | fewer characters and with more readability by using short strings instead 204 | of a long sequence of keyword arguments. 205 | 206 | A typical (but very precise) spec string looks like this: 207 | 208 | '1 day, 4 hours, 5 minutes, 3 seconds, 120 microseconds' 209 | 210 | ``datetime.timedelta`` doesn’t allow deltas containing months or years, 211 | because of the differences between different months, leap years, etc., so 212 | this function doesn’t support them either. 213 | 214 | The parser is very simple; it takes a series of comma-separated values, 215 | each of which represents a number of units of time (such as one day, 216 | four hours, five minutes, et cetera). These ‘specifiers’ consist of a 217 | number and a unit of time, optionally separated by whitespace. The units 218 | of time accepted are (case-insensitive): 219 | 220 | * Days ('d', 'day', 'days') 221 | * Hours ('h', 'hr', 'hrs', 'hour', 'hours') 222 | * Minutes ('m', 'min', 'mins', 'minute', 'minutes') 223 | * Seconds ('s', 'sec', 'secs', 'second', 'seconds') 224 | * Microseconds ('ms', 'microsec', 'microsecs' 'microsecond', 225 | 'microseconds') 226 | 227 | If an illegal specifier is present, the parser will raise a ValueError. 228 | 229 | This utility is provided as a class, but acts as a function (using the 230 | ``__new__`` method). This is so that the names and aliases for units are 231 | stored on the class object itself: as ``UNIT_NAMES``, which is a mapping 232 | of names to aliases, and ``UNIT_ALIASES``, the converse. 233 | """ 234 | 235 | UNIT_NAMES = { 236 | ## unit_name: unit_aliases 237 | 'days': 'd day'.split(), 238 | 'hours': 'h hr hrs hour'.split(), 239 | 'minutes': 'm min mins minute'.split(), 240 | 'seconds': 's sec secs second'.split(), 241 | 'microseconds': 'ms microsec microsecs microsecond'.split(), 242 | } 243 | 244 | # Turn `UNIT_NAMES` inside-out, so that unit aliases point to canonical 245 | # unit names. 246 | UNIT_ALIASES = {} 247 | 248 | for cname, aliases in UNIT_NAMES.items(): 249 | for alias in aliases: 250 | UNIT_ALIASES[alias] = cname 251 | # Make the canonical unit name point to itself. 252 | UNIT_ALIASES[cname] = cname 253 | 254 | def __new__(cls, string): 255 | specifiers = (specifier.strip() for specifier in string.split(',')) 256 | kwargs = {} 257 | 258 | for specifier in specifiers: 259 | match = re.match(r'^(\d+)\s*(\w+)$', specifier) 260 | if not match: 261 | raise ValueError('Invalid delta specifier: %r' % (specifier,)) 262 | 263 | number, unit_alias = match.groups() 264 | number, unit_alias = int(number), unit_alias.lower() 265 | 266 | unit_cname = cls.UNIT_ALIASES.get(unit_alias) 267 | if not unit_cname: 268 | raise ValueError('Invalid unit: %r' % (unit_alias,)) 269 | kwargs[unit_cname] = kwargs.get(unit_cname, 0) + number 270 | 271 | return datetime.timedelta(**kwargs) 272 | 273 | 274 | if __name__ == '__main__': 275 | import doctest 276 | doctest.testmod() 277 | --------------------------------------------------------------------------------