├── .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 |
--------------------------------------------------------------------------------