Calculating timedeltas across daylight saving - python

I'm facing a python timezones problem and am unsure of what is the right approach to deal with it. I have to calculate timedeltas from given start and end DateTime objects. It can happen that daylight saving time will change during the runtime of my events, so I have to take that into account.
So far I've learned that for this to work I need to save my start and end times as timezone aware DateTime objects rather than regular UTC DateTimes.
I've been looking into DateTime.tzinfo, pytz,and dateutil but from what I understand these are all mostly focused on localised display of UTC DateTime objects or calculating the offsets between different timezones. Other helpers I found expect the timezone as a UTC offset, so would already require me to know if a date is affected by daylight saving or not.
So, I guess my question is: Is there a way so save a DateTime as "Central Europe" and have it be aware of daytime savings when doing calculations with them? Or, if not, what would be the established way to check if two DateTime objects are within daylight saving, so I can manually adjust the result if necessary?
I'd be grateful for any pointers.

You just need to produce an aware (localised) datetime instance, then any calculation you do with it will take DST into account. Here as an example with pytz:
>>> import pytz
>>> from datetime import *
>>> berlin = pytz.timezone('Europe/Berlin')
>>> d1 = berlin.localize(datetime(2023, 3, 25, 12))
datetime.datetime(2023, 3, 25, 12, 0, tzinfo=<DstTzInfo 'Europe/Berlin' CET+1:00:00 STD>)
>>> d2 = berlin.localize(datetime(2023, 3, 26, 12))
datetime.datetime(2023, 3, 26, 12, 0, tzinfo=<DstTzInfo 'Europe/Berlin' CEST+2:00:00 DST>)
>>> d2 - d1
datetime.timedelta(seconds=82800)
>>> (d2 - d1).total_seconds() / 60 / 60
23.0

Related

Calculate hours difference between timezone and UTC based timezone name in following format: America/New_York

I have a library which returns the timezone of a location in the format: America/New_York. Based on that timezone name, I want to calculate the hours between that time zone and UTC, taking into account daylight savings time and all. I'm using Python.
My first idea was to use the Google python library and search for 'America/New_York time' but that only gave me back a list of urls which I could visit to get the info myself. It would be awesome if I could get the current time seen if I were to manually search 'America/New_York time' into google, right into my program.
I'm sure this question has been asked before, but I am new to stack overflow and python so help is appreciated.
Thanks!
The offset from UTC depends on the date (since daylight saving time may or may not be in effect). So you need to provide a datetime for the comparison.
ZoneInfo.utcoffset will return a timedelta object directly.
>>> from zoneinfo import ZoneInfo
>>> from datetime import datetime
>>> ZoneInfo("America/New_York").utcoffset(datetime(2021, 10, 23)) #EDT
datetime.timedelta(days=-1, seconds=72000)
>>> ZoneInfo("America/New_York").utcoffset(datetime(2021, 11, 15)) #EST
datetime.timedelta(days=-1, seconds=68400)
>>> ZoneInfo("Asia/Tokyo").utcoffset(datetime(2021, 10, 23))
datetime.timedelta(seconds=32400)
Not a complete answer, but maybe you could implement a dictionary that connects these format to the normal format with three letters. With this you can then use datetime and pytz to make the rest. If you don't have too many possible outputs in the current format this would be feasible, otherwise of course not.
>>> from datetime import datetime, timedelta
>>> from datetime import timezone
>>> from zoneinfo import ZoneInfo
>>> dt1 = datetime(2020, 11, 1, 8, tzinfo=timezone.utc)
>>> dt2 = datetime(2020, 11, 1, 8, tzinfo=ZoneInfo("America/New_York"))
>>> dt2 - dt1
datetime.timedelta(seconds=18000)
>>>
Note that the difference will be four for five hours depending on whether daylight saving time is in effect or not.

Given a UTC time, get a specified timezone's midnight

