views:

314

answers:

4

How can you calculate the following Friday at 3am as a datetime object?

Clarification: i.e., the calculated date should always be greater than 7 days away, and less than or equal to 14.


Going with a slightly modified version of Mark's solution:

def _next_weekday(day_of_week=4, time_of_day=datetime.time(hour=3), dt=None):
    if dt is None: dt = datetime.datetime.now()
    dt += datetime.timedelta(days=7)
    if dt.time() < time_of_day: dt = dt.combine(dt.date(), time_of_day)
    else: dt = dt.combine(dt.date(), time_of_day) + datetime.timedelta(days=1)
    return dt + datetime.timedelta((day_of_week - dt.weekday()) % 7)
+5  A: 

If you install dateutil, then you could do something like this:

import datetime
import dateutil.relativedelta as reldate

def following_friday(dt):   
    rd=reldate.relativedelta(
        weekday=reldate.FR(+2),
        hours=+21)
    rd2=reldate.relativedelta(
        hour=3,minute=0,second=0,microsecond=0)
    return dt+rd+rd2

Above, hours=+21 tells relativedelta to increment the dt by 21 hours before finding the next Friday. So, if dt is March 12, 2010 at 2am, adding 21 hours makes it 11pm of the same day, but if dt is after 3am, then adding 21 hours pushs dt into Saturday.

Here is some test code.

if __name__=='__main__':
    today=datetime.datetime.now()
    for dt in [today+datetime.timedelta(days=i) for i in range(-7,8)]:
        print('%s --> %s'%(dt,following_friday(dt)))

which yields:

2010-03-05 20:42:09.246124 --> 2010-03-19 03:00:00
2010-03-06 20:42:09.246124 --> 2010-03-19 03:00:00
2010-03-07 20:42:09.246124 --> 2010-03-19 03:00:00
2010-03-08 20:42:09.246124 --> 2010-03-19 03:00:00
2010-03-09 20:42:09.246124 --> 2010-03-19 03:00:00
2010-03-10 20:42:09.246124 --> 2010-03-19 03:00:00
2010-03-11 20:42:09.246124 --> 2010-03-19 03:00:00
2010-03-12 20:42:09.246124 --> 2010-03-26 03:00:00 
2010-03-13 20:42:09.246124 --> 2010-03-26 03:00:00
2010-03-14 20:42:09.246124 --> 2010-03-26 03:00:00
2010-03-15 20:42:09.246124 --> 2010-03-26 03:00:00
2010-03-16 20:42:09.246124 --> 2010-03-26 03:00:00
2010-03-17 20:42:09.246124 --> 2010-03-26 03:00:00
2010-03-18 20:42:09.246124 --> 2010-03-26 03:00:00
2010-03-19 20:42:09.246124 --> 2010-04-02 03:00:00

while before 3am:

two = datetime.datetime(2010, 3, 12, 2, 0)
for date in [two+datetime.timedelta(days=i) for i in range(-7,8)]:
    result = following_friday(date)
    print('{0}-->{1}'.format(date,result))

yields:

