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