2
current_datetime = datetime.now(tz)
next_hour = datetime(current_datetime.year, current_datetime.month, current_datetime.day, 7, 0, 0, 0, tz)
timedelta_until_next_hour = next_hour - current_datetime
if timedelta_until_next_hour.total_seconds() < 0:
    timedelta_until_next_hour += timedelta(days=1)
return timedelta_until_next_hour.total_seconds()

I'm trying to find the next time it's 7am for a local timezone and return the number of seconds until that.

I'm having some daylight savings time issues. For Example: America/New_York current_datetime has a utcoffset of -4 hours Whereas next_hour has an offset of -5 hours, so the subtraction of the two is off by an hour

Paul
  • 10,381
  • 13
  • 48
  • 86
quikst3r
  • 1,783
  • 1
  • 10
  • 15
  • Can you give the date/time/tz values is this reproducable for? – Eric Apr 13 '16 at 01:36
  • related: [How to convert tomorrows (at specific time) date to a timestamp](http://stackoverflow.com/q/30822699/4279) – jfs Apr 14 '16 at 09:08

2 Answers2

4

Finding the next 7am

You can do this pretty easily with python-dateutil's relativedelta module:

from dateutil.relativedelta import relativedelta
def next_7am(dt):
    relative_days = (dt.hour >= 7)
    absolute_kwargs = dict(hour=7, minute=0, second=0, microsecond=0)
    return dt + relativedelta(days=relative_days, **absolute_kwargs)

The way it works is that relativedelta takes absolute arguments (denoted by being in the singular, e.g. month, year, day) and relative arguments (denoted by being in the plural, e.g. months, years, days). If you add a relativedelta object to a datetime, it will replace absolute values in the datetime, then add the relative values, so what I've done above is specify that relative_days should be 1 if it's already 7am, otherwise it should be 0, and the absolute arguments say "replace the time with 7 AM". Add that to your datetime and it will give you the next 7am.

Dealing with time zones

The next step depends on what you are using for your time zone. If you are using a dateutil time zone, then you can just use the function defined above:

dt_next_7am = next_7am(dt)

If you are using a pytz timezone, you should strip it off and do the calculation as a naive date-time, then re-localize the time zone, as below:

dt_next_7am = tz.localize(next_7am(dt.replace(tzinfo=None)))

If you want to get the absolute number of hours between those two times, you should do the arithmetic in UTC:

time_between = dt_next_7am.astimezone(tz=UTC) - dt.astimezone(tz=UTC)

Where UTC has been defined as either dateutil.tz.tzutc() or pytz.UTC or equivalent.

Examples across a DST transition

Here is an example using dateutil (with the result in the comment):

from datetime import datetime
from dateutil.tz import gettz, tzutc

LA = gettz('America/Los_Angeles')
dt = datetime(2011, 11, 5, 12, 30, tzinfo=LA)
dt7 = next_7am(dt)

print(dt7.astimezone(tzutc()) - dt.astimezone(tzutc()))  # 19:30:00

And an example showing the wrong and right way to do this with pytz:

from datetime import datetime
import pytz
LA = pytz.timezone('America/Los_Angeles')
UTC = pytz.UTC

dt = LA.localize(datetime(2011, 11, 5, 12, 30))
dt7_bad = next_7am(dt)      # pytz won't like this
dt7_good = LA.localize(next_7am(dt.replace(tzinfo=None)))

dt_utc = dt.astimezone(pytz.UTC)
print(dt7_bad.astimezone(pytz.UTC) - dt_utc)   # 18:30:00 (Wrong)
print(dt7_good.astimezone(pytz.UTC) - dt_utc)  # 19:30:00 (Right)

Ambiguous / Non-existent 7 AM

If you are dealing with certain dates in certain zones, specifically those that would result in an ambiguous time are on the following list (as of April 2016):

1901-12-13 07:00:00 (/Pacific/Fakaofo)
1901-12-14 07:00:00 (/Asia/Kamchatka)
1901-12-14 07:00:00 (/Asia/Ust-Nera)
1901-12-14 07:00:00 (/Pacific/Bougainville)
1901-12-14 07:00:00 (/Pacific/Kosrae)
1901-12-14 07:00:00 (/Pacific/Majuro)
1917-03-25 07:00:00 (/Antarctica/Macquarie)
1918-03-31 07:00:00 (/EST5EDT)
1919-03-31 07:00:00 (/Antarctica/Macquarie)
1952-01-13 07:00:00 (/Antarctica/DumontDUrville)
1954-02-13 07:00:00 (/Antarctica/Mawson)
1957-01-13 07:00:00 (/Antarctica/Davis)
1969-01-01 07:00:00 (/Antarctica/Casey)
1969-02-01 07:00:00 (/Antarctica/Davis)
1969-09-29 07:00:00 (/Kwajalein)
1969-09-29 07:00:00 (/Pacific/Kwajalein)
1979-09-30 07:00:00 (/Pacific/Enderbury)
1979-09-30 07:00:00 (/Pacific/Kiritimati)
2009-10-18 07:00:00 (/Antarctica/Casey)
2011-09-23 07:00:00 (/Pacific/Apia)
2011-10-28 07:00:00 (/Antarctica/Casey)

Then the resulting 7AM value will be either ambiguous or non-existent. If you want to handle these edge cases, see this answer. It is probably worth noting that after PEP495 has been implemented, dealing with ambiguous times will probably be handled slightly differently.

An alternative implementation using python-dateutil's rrule module for generating recurrence rules and approach with pytz zones is below (note that this will work with non-pytz zones, but it will not resolve ambiguious/non-existent times properly):

from datetime import datetime
from dateutil import rrule
import pytz

def next_real_7am(dt):
    tzi = dt.tzinfo

    dt_naive = dt.replace(tzinfo=None)
    rr = rrule.rrule(freq=rrule.DAILY, byhour=7, dtstart=dt_naive)

    for ndt in rr:
        localize = getattr(tzi, 'localize', None)
        if tzi is not None and localize is not None:
            try:
                ndt = localize(ndt, is_dst=None)
            except pytz.AmbiguousTimeError:
                return min([localize(ndt, is_dst=True),
                            localize(ndt, is_dst=False)])
            except pytz.NonExistentTimeError:
                continue
        else:
            ndt = ndt.replace(tzinfo=tzi)

        return ndt

KWA = pytz.timezone('Pacific/Kwajalein')

dtstart = KWA.localize(datetime(1969, 9, 29, 18))
dt7 = next_real_7am(dtstart)

print(dt7.tzname())         # Should be MHT, before the transition

dtstart = KWA.localize(datetime(1993, 8, 19, 18))  # There was no 8/20 in this zone
dt7 = next_real_7am(dtstart)
print(dt7)                  # Should be 1993-8-21 07:00:00
jfs
  • 399,953
  • 195
  • 994
  • 1,670
Paul
  • 10,381
  • 13
  • 48
  • 86
  • `dt.replace(hour=7, ...)` gives 7am today, not the nearest 7am in the future – Eric Apr 13 '16 at 01:34
  • Oops, my bad, adjusting. – Paul Apr 13 '16 at 01:34
  • Thank you. Is there a reason to use relativedelta instead of timedelta? nvm I got it :P – quikst3r Apr 13 '16 at 01:52
  • @quikst3r They do two different things. `timedelta` does pure "wall clock" arithmetic. `relativedelta` allows you to make absolute replacements during the addition step. It has a number of other features you can read about in the linked documentation if you are interested. – Paul Apr 13 '16 at 01:55
  • the code is wrong if the input time is non-existent/ambiguous in the given time zone (e.g., during DST transitions). See [how non-existent/ambiguous time can be handled](http://stackoverflow.com/a/30840025/4279) – jfs Apr 15 '16 at 05:42
  • If the original date is an unambiguous representation of an ambiguous datetime (e.g. it has a time zone properly attached), this code works perfectly well, [as you can see in this gist](https://gist.github.com/anonymous/0a8b69a7d641366de5879fccaba479d7). Constructing such a representation is unnecessary in this example, though, since the question had nothing to do with how to construct ambiguous datetimes, and the OP didn't even tell us what he was using for a timezone. – Paul Apr 15 '16 at 12:17
  • @Paul 1- I'm not talking about the input date (it is `now(tz)` and it always works (with pytz tzinfos)). I'm talking about the result ("next 7am"). Follow the link in my comment and see how the exceptions (due to ambiguous or non-existent time) are handled. 2- use @-syntax if you want me to be notified about your comments. – jfs Apr 16 '16 at 06:15
  • @J.F.Sebastian Ah, I understand what you mean. Yes, I will update with a link to that answer. Personally, I think it makes things a bit messy to deal with it, I'd rather just wait for PEP495 and use the `fold` attribute, but obviously not everyone can do that. – Paul Apr 16 '16 at 15:17
  • the structure of your answer recommends wrong solutions without a proper disclaimer. It is easy to write `dt + relativedelta(days=(dt.hour > hour), hour=hour, minute=0, second=0, microsecond=0)` and forget that it may produce a wrong result if there is a DST transition in between (for pytz tzinfos) or if the result itself is a non-existent or ambiguous time. Later code examples are trying to fix it but they are either wrong (e.g., `next_real_7am()` doesn't work for non-pytz tzinfos) or just misleading (there is missing `is_dst=None`, to assert that both the input and result exist and unique). – jfs Apr 18 '16 at 08:40
  • @J.F.Sebastian 1. The answer is structured to go from the general to the specific. It solves the general problem, then adds in edge cases one at a time. I'm sorry you disagree with this approach. 2. I have now fixed the issue with `next_real_7am()`. – Paul Apr 18 '16 at 11:13
  • @Paul you are entitled to your own opinion, but you are NOT entitled to your own facts. If the code is wrong for a valid input then it is wrong even if it works for some input. It is like saying "`x**2` is always positive" if your program may receive zero, complex numbers. The statement is wrong unless it is true for all possible input numbers. To make it true, you could limit the allowed input e.g., "the code expects that the UTC offset for the local time zone is fixed in the related time interval.", etc. – jfs Apr 18 '16 at 11:44
  • I don't see how that's what I'm doing. I addressed all edge cases, you just don't like the order I chose to do it in. – Paul Apr 18 '16 at 11:54
  • There is an off by one error in this post, Should be `relative_days = (dt.hour >= 7)` I tried making an edit but hasn't been approved – quikst3r Apr 26 '16 at 05:35
-2

I'm trying to find the next time it's 7am for a local timezone and return the number of seconds until that.

Find dt7 using the same code as for dt6 (replace time(6) with time(7)).

Then the number of seconds until that is (dt7 - now).total_seconds().

See the bullet points that explain when other solutions may fail.

Community
  • 1
  • 1
jfs
  • 399,953
  • 195
  • 994
  • 1,670
  • I don't mind the downvote. But would appreciate an explanation. How the answer could be improved? – jfs Nov 10 '16 at 21:38