2010-03-05 02:00:00-->2010-03-12 03:00:00
2010-03-06 02:00:00-->2010-03-19 03:00:00
2010-03-07 02:00:00-->2010-03-19 03:00:00
2010-03-08 02:00:00-->2010-03-19 03:00:00
2010-03-09 02:00:00-->2010-03-19 03:00:00
2010-03-10 02:00:00-->2010-03-19 03:00:00
2010-03-11 02:00:00-->2010-03-19 03:00:00
2010-03-12 02:00:00-->2010-03-19 03:00:00
2010-03-13 02:00:00-->2010-03-26 03:00:00
2010-03-14 02:00:00-->2010-03-26 03:00:00
2010-03-15 02:00:00-->2010-03-26 03:00:00
2010-03-16 02:00:00-->2010-03-26 03:00:00
2010-03-17 02:00:00-->2010-03-26 03:00:00
2010-03-18 02:00:00-->2010-03-26 03:00:00
2010-03-19 02:00:00-->2010-03-26 03:00:00
unutbu
`relativedelta` also takes absolute values such as `hour`, `minute`, etc. That way you can produce a delta that will also set hours and minutes and seconds on your result to 3am sharp.See: http://labix.org/python-dateutil#head-6a1472b7c74e5b8bab7784f11214250d34e09aa5
Pavel Repin
ack! I didn't mean thursday was a special case, that was just an example. although, if we replace +2 with +7.... it should work right?
Mark
@Mark: I've edited the code to hopefully conform with what you want. `days=+8` should do it if I understand correctly.
unutbu
@Pavel: Thanks!
unutbu
@unutbu: Hang on... if it's Friday 2:59am, it should return a datetime 7 days and 1 minute away, not 14 days and 1 minute away. You sure +8 is right?
Mark
@Mark: If `dt` is a Friday, then `dt+relativedelta(weekday=FR)` returns `dt` (the same Friday). To get `relativedelta` to always return a different Friday, you must add one day. To get the following Friday, you then add another 7 days. All told, I think you need to add 8 days. Is the output above generating the right Fridays?
unutbu
You missed a test case... Friday before 3 am. Updating answer.
Mark
@Mark: Okay, I think I've corrected the mistake. The idea is instead of using `days=+1` to push ahead one day, you use `hours+=21` to push ahead one day only when the time is after 3am. Instead of adding an additional 7 days, I used `weekday=FR(+2)`, which is just another way to say the same thing.
unutbu
+4  A: 

I like dateutil for such tasks in general, but I don't understand the heuristics you want -- as I use the words, if I say "next Friday" and it's Thursday I would mean tomorrow (probably I've been working too hard and lost track of what day of the week it is). If you can specify your heuristics rigorously they can surely be programmed, of course, but if they're weird and quirky enough you're unlikely to find them already pre-programmed for you in existing packages;-).

Alex Martelli
I dunno, people use the word "next" ambiguously. Typically, people use "this Friday" to mean "this coming Friday; the one within this week", and "next Friday" to mean the "the Friday that resides in the following week". I simply mean the latter.
Mark
I've argued with various people about what "next" means when it comes to phrases such as "next Friday". To me, the interpretation that makes the most sense would be the first Friday following today, but everyone else seems to think it means the one second Friday following today. According to those people, you would say "this Friday" to refer to the first Friday following today.
allyourcode
+2  A: 

Based on your clarification... I think you can do something like this:

from datetime import *
>>> today = datetime.today()
>>> todayAtThreeAm = datetime(today.year, today.month, today.day, 3)
>>> todayAtThreeAm
datetime.datetime(2010, 3, 12, 3, 0)
>>> nextFridayAtThreeAm = todayAtThreeAm + timedelta(12 - today.isoweekday())
>>> nextFridayAtThreeAm
datetime.datetime(2010, 3, 19, 3, 0)

Notice isoweekday() returns 1 to 7 for monday to sunday. 12 represents friday of the following week. So 12 - today.isoweekday() gives you the correct time delta you need to add to today.

Hope this helps.

Tom
I think that's incorrect. It should actually return March 26th, since it is currently after 3am on Friday. Remember that it should be strictly greater than 7 days away :)
Mark
Maybe a conditional would do it? `days=12-today.isoweekday(); if days <= 7: days += 7`?
Mark
+3  A: 

Here's a function and a test that it meets the OP's requirements:

import datetime

_3AM = datetime.time(hour=3)
_FRI = 4 # Monday=0 for weekday()

def next_friday_3am(now):
    now += datetime.timedelta(days=7)
    if now.time() < _3AM:
        now = now.combine(now.date(),_3AM)
    else:
        now = now.combine(now.date(),_3AM) + datetime.timedelta(days=1)
    return now + datetime.timedelta((_FRI - now.weekday()) % 7)

if __name__ == '__main__':
    start = datetime.datetime.now()
    for i in xrange(7*24*60*60):
        now = start + datetime.timedelta(seconds=i)
        then = next_friday_3am(now)
        assert datetime.timedelta(days=7) < then - now <= datetime.timedelta(days=14)
        assert then.weekday() == _FRI
        assert then.time() == _3AM
Mark Tolonen
Haha...very comprehensive test. I was wondering why it was taking so long to run until I read it :D
Mark
I quite like this solution actually. Doesn't depend on another library, and it's simply to modify for other days/times.
Mark