Note this is not quite the same as this question. That question assumes the time you want is "now", which is not the same as for an arbitrary point in time.
I have a UTC, aware, datetime object, call it point_in_time (e.g. datetime(2017, 3, 12, 16, tzinfo=tz.tzutc())).
I have a timezone, call it location (e.g. 'US/Pacific'), because I care about where it is, but its hours offset from UTC may change throughout the year with daylight savings and whatnot.
I want to
1) get the date of point_in_time if I'm standing in location,
2) get midnight of that date if I'm standing in location.
===
I tried to simply use .astimezone(timezone('US/Pacific')) and then .replace(hours=0, ...) to move to midnight, but as you might notice about my example point_in_time, the midnight for that date is on the other side of a daylight savings switch!
The result was that I got a time representing UTC datetime(2017, 3, 12, 7), instead of a time representing UTC datetime(2017, 3, 12, 8), which is the true midnight.
EDIT:
I'm actually thinking the difference between mine and the linked question is that I'm looking for the most recent midnight in the past. That question's answer seems to be able to give a midnight that could be in the past or future, perhaps?
Your example highlights the perils of doing datetime arithmetic in a local time zone.
You can probably achieve this using pytz's normalize() function, but here's the method that occurs to me:
point_in_time = datetime(2017, 3, 12, 16, tzinfo=pytz.utc)
pacific = pytz.timezone("US/Pacific")
pacific_time = point_in_time.astimezone(pacific)
pacific_midnight_naive = pacific_time.replace(hour=0, tzinfo=None)
pacific_midnight_aware = pacific.localize(pacific_midnight_naive)
pacific_midnight_aware.astimezone(pytz.utc) # datetime(2017, 3, 12, 8)
In other words, you first convert to Pacific time to figure out the right date; then you convert again from midnight on that date to get the correct local time.
Named timezones such as "US/Pacific" are by definition daylight-savings aware. If you wish to use a fixed non-daylight-savings-aware offset from GMT you can use the timezones "Etc/GMT+*", where * is the desired offset. For example for US Pacific Standard Time you would use "Etc/GMT+8":
import pandas as pd
point_in_time = pd.to_datetime('2017-03-12 16:00:00').tz_localize('UTC')
# not what you want
local_time = point_in_time.tz_convert("US/Pacific")
(local_time - pd.Timedelta(hours=local_time.hour)).tz_convert('UTC')
# Timestamp('2017-03-12 07:00:00+0000', tz='UTC')
# what you want
local_time = point_in_time.tz_convert("Etc/GMT+8")
(local_time - pd.Timedelta(hours=local_time.hour)).tz_convert('UTC')
# Timestamp('2017-03-12 08:00:00+0000', tz='UTC')
See the docs at http://pvlib-python.readthedocs.io/en/latest/timetimezones.html for more info.
EDIT Now that I think about it, Midnight PST will always be 8am UTC, so you could simplify this as
if point_in_time.hour >=8:
local_midnight = point_in_time - point_in_time.hour + 8
else:
local_midnight = point_in_time - point_in_time.hour - 16

In Python, how to get a localized timestamp with only the datetime package?

I have a unix timestamp in seconds (such as 1294778181) that I can convert to UTC using
from datetime import datetime
datetime.utcfromtimestamp(unix_timestamp)
Problem is, I would like to get the corresponding time in 'US/Eastern' (considering any DST) and I cannot use pytz and other utilities.
Only datetime is available to me.
Is that possible?
Thanks!
Easiest, but not supersmart solution is using timedelta
import datetime
>>> now = datetime.datetime.utcnow()
US/Eastern is 5 hours behind UTC, so let's just create thouse five hours as a timedelta object and make it negative, so that when reading back our code we can see that the offset is -5 and that there's no magic to deciding when to add and when to subtract timezone offset
>>> eastern_offset = -(datetime.timedelta(hours=5))
>>> eastern = now + eastern_offset
>>> now
datetime.datetime(2016, 8, 26, 20, 7, 12, 375841)
>>> eastern
datetime.datetime(2016, 8, 26, 15, 7, 12, 375841)
If we wanted to fix DST, we'd run the datetime through smoething like this (not entirely accurate, timezones are not my expertise (googling a bit now it changes each year, yuck))
if now.month > 2 and now.month < 12:
if (now.month == 3 and now.day > 12) or (now.month == 11 and now.day < 5):
eastern.offset(datetime.timedelta(hours=5))
You could go even into more detail, add hours, find out how exactly it changes each year... I'm not going to go through all that :)

