2

I am writing a module that calculates the half hour trading period in a given day. The trading periods start on the half hour and are consecutively numbered from 1 (starting 00:00) to 48 (starting 23:30). Normally there are 48 trading periods in a day, but on the day that daylight savings starts there are 46 and on the day it ends there are 50.

The code below works on all 'normal' days, but fails to give correct trading period numbers on days when daylight savings starts or ends because datetime.replace() in the code below uses the same UTC time offset for the start of the day. On days when daylight savings changes this assumption is incorrect.

Is it possible for datetime.replace() to set the time at 00:00 for 'wall clock' time, so that the time difference matches what you get if you set a stopwatch at 00:00 then counted the number of half hour intervals it would match correctly for all days? I have not found an automatic way to do this.

An example: Daylight savings ended in New Zealand on 5th April 2015 at 03:00 (2015-05-04 14:00Z). Therefore the hour 02:00-02:59 (2015-05-04 14:00Z - 2015-05-04 14:59Z) was repeated in 'wall clock' time. Therefore in New Zealand it took 18000 seconds to reach 4am on the 5th April 2015, as daylight savings time ended. On 28th September 2014 it took 10800 seconds, as daylight savings time started.

@staticmethod
def half_hour_from_utc(time_utc = None):
    # Get the time as NZ civil time.
    time_nz = time_utc.astimezone(pytz.timezone('Pacific/Auckland'))



    # Get the time tuple for the start of the day. This is done by keeping
    # the date the same but setting the hours, minutes, seconds and
    # microseconds to zero.
    time_start_of_day = time_nz.replace(hour = 0, minute = 0, second = 0, microsecond = 0)

    # Get total number of seconds. The half hour period is the number of
    # 1800 second periods that have passed + 1
    total_secs = int((time_nz - time_start_of_day).total_seconds())
    half_hour = 1 + total_secs // 1800


    print('%s %s %s %s\n' % (time_nz, time_start_of_day, total_secs, half_hour))
bjem
  • 175
  • 6

2 Answers2

1

The issue is the .replace() call that may return a non-normalized datetime value i.e., tzinfo may be wrong for the midnight. See How do I get the UTC time of “midnight” for a given timezone?

from datetime import datetime, time as datetime_time, timedelta
import pytz # $ pip install pytz

def half_hour_from_utc(time_utc, tz=pytz.timezone('Pacific/Auckland')):
    time_nz = time_utc.astimezone(tz) # no need to call normalize() here
    midnight = datetime.combine(time_nz, datetime_time(0, 0)) # naive
    time_start_of_day = tz.localize(midnight, is_dst=None) # aware

    return 1 + (time_nz - time_start_of_day) // timedelta(minutes=30) # Python 3

To emulate 1 + td // timedelta(minutes=30) on Python 2:

td = time_nz - time_start_of_day
assert td.days == 0
return 1 + td.seconds // 1800

If DST transition may happen at midnight in the given timezone then you could use a minimum for the start of the day:

time_start_of_day = min(tz.localize(midnight, is_dst=False),
                        tz.localize(midnight, is_dst=True))

Note: it works even if 00:00 time does not exist in the given timezone on a given date: to find the difference only the corresponding utc time matters.

Community
  • 1
  • 1
jfs
  • 399,953
  • 195
  • 994
  • 1,670
  • In New Zealand, DST transitions never happen at midnight local time, so the specific problem will not occur. Can you give a counter-example? – bjem Jul 29 '15 at 21:39
  • @bjem: 0. That is why I use `is_dst=None` in the main code section 1. You can't guarantee that it will never happen (the rules may change) 2. People with a similar question might want to use a different timezone – jfs Jul 29 '15 at 21:42
  • @bjem: If you mean that the `.replace()` problem won't occur then you are wrong. It occurs when DST transition happens in *between* midnight and the given time e.g., if `time_nz` is 9am on 5th April 2015 then `.replace()`-based code in your question will produce the wrong result. – jfs Jul 30 '15 at 15:03
  • [My original comment](http://stackoverflow.com/questions/31695958/getting-number-of-seconds-elapsed-in-wall-clock-time#comment51361286_31710545) is about the `.localize()` call (unrelated to `.replace()` issue) in my answer. `localize(is_dst=None)` is correct for the current Pacific/Auckland timezone rules. The `.replace()` call in your question is not correct. You should not use it. – jfs Jul 30 '15 at 15:11
0

You should use normalize() and localize() when dealing with wall clock arithmetic:

def half_hour_from_utc(time_utc = None):
    tz = pytz.timezone('Pacific/Auckland')
    time_nz = tz.normalize(time_utc)
    time_start_of_day = tz.localize(datetime.datetime(time_nz.year, time_nz.month, time_nz.day))
    total_secs = int((time_nz - time_start_of_day).total_seconds())
    half_hour = 1 + total_secs // 1800
    print('%s %s %s %s\n' % (time_nz, time_start_of_day, total_secs, half_hour))
  • normalize() = convert datetime with timezone to datetime with another timezone.
  • localize() = convert datetime without timezone to datetime with timezone.

These methods take into account the necessary logic for computing the correct datetime with timezone.

Simeon Visser
  • 118,920
  • 18
  • 185
  • 180
  • That did the job. Thanks. I have seen all those methods but I am trying to get my head around them all. – bjem Jul 29 '15 at 11:34
  • it is a coincidence that `.normalize()` works in this case; it has a different purpose. [Use `.astimezone()` instead](http://stackoverflow.com/a/31710545/4279). – jfs Jul 29 '15 at 20:39
  • `tz.localize()` is equivalent to `tz.localize(is_dst=False)` and therefore may return a wrong time in some timezones. [Use `is_dst=None` to assert that DST transition can't happen at midnight or take the earliest time as the start of the day](http://stackoverflow.com/a/31710545/4279). – jfs Jul 29 '15 at 20:56