4

I would like to calculate how many hours there are in a date interval: for example "2014.03.29-30" should give 47, because of the daylight savings.

My method is making two datetime objects, in the example the following:

datetime.datetime(2014, 3, 29, 0, 0, tzinfo=<DstTzInfo 'Europe/Budapest' LMT+1:16:00 STD>)
datetime.datetime(2014, 3, 30, 23, 59, tzinfo=<DstTzInfo 'Europe/Budapest' LMT+1:16:00 STD>)
return (date2-date1) + timedelta(minutes=1)

However, it gives "2 days, 0:00:00", which is not correct. How could I make a timedelta object which takes timezones and dst into account? Also, if there's a simpler solution for the whole problem, I'm open to it.

Thank you!

Rolf
  • 331
  • 4
  • 13

1 Answers1

5

Before 1901-12-13 20:45:52 UTC, the 'Europe/Budapest' timezone was LMT+1:16:00 STD. Currently, as of 2016-05-05, the 'Europe/Budapest' timezone is CET+2:00:00 DST.

If you use pytz's localize method, then pytz will choose the timezone (utcoffset and dstoffset) for 'Europe/Budapest' which is appropriate for the given naive datetime:

import datetime as DT
import pytz

tzone = pytz.timezone('Europe/Budapest')
date1 = tzone.localize(DT.datetime(2014, 3, 29, 0, 0), is_dst=None)
# datetime.datetime(2014, 3, 29, 0, 0, tzinfo=<DstTzInfo 'Europe/Budapest' CET+1:00:00 STD>)

In contrast, if you supply tzinfo=tzone directly to datetime.datetime, as in:

wrong_date1 = datetime.datetime(2014, 3, 29, 0, 0, tzinfo=tzone)
# datetime.datetime(2014, 3, 29, 0, 0, tzinfo=<DstTzInfo 'Europe/Budapest' LMT+1:16:00 STD>)

then the datetime.datetime incorrectly chooses the very first timezone associated with 'Europe/Budapest' regardless of whether or not that was the timezone in effect on 2014-3-29.

Therefore, when using pytz, always use tzone.localize to make naive datetimes timezone-aware:

import datetime as DT
import pytz
tzone = pytz.timezone('Europe/Budapest')
date1 = tzone.localize(DT.datetime(2014, 3, 29, 0, 0), is_dst=None)
date2 = tzone.localize(DT.datetime(2014, 3, 30, 23, 59), is_dst=None)
print(((date2-date1) + DT.timedelta(minutes=1)).total_seconds()/3600.)
# 47.0

Do not use tzinfo=tzone unless tzone is pytz.utc (or a timezone which is alway the same throughout its history.)


Where did the date 1901-12-13 20:45:52 UTC come from?

You can peek at a pytz timezone's utc transition times (and associated transition info) using its tzone._utc_transition_times and tzone._transition_info private attributes:

In [43]: [(utcdate, utcoffset, dstoffset, tzabbrev) for utcdate, (utcoffset, dstoffset, tzabbrev) in zip(tzone._utc_transition_times, tzone._transition_info)][:2]
Out[43]: 
[(datetime.datetime(1, 1, 1, 0, 0),
  datetime.timedelta(0, 4560),
  datetime.timedelta(0),
  'LMT'),
 (datetime.datetime(1901, 12, 13, 20, 45, 52),
  datetime.timedelta(0, 3600),
  datetime.timedelta(0),
  'CET')]

This shows that from the date 1-1-1 UTC to 1901-12-13 20:45:52 UTC the timezone abbreviation was LMT and the utcoffset was 4560 seconds, which equals 1 hour and 16 minutes:

In [47]: print(DT.timedelta(0, 4560))
1:16:00

Hence the first timezone associated with 'Europe/Budapest' is LMT+1:16:00 STD.

unutbu
  • 842,883
  • 184
  • 1,785
  • 1,677
  • Thank you! One last thing: what does the `is_dst=None` part do? I accidentally skipped it, but it works as it should, too. – Rolf May 05 '16 at 18:20
  • 2
    `is_dst=None` tells `tzone.localize` to raise an `AmbiguousTimeError` for ambiguous times when Daylight Savings Time is ending -- "falling back" causes the same naive time to occur twice. Otherwise it does what is appropriate. It is used out of "an abundance of caution", as a good general habit, even though it has no effect here. By default, `is_dst=False`, which means `tzone.localize` will resolve ambiguous times by *assuming* that the naive datetime refers to the datetime when Daylight Savings Time is not in effect. – unutbu May 05 '16 at 19:21
  • `is_dst=None` may also raise a `NonExistentTimeError` when Daylight Savings Time is starting. For example, if `pacific = pytz.timezone('US/Pacific')`, `date = DT.datetime(2008, 3, 9, 2, 0, 0)`, then `pacific.localize(date, is_dst=None)` raises `NonExistentTimeError`, even though `pacific.normalize(pacific.localize(date, is_dst=True))` (or the same with `is_dst=False`) returns a date (1 o'clock or 3 o'clock, but never 2 o'clock). – unutbu May 05 '16 at 20:06
  • Thanks! Great tip not to use the conventional `datetime.datetime(2014, 3, 29, 0, 0, tzinfo=tzone)` An absolute lifesaver! – NicoE May 30 '22 at 20:32