11

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.

mpen
  • 272,448
  • 266
  • 850
  • 1,236
  • what about if it's monday? Should it calculate 4 days from now? Or 11 days from now? And is sunday 5 days from now? Please clarify! – Tom Mar 13 '10 at 01:28
  • Do you need to account for daylight savings time? – John La Rooy Mar 13 '10 at 04:36
  • @gnibbler: Uhh... I dunno, I don't really care as long as it's consistent :) Not worried if my little script shifts an hour during different parts of the year... it'll still be during the middle of the night when there's less traffic. – mpen Mar 13 '10 at 08:32
  • 3
    @Mark_the_OP: **DANGER** `def next_weekday(dt=datetime.datetime.now(), ...` that default argument value is evaluated WHEN THE MODULE CONTAINING THE FUNCTION IS IMPORTED. It is NOT evaluated every time you call the function. If you want such a gimmick, you'll need to code it yourself. `def next_weekday(dt=None, ...): if dt is None: dt = datetime.datetime.now()` – John Machin Mar 13 '10 at 14:23
  • @John: Isn't the module usually imported a split second before that function actually gets run (assuming my script doesn't take forever to run)? Or maybe my WSGI thingy keeps it in memory? I'll do it your way just to be safe, thanks ! – mpen Mar 14 '10 at 00:29
  • @Mark_the_OP: "usually" is a very moveable feast. Usually your script jumps in, does something, and vanishes. [Aside: You didn't mention that fact, and had you mentioned it, I would have cautioned against considering that the fact was relevant.] Usually person B's script runs all night while the users are sleeping. Usually person C's module is used in a 24/7 app and the module is imported only intermittently. Usually person D's next_Friday function is in a library that is intended to behave robustly in ALL circumstances, usual or not. – John Machin Mar 14 '10 at 00:44
  • I might be Person C. It's a web app... I dunno what it's doing behind the scenes :p Thanks for the tip. – mpen Mar 14 '10 at 01:05

5 Answers5

9

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
  • 842,883
  • 184
  • 1,785
  • 1,677
  • `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 Mar 13 '10 at 01:26
  • 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? – mpen Mar 13 '10 at 01:34
  • 1
    @Mark: I've edited the code to hopefully conform with what you want. `days=+8` should do it if I understand correctly. – unutbu Mar 13 '10 at 01:46
  • @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? – mpen Mar 13 '10 at 02:06
  • @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 Mar 13 '10 at 02:21
  • You missed a test case... Friday before 3 am. Updating answer. – mpen Mar 13 '10 at 02:54
  • 1
    @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 Mar 13 '10 at 03:51
7

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
  • 166,664
  • 26
  • 169
  • 251
  • Haha...very comprehensive test. I was wondering why it was taking so long to run until I read it :D – mpen Mar 13 '10 at 08:38
  • I quite like this solution actually. Doesn't depend on another library, and it's simply to modify for other days/times. – mpen Mar 13 '10 at 08:55
4

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
  • 854,459
  • 170
  • 1,222
  • 1,395
  • 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. – mpen Mar 13 '10 at 01:37
  • 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 Mar 13 '10 at 02:48
2

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
  • 21,468
  • 6
  • 39
  • 44
  • 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 :) – mpen Mar 13 '10 at 02:18
  • Maybe a conditional would do it? `days=12-today.isoweekday(); if days <= 7: days += 7`? – mpen Mar 13 '10 at 02:24
1

With pendulum, you can do:

In [15]: pendulum.now().next(pendulum.FRIDAY).next(pendulum.FRIDAY).add(hours=3)
Out[15]: DateTime(2019, 5, 3, 3, 0, 0, tzinfo=Timezone('America/Los_Angeles'))

Note that there are two next Friday in this line.

To convert it into string,

In [16]: pendulum.now().next(pendulum.FRIDAY).next(pendulum.FRIDAY).add(hours=3).to_iso8601_string()
Out[16]: '2019-05-03T03:00:00-07:00'
r44
  • 434
  • 6
  • 5