Python: Figure out local timezone

I want to compare UTC timestamps from a log file with local timestamps. When creating the local datetime object, I use something like:
>>> local_time=datetime.datetime(2010, 4, 27, 12, 0, 0, 0,
tzinfo=pytz.timezone('Israel'))
I want to find an automatic tool that would replace thetzinfo=pytz.timezone('Israel') with the current local time zone.
Any ideas?
In Python 3.x, local timezone can be figured out like this:
>>> import datetime
>>> print(datetime.datetime.now(datetime.timezone.utc).astimezone().tzinfo)
AEST
It's a tricky use of datetime's code .
For python < 3.6, you'll need
>>> import datetime
>>> print(datetime.datetime.now(datetime.timezone(datetime.timedelta(0))).astimezone().tzinfo)
AEST
Try dateutil, which has a tzlocal type that does what you need.
to compare UTC timestamps from a log file with local timestamps.
It is hard to find out Olson TZ name for a local timezone in a portable manner. Fortunately, you don't need it to perform the comparison.
tzlocal module returns a pytz timezone corresponding to the local timezone:
from datetime import datetime
import pytz # $ pip install pytz
from tzlocal import get_localzone # $ pip install tzlocal
tz = get_localzone()
local_dt = tz.localize(datetime(2010, 4, 27, 12, 0, 0, 0), is_dst=None)
utc_dt = local_dt.astimezone(pytz.utc) #NOTE: utc.normalize() is unnecessary here
Unlike other solutions presented so far the above code avoids the following issues:
local time can be ambiguous i.e., a precise comparison might be impossible for some local times
utc offset can be different for the same local timezone name for dates in the past. Some libraries that support timezone-aware datetime objects (e.g., dateutil) fail to take that into account
Note: to get timezone-aware datetime object from a naive datetime object, you should use*:
local_dt = tz.localize(datetime(2010, 4, 27, 12, 0, 0, 0), is_dst=None)
instead of:
#XXX fails for some timezones
local_dt = datetime(2010, 4, 27, 12, 0, 0, 0, tzinfo=tz)
*is_dst=None forces an exception if given local time is ambiguous or non-existent.
If you are certain that all local timestamps use the same (current) utc offset for the local timezone then you could perform the comparison using only stdlib:
# convert a naive datetime object that represents time in local timezone to epoch time
timestamp1 = (datetime(2010, 4, 27, 12, 0, 0, 0) - datetime.fromtimestamp(0)).total_seconds()
# convert a naive datetime object that represents time in UTC to epoch time
timestamp2 = (datetime(2010, 4, 27, 9, 0) - datetime.utcfromtimestamp(0)).total_seconds()
timestamp1 and timestamp2 can be compared directly.
Note:
timestamp1 formula works only if the UTC offset at epoch (datetime.fromtimestamp(0)) is the same as now
fromtimestamp() creates a naive datetime object in the current local timezone
utcfromtimestamp() creates a naive datetime object in UTC.
I was asking the same to myself, and I found the answer in 1:
Take a look at section 8.1.7: the format "%z" (lowercase, the Z uppercase returns also the time zone, but not in the 4-digit format, but in the form of timezone abbreviations, like in [3]) of strftime returns the form "+/- 4DIGIT" that is standard in email headers (see section 3.3 of RFC 2822, see [2], which obsoletes the other ways of specifying the timezone for email headers).
So, if you want your timezone in this format, use:
time.strftime("%z")
[1] http://docs.python.org/2/library/datetime.html
[2] https://www.rfc-editor.org/rfc/rfc2822#section-3.3
[3] Timezone abbreviations: http://en.wikipedia.org/wiki/List_of_time_zone_abbreviations , only for reference.
The following appears to work for 3.7+, using standard libs:
from datetime import timedelta
from datetime import timezone
import time
def currenttz():
if time.daylight:
return timezone(timedelta(seconds=-time.altzone),time.tzname[1])
else:
return timezone(timedelta(seconds=-time.timezone),time.tzname[0])
First get pytz and tzlocal modules
pip install pytz tzlocal
then
from tzlocal import get_localzone
local = get_localzone()
then you can do things like
from datetime import datetime
print(datetime.now(local))
Here's a way to get the local timezone using only the standard library, (only works in a *nix environment):
>>> '/'.join(os.path.realpath('/etc/localtime').split('/')[-2:])
'Australia/Sydney'
You can use this to create a pytz timezone:
>>> import pytz
>>> my_tz_name = '/'.join(os.path.realpath('/etc/localtime').split('/')[-2:])
>>> my_tz = pytz.timezone(my_tz_name)
>>> my_tz
<DstTzInfo 'Australia/Sydney' LMT+10:05:00 STD>
...which you can then apply to a datetime:
>>> import datetime
>>> now = datetime.datetime.now()
>>> now
datetime.datetime(2014, 9, 3, 9, 23, 24, 139059)
>>> now.replace(tzinfo=my_tz)
>>> now
datetime.datetime(2014, 9, 3, 9, 23, 24, 139059, tzinfo=<DstTzInfo 'Australia/Sydney' LMT+10:05:00 STD>)
Here's a slightly more concise version of #vbem's solution:
from datetime import datetime as dt
dt.utcnow().astimezone().tzinfo
The only substantive difference is that I replaced datetime.datetime.now(datetime.timezone.utc) with datetime.datetime.utcnow(). For brevity, I also aliased datetime.datetime as dt.
For my purposes, I want the UTC offset in seconds. Here's what that looks like:
dt.utcnow().astimezone().utcoffset().total_seconds()
Avoiding non-standard module (seems to be a missing method of datetime module):
from datetime import datetime
utcOffset_min = int(round((datetime.now() - datetime.utcnow()).total_seconds())) / 60 # round for taking time twice
utcOffset_h = utcOffset_min / 60
assert(utcOffset_min == utcOffset_h * 60) # we do not handle 1/2 h timezone offsets
print 'Local time offset is %i h to UTC.' % (utcOffset_h)
To create an ISO formatted string that includes the ISO representation of your local time zone in Israel (+04:00) :
on a server in Israel:
>>> datetime.now(datetime.now().astimezone().tzinfo).isoformat()
'2021-09-07T01:02.030042+04:00'
This will create a "timezone aware" date object that will compare to any other datetime object in UTC or local time appropriately. But the time zone ISO representation (and the date/time string itself) will change if you ran this on a server in San Francisco at the exact same time, as I did:
on a server in San Francisco, CA, USA (Pacific):
>>> datetime.now(datetime.now().astimezone().tzinfo).isoformat()
'2021-09-06T14:01:02.030042-07:00'
The datetime objects in in both cases would be compatible with each other. So if you subtracted them you'd get a time delta of 0:
On a server anywhere in Python3.6+:
>>> (datetime.fromisoformat('2021-09-06T14:01:02.030042-07:00') -
... datetime.fromisoformat('2021-09-07T01:01:02.030042+04:00'))
datetime.timedelta(0)
Based on Thoku's answer above, here's an answer that resolves the time zone to the nearest half hour (which is relevant for some timezones eg South Australia's) :
from datetime import datetime
round((round((datetime.now()-datetime.utcnow()).total_seconds())/1800)/2)
Based on J. F. Sebastian's answer, you can do this with the standard library:
import time, datetime
local_timezone = datetime.timezone(datetime.timedelta(seconds=-time.timezone))
Tested in 3.4, should work on 3.4+
You may be happy with pendulum
>>> pendulum.datetime(2015, 2, 5, tz='local').timezone.name
'Israel'
Pendulum has a well designed API for manipulating dates. Everything is TZ-aware.
I have also been looking for a simple way to read the local host configuration and get timezone aware local_time based on it. As of python 3.6+ the simplest approach is use dateutil.tz which will read /etc/localtime and assist in getting timezone aware datetime object.
Here is more info on it: https://dateutil.readthedocs.io/en/stable/tz.html
The implementation to accomplish what you're looking for is as follows:
from datetime import datetime
from dateutil import tz
local_time = datetime.now(tz.gettz())
This will provide you the following local_time:
2019-10-18 13:41:06.624536-05:00
Additional Resources I used in researching this topic:
Paul Ganssle Presentation about time zones:
https://www.youtube.com/watch?v=l4UCKCo9FWY
pytz: The Fastest Footgun in the West
https://blog.ganssle.io/articles/2018/03/pytz-fastest-footgun.html
I want to compare UTC timestamps from a log file with local timestamps
If this is your intent, then I wouldn't worry about specifying specific tzinfo parameters or any additional external libraries. Since Python 3.5, the built in datetime module is all you need to create a UTC and a local timestamp automatically.
import datetime
f = "%a %b %d %H:%M:%S %Z %Y" # Full format with timezone
# tzinfo=None
cdatetime = datetime.datetime(2010, 4, 27, 12, 0, 0, 0) # 1. Your example from log
cdatetime = datetime.datetime.now() # 2. Basic date creation (default: local time)
print(cdatetime.strftime(f)) # no timezone printed
# Tue Apr 27 12:00:00 2010
utctimestamp = cdatetime.astimezone(tz=datetime.timezone.utc) # 1. convert to UTC
utctimestamp = datetime.datetime.now(tz=datetime.timezone.utc) # 2. create in UTC
print(utctimestamp.strftime(f))
# Tue Apr 27 17:00:00 UTC 2010
localtimestamp = cdatetime.astimezone() # 1. convert to local [default]
localtimestamp = datetime.datetime.now().astimezone() # 2. create with local timezone
print(localtimestamp.strftime(f))
# Tue Apr 27 12:00:00 CDT 2010
The '%Z' parameter of datetime.strftime() prints the timezone acronym into the timestamp for humans to read.
For simple things, the following tzinfo implementation can be used, which queries the OS for time zone offsets:
import datetime
import time
class LocalTZ(datetime.tzinfo):
_unixEpochOrdinal = datetime.datetime.utcfromtimestamp(0).toordinal()
def dst(self, dt):
return datetime.timedelta(0)
def utcoffset(self, dt):
t = (dt.toordinal() - self._unixEpochOrdinal)*86400 + dt.hour*3600 + dt.minute*60 + dt.second + time.timezone
utc = datetime.datetime(*time.gmtime(t)[:6])
local = datetime.datetime(*time.localtime(t)[:6])
return local - utc
print datetime.datetime.now(LocalTZ())
print datetime.datetime(2010, 4, 27, 12, 0, 0, tzinfo=LocalTZ())
# If you're in the EU, the following datetimes are right on the DST change.
print datetime.datetime(2013, 3, 31, 0, 59, 59, tzinfo=LocalTZ())
print datetime.datetime(2013, 3, 31, 1, 0, 0, tzinfo=LocalTZ())
print datetime.datetime(2013, 3, 31, 1, 59, 59, tzinfo=LocalTZ())
# The following datetime is invalid, as the clock moves directly from
# 01:59:59 standard time to 03:00:00 daylight savings time.
print datetime.datetime(2013, 3, 31, 2, 0, 0, tzinfo=LocalTZ())
print datetime.datetime(2013, 10, 27, 0, 59, 59, tzinfo=LocalTZ())
print datetime.datetime(2013, 10, 27, 1, 0, 0, tzinfo=LocalTZ())
print datetime.datetime(2013, 10, 27, 1, 59, 59, tzinfo=LocalTZ())
# The following datetime is ambigous, as 02:00 can be either DST or standard
# time. (It is interpreted as standard time.)
print datetime.datetime(2013, 10, 27, 2, 0, 0, tzinfo=LocalTZ())
tzlocal from dateutil.
Code example follows. Last string suitable for use in filenames.
>>> from datetime import datetime
>>> from dateutil.tz import tzlocal
>>> str(datetime.now(tzlocal()))
'2015-04-01 11:19:47.980883-07:00'
>>> str(datetime.now(tzlocal())).replace(' ','-').replace(':','').replace('.','-')
'2015-04-01-111947-981879-0700'
>>>
First, note that the question presents an incorrect initialization of an aware datetime object:
>>> local_time=datetime.datetime(2010, 4, 27, 12, 0, 0, 0,
... tzinfo=pytz.timezone('Israel'))
creates an invalid instance. One can see the problem by computing the UTC offset of the resulting object:
>>> print(local_time.utcoffset())
2:21:00
(Note the result which is an odd fraction of an hour.)
To initialize an aware datetime properly using pytz one should use the localize() method as follows:
>>> local_time=pytz.timezone('Israel').localize(datetime.datetime(2010, 4, 27, 12))
>>> print(local_time.utcoffset())
3:00:00
Now, if you require a local pytz timezone as the new tzinfo, you should use the tzlocal package as others have explained, but if all you need is an instance with a correct local time zone offset and abbreviation then tarting with Python 3.3, you can call the astimezone() method with no arguments to convert an aware datetime instance to your local timezone:
>>> local_time.astimezone().strftime('%Y-%m-%d %H:%M %Z %z')
'2010-04-27 05:00 EDT -0400'
now_dt = datetime.datetime.now()
utc_now = datetime.datetime.utcnow()
now_ts, utc_ts = map(time.mktime, map(datetime.datetime.timetuple, (now_dt, utc_now)))
offset = int((now_ts - utc_ts) / 3600)
hope this will help you.

Python datetime not including DST when using pytz timezone

If I convert a UTC datetime to swedish format, summertime is included (CEST). However, while creating a datetime with sweden as the timezone, it gets CET instead of CEST. Why is this?
>>> # Modified for readability
>>> import pytz
>>> import datetime
>>> sweden = pytz.timezone('Europe/Stockholm')
>>>
>>> datetime.datetime(2010, 4, 20, 16, 20, tzinfo=pytz.utc).astimezone(sweden)
datetime(2010, 4, 20, 18, 20, tzinfo=<... 'Europe/Stockholm' CEST+2:00:00 DST>)
>>>
>>> datetime.datetime(2010, 4, 20, 18, 20, tzinfo=sweden)
datetime(2010, 4, 20, 18, 20, tzinfo=<... 'Europe/Stockholm' CET+1:00:00 STD>)
>>>
The sweden object specifies the CET time zone by default but contains enough information to know when CEST starts and stop.
In the first example, you create a datetime object and convert it to local time. The sweden object knows that the UTC time you passed occurs during daylight savings time and can convert it appropriately.
In the second example, the datetime constructor always interprets your input as not-daylight-savings-time and returns an appropriate object.
If datetime treated your input as wall-clock time and chose the appropriate daylight-savings setting for you, there would be an ambiguity during the time of year when clocks are set back. On a wall-clock the same hour occurs twice. Hence, datetime forces you to specify which timezone you're using when you create the datetime object.
Timezone abbreviations are not unique. For example "IST" could refer to "Irish Standard Time", "Iranian Standard Time", "Indian Standard Time" or "Isreali Standard Time". You shouldn't rely on parsing that, and instead should use zoneinfo timezones.

Categories

Resources