views:

1813

answers:

7

This is working fine, but I'm looking for any feedback on how to do it better. Right now I think it's better than nested loops, but it starts to get Perl-one-linerish when you have a generator in a list comprehension. Any suggestions are welcome.

day_count = (end_date - start_date).days + 1
for single_date in [d for d in (start_date + timedelta(n) for n in range(day_count)) if d <= end_date]:
    print strftime("%Y-%m-%d", single_date.timetuple())

Notes: I'm not actually using this to print; that's just for demo purposes. The variables start_date and end_date are datetime.date objects, because I don't need the timestamps (they're going to be used to generate a report).

I checked the StackOverflow questions which were similar before posting this, but none were exactly the same.

Sample Output (for a start date of 2009-05-30 and an end date of 2009-06-09):

2009-05-30
2009-05-31
2009-06-01
2009-06-02
2009-06-03
2009-06-04
2009-06-05
2009-06-06
2009-06-07
2009-06-08
2009-06-09
+3  A: 

Why are the two nested iterations? For me it produces the same list of data with only one iteration:

for single_date in (start_date + timedelta(n) for n in range(day_count)):
    print ...

And not list get stored, only one generator is iterated over. Also the "if" in the generator seems to be unnecessary.

After all, a linear sequence should only require one iterator, not two.

Upate after Diskussion with John Machin:

Maybe the most elegant solution is using a generator function to completely hide/abstract the iteration over the range of dates:

def daterange(start_date, end_date):
    for n in range((end_date - start_date).days):
        yield start_date + timedelta(n)

for single_date in daterange(start_date, end_date):
    print strftime("%Y-%m-%d", single_date.timetuple())

NB: For consistency withe built-in range() function this iteration stops before reaching the end_date. So for inclusive iteration use the next day, as you would with range().

Ber
Thanks, good point. That if statement was left over from a previous version, before I was subtracting start date from end date.
ShawnMilo
Thanks Ber, that's awesome. It works perfectly and looks much better than my old mess.
ShawnMilo
-1 ... having a preliminary calculation of day_count and using range is not awesome when a simple while loop will suffice.
John Machin
@John Machin: Post your own code to compare!
Ber
@Ber: Haven't you seen Sean Cavanagh's code?
John Machin
@John Machin: Okay. I do however prever an iteration over while loops with explicit incrementation of some counter or value. The interation pattern is more pythonic (at least in my personal view) and also more general, as it allows to express an iteration while hiding the details of how that iteration is done.
Ber
@John Machin: I have updated my answer to show what I mean. Hope you'll like it.
Ber
@Ber: I don't like it at all; it's DOUBLY bad. You ALREADY had an iteration! By wrapping the complained-about constructs in a generator, you have added even more execution overhead plus diverted the user's attention to somewhere else to read your 3-liner's code and/or docs. -2
John Machin
@John Machin: I disagree. The point is not about reducing the number of lines to the absolute minimum. After all, we're not talking Perl here. Also, my code does only one iteration (that's how the generator works, but I guess you know that). *** My point is about abstracting concepts for re-use and self explanatory code. I maintain that this is far more worthwhile than have the shortest code possible.
Ber
You can make this code slightly shorter if you use the datetime object's strftime method.
itsadok
+7  A: 

This might be more clear:

d = start_date
delta = datetime.timedelta(days=1)
while d <= end_date:
    print time.strftime("%Y-%m-%d", d.timetuple())
    d += delta
Sean Cavanagh
+5  A: 
import datetime

def daterange(start, stop, step=datetime.timedelta(days=1), inclusive=False):
  # inclusive=False to behave like range by default
  if step.days > 0:
    while start < stop:
      yield start
      start = start + step
      # not +=! don't modify object passed in if it's mutable
      # since this function is not restricted to
      # only types from datetime module
  elif step.days < 0:
    while start > stop:
      yield start
      start = start + step
  if inclusive and start == stop:
    yield start

# ...

for date in daterange(start_date, end_date, inclusive=True):
  print strftime("%Y-%m-%d", date.timetuple())

This function does more than you strictly require, by supporting negative step, etc. As long as you factor out your range logic, then you don't need the separate day_count and most importantly the code becomes easier to read as you call the function from multiple places.

Roger Pate
Variable `delta`
Evan Fosmark
Thanks, renamed to more closely match range's parameters, forgot to change in the body.
Roger Pate
+1 ... but as you are allowing the step to be a timedelta, you should either (a) call it dateTIMErange() and make steps of e.g. timedelta(hours=12) and timedelta(hours=36) work properly or (b) trap steps that aren't an integral number of days or (c) save the caller the hassle and express the step as a number of days instead of a timedelta.
John Machin
Any timedelta should work already, but I did add datetime_range and date_range to my personal scrap collection after writing this, because of (a). Not sure another function is worthwhile for (c), the most common case of days=1 is already taken care of, and having to pass an explicit timedelta avoids confusion. Maybe uploading it somewhere is best: http://bitbucket.org/kniht/scraps/src/tip/python/gen_range.py
Roger Pate
+4  A: 

use the dateutil library:

from datetime import date
from dateutil.rrule import rrule, DAILY
a = date(2009, 5, 30)
b = date(2009, 6, 9)
for dt in rrule(DAILY, dtstart=a, until=b):
    print dt.strftime("%Y-%m-%d")

This python library has many more advanced features, some very useful, like relativedeltas - and is implemented as a single .py python file that can be easily included into your project.

nosklo
A: 
import datetime

def daterange(start, stop, step_days=1):
    current = start
    step = datetime.timedelta(step_days)
    if step_days > 0:
        while current < stop:
            yield current
            current += step
    elif step_days < 0:
        while current > stop:
            yield current
            current += step
    else:
        raise ValueError("daterange() step_days argument must not be zero")

if __name__ == "__main__":
    from pprint import pprint as pp
    lo = datetime.date(2008, 12, 27)
    hi = datetime.date(2009, 1, 5)
    pp(list(daterange(lo, hi)))
    pp(list(daterange(hi, lo, -1)))
    pp(list(daterange(lo, hi, 7)))
    pp(list(daterange(hi, lo, -7))) 
    assert not list(daterange(lo, hi, -1))
    assert not list(daterange(hi, lo))
    assert not list(daterange(lo, hi, -7))
    assert not list(daterange(hi, lo, 7))
John Machin
+1  A: 

Why would anyone want to reinvent the wheel?

from dateutil.rrule import rrule, DAILY
rr = rrule(DAILY, dtstart=start_date, until=end_date)
print list(rr)

and you can do much more

parxier
A: 

for i in range(16): print datetime.date.today()+datetime.timedelta(days